diff --git a/src/core/src/main/kotlin/java/Custom.kt b/src/core/src/main/kotlin/java/Custom.kt index b1fc6be..025b8b9 100644 --- a/src/core/src/main/kotlin/java/Custom.kt +++ b/src/core/src/main/kotlin/java/Custom.kt @@ -3,6 +3,7 @@ package solutions.bitbadger.documents.java import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Parameter +import java.io.PrintWriter import java.sql.Connection import java.sql.ResultSet import java.sql.SQLException @@ -87,6 +88,46 @@ object Custom { fun jsonArray(query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String) = Configuration.dbConn().use { jsonArray(query, parameters, it, mapFunc) } + /** + * Execute a query, writing its JSON array of results to the given `PrintWriter` + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + conn: Connection, + mapFunc: (ResultSet) -> String + ) = Parameters.apply(conn, query, parameters).use { Results.writeJsonArray(writer, it, mapFunc) } + + /** + * Execute a query, writing its JSON array of results to the given `PrintWriter` (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + mapFunc: (ResultSet) -> String + ) = Configuration.dbConn().use { writeJsonArray(query, parameters, writer, it, mapFunc) } + /** * Execute a query that returns one or no results * diff --git a/src/core/src/main/kotlin/java/Results.kt b/src/core/src/main/kotlin/java/Results.kt index c71557c..0e98fc3 100644 --- a/src/core/src/main/kotlin/java/Results.kt +++ b/src/core/src/main/kotlin/java/Results.kt @@ -3,6 +3,7 @@ package solutions.bitbadger.documents.java import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException +import java.io.PrintWriter import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.SQLException @@ -133,4 +134,34 @@ object Results { } catch (ex: SQLException) { throw DocumentException("Error retrieving documents from query: ${ex.message}", ex) } + + /** + * Write a JSON array of items for the results of the given command to the given `PrintWriter`, using the specified + * mapping function + * + * @param writer The writer for the results of the query + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to JSON text + * @return A string with a JSON array of documents from the query's result + * @throws DocumentException If there is a problem executing the query (unchecked) + */ + @JvmStatic + fun writeJsonArray(writer: PrintWriter, stmt: PreparedStatement, mapFunc: (ResultSet) -> String) = + try { + writer.write("[") + stmt.executeQuery().use { + var isFirst = true + while (it.next()) { + if (isFirst) { + isFirst = false + } else { + writer.write(",") + } + writer.write(mapFunc(it)) + } + } + writer.write("]") + } catch (ex: SQLException) { + throw DocumentException("Error writing documents from query: ${ex.message}", ex) + } } diff --git a/src/core/src/main/kotlin/java/extensions/Connection.kt b/src/core/src/main/kotlin/java/extensions/Connection.kt index f5c9ac1..cb163b3 100644 --- a/src/core/src/main/kotlin/java/extensions/Connection.kt +++ b/src/core/src/main/kotlin/java/extensions/Connection.kt @@ -4,6 +4,7 @@ package solutions.bitbadger.documents.java.extensions import solutions.bitbadger.documents.* import solutions.bitbadger.documents.java.* +import java.io.PrintWriter import java.sql.Connection import java.sql.ResultSet import kotlin.jvm.Throws @@ -26,8 +27,7 @@ fun Connection.customList( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> TDoc -) = - Custom.list(query, parameters, clazz, this, mapFunc) +) = Custom.list(query, parameters, clazz, this, mapFunc) /** * Execute a query that returns a JSON array of results @@ -43,8 +43,25 @@ fun Connection.customJsonArray( query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String -) = - Custom.jsonArray(query, parameters, this, mapFunc) +) = Custom.jsonArray(query, parameters, this, mapFunc) + +/** + * Execute a query, writing its JSON array of results to the given `PrintWriter` (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ +@Throws(DocumentException::class) +fun Connection.writeCustomJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + mapFunc: (ResultSet) -> String +) = Custom.writeJsonArray(query, parameters, writer, this, mapFunc) /** * Execute a query that returns one or no results @@ -62,8 +79,7 @@ fun Connection.customSingle( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> TDoc -) = - Custom.single(query, parameters, clazz, this, mapFunc) +) = Custom.single(query, parameters, clazz, this, mapFunc) /** * Execute a query that returns JSON for one or no documents @@ -79,8 +95,7 @@ fun Connection.customJsonSingle( query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String -) = - Configuration.dbConn().use { Custom.jsonSingle(query, parameters, it, mapFunc) } +) = Configuration.dbConn().use { Custom.jsonSingle(query, parameters, it, mapFunc) } /** * Execute a query that returns no results @@ -110,8 +125,7 @@ fun Connection.customScalar( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> T -) = - Custom.scalar(query, parameters, clazz, this, mapFunc) +) = Custom.scalar(query, parameters, clazz, this, mapFunc) // ~~~ DEFINITION QUERIES ~~~ @@ -591,8 +605,7 @@ fun Connection.patchByFields( fields: Collection>, patch: TPatch, howMatched: FieldMatch? = null -) = - Patch.byFields(tableName, fields, patch, howMatched, this) +) = Patch.byFields(tableName, fields, patch, howMatched, this) /** * Patch documents using a JSON containment query (PostgreSQL only) @@ -607,8 +620,7 @@ fun Connection.patchByContains( tableName: String, criteria: TContains, patch: TPatch -) = - Patch.byContains(tableName, criteria, patch, this) +) = Patch.byContains(tableName, criteria, patch, this) /** * Patch documents using a JSON Path match query (PostgreSQL only) @@ -652,8 +664,7 @@ fun Connection.removeFieldsByFields( fields: Collection>, toRemove: Collection, howMatched: FieldMatch? = null -) = - RemoveFields.byFields(tableName, fields, toRemove, howMatched, this) +) = RemoveFields.byFields(tableName, fields, toRemove, howMatched, this) /** * Remove fields from documents using a JSON containment query (PostgreSQL only) @@ -668,8 +679,7 @@ fun Connection.removeFieldsByContains( tableName: String, criteria: TContains, toRemove: Collection -) = - RemoveFields.byContains(tableName, criteria, toRemove, this) +) = RemoveFields.byContains(tableName, criteria, toRemove, this) /** * Remove fields from documents using a JSON Path match query (PostgreSQL only) diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java index a2a4d55..f0a2dc9 100644 --- a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java @@ -7,6 +7,8 @@ import solutions.bitbadger.documents.query.CountQuery; import solutions.bitbadger.documents.query.DeleteQuery; import solutions.bitbadger.documents.query.FindQuery; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.Collection; import java.util.List; @@ -55,6 +57,35 @@ final public class CustomFunctions { "A multiple document list was not represented correctly"); } + public static void writeJsonArrayEmpty(ThrowawayDatabase db) throws DocumentException { + assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "The test table should be empty"); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), writer, Results::jsonFromData); + assertEquals("[]", output.toString(), "An empty list was not represented correctly"); + } + + public static void writeJsonArraySingle(ThrowawayDatabase db) throws DocumentException { + insert(db.getConn(), TEST_TABLE, new ArrayDocument("one", List.of("2", "3"))); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), writer, Results::jsonFromData); + assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), output.toString(), + "A single document list was not represented correctly"); + } + + public static void writeJsonArrayMany(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE) + orderBy(List.of(Field.named("id"))), List.of(), + writer, Results::jsonFromData); + assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"first\",\"values\":[\"a\",\"b\",\"c\"]}," + + "{\"id\":\"second\",\"values\":[\"c\",\"d\",\"e\"]}," + + "{\"id\":\"third\",\"values\":[\"x\",\"y\",\"z\"]}]"), + output.toString(), "A multiple document list was not represented correctly"); + } + public static void singleNone(ThrowawayDatabase db) throws DocumentException { assertFalse( customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData) diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java index a2d8f5c..3f12577 100644 --- a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java @@ -51,6 +51,30 @@ final public class PostgreSQLCustomIT { } } + @Test + @DisplayName("writeJsonArray succeeds with empty array") + public void writeJsonArrayEmpty() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.writeJsonArrayEmpty(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + public void writeJsonArraySingle() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.writeJsonArraySingle(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + public void writeJsonArrayMany() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.writeJsonArrayMany(db); + } + } + @Test @DisplayName("single succeeds when document not found") public void singleNone() throws DocumentException { diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java index 66bc246..b2e25bf 100644 --- a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java @@ -51,6 +51,30 @@ final public class SQLiteCustomIT { } } + @Test + @DisplayName("writeJsonArray succeeds with empty array") + public void writeJsonArrayEmpty() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.writeJsonArrayEmpty(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + public void writeJsonArraySingle() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.writeJsonArraySingle(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + public void writeJsonArrayMany() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.writeJsonArrayMany(db); + } + } + @Test @DisplayName("single succeeds when document not found") public void singleNone() throws DocumentException { diff --git a/src/core/src/test/kotlin/integration/CustomFunctions.kt b/src/core/src/test/kotlin/integration/CustomFunctions.kt index dafce1c..6ce54d4 100644 --- a/src/core/src/test/kotlin/integration/CustomFunctions.kt +++ b/src/core/src/test/kotlin/integration/CustomFunctions.kt @@ -8,6 +8,8 @@ import solutions.bitbadger.documents.query.CountQuery import solutions.bitbadger.documents.query.DeleteQuery import solutions.bitbadger.documents.query.FindQuery import solutions.bitbadger.documents.query.orderBy +import java.io.PrintWriter +import java.io.StringWriter import kotlin.test.* /** @@ -42,7 +44,7 @@ object CustomFunctions { fun jsonArraySingle(db: ThrowawayDatabase) { db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3"))) assertEquals( - JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), + JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), db.conn.customJsonArray(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), "A single document list was not represented correctly" ) @@ -51,15 +53,48 @@ object CustomFunctions { fun jsonArrayMany(db: ThrowawayDatabase) { ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } assertEquals( - JsonFunctions.maybeJsonB("[{\"id\":\"first\",\"values\":[\"a\",\"b\",\"c\"]}," - + "{\"id\":\"second\",\"values\":[\"c\",\"d\",\"e\"]}," - + "{\"id\":\"third\",\"values\":[\"x\",\"y\",\"z\"]}]"), + JsonFunctions.maybeJsonB("""[{"id":"first","values":["a","b","c"]},""" + + """{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]"""), db.conn.customJsonArray(FindQuery.all(TEST_TABLE) + orderBy(listOf(Field.named("id"))), listOf(), Results::jsonFromData), "A multiple document list was not represented correctly" ) } + fun writeJsonArrayEmpty(db: ThrowawayDatabase) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), "The test table should be empty") + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), listOf(), writer, Results::jsonFromData) + assertEquals("[]", output.toString(), "An empty list was not represented correctly") + } + + fun writeJsonArraySingle(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3"))) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), listOf(), writer, Results::jsonFromData) + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), + output.toString(), + "A single document list was not represented correctly" + ) + } + + fun writeJsonArrayMany(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE) + orderBy(listOf(Field.named("id"))), listOf(), writer, + Results::jsonFromData) + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"first","values":["a","b","c"]},""" + + """{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]"""), + output.toString(), + "A multiple document list was not represented correctly" + ) + } + fun singleNone(db: ThrowawayDatabase) = assertFalse( db.conn.customSingle( diff --git a/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt index ca28dd7..d96b076 100644 --- a/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt +++ b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -35,6 +35,21 @@ class PostgreSQLCustomIT { fun jsonArrayMany() = PgDB().use(CustomFunctions::jsonArrayMany) + @Test + @DisplayName("writeJsonArray succeeds with empty array") + fun writeJsonArrayEmpty() = + PgDB().use(CustomFunctions::writeJsonArrayEmpty) + + @Test + @DisplayName("writeJsonArray succeeds with a single-item list") + fun writeJsonArraySingle() = + PgDB().use(CustomFunctions::writeJsonArraySingle) + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item list") + fun writeJsonArrayMany() = + PgDB().use(CustomFunctions::writeJsonArrayMany) + @Test @DisplayName("single succeeds when document not found") fun singleNone() = diff --git a/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt index 9eca13a..01f8ffe 100644 --- a/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -34,6 +34,21 @@ class SQLiteCustomIT { fun jsonArrayMany() = SQLiteDB().use(CustomFunctions::jsonArrayMany) + @Test + @DisplayName("writeJsonArray succeeds with empty array") + fun writeJsonArrayEmpty() = + SQLiteDB().use(CustomFunctions::writeJsonArrayEmpty) + + @Test + @DisplayName("writeJsonArray succeeds with a single-item list") + fun writeJsonArraySingle() = + SQLiteDB().use(CustomFunctions::writeJsonArraySingle) + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item list") + fun writeJsonArrayMany() = + SQLiteDB().use(CustomFunctions::writeJsonArrayMany) + @Test @DisplayName("single succeeds when document not found") fun singleNone() =