From 40f8bded81887f5a3dd114554beb90663fb5f897 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 31 Mar 2025 23:04:51 -0400 Subject: [PATCH] Add JSON writer to Scala Custom --- src/scala/src/main/scala/Custom.scala | 55 +++++++++++++++++++ src/scala/src/main/scala/Results.scala | 37 +++++++++++-- .../src/main/scala/extensions/package.scala | 27 +++++++++ .../scala/integration/CustomFunctions.scala | 33 +++++++++-- .../integration/PostgreSQLCustomIT.scala | 15 +++++ .../scala/integration/SQLiteCustomIT.scala | 15 +++++ 6 files changed, 172 insertions(+), 10 deletions(-) diff --git a/src/scala/src/main/scala/Custom.scala b/src/scala/src/main/scala/Custom.scala index 110562f..63704b4 100644 --- a/src/scala/src/main/scala/Custom.scala +++ b/src/scala/src/main/scala/Custom.scala @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.scala import solutions.bitbadger.documents.{Configuration, Parameter} +import java.io.PrintWriter import java.sql.{Connection, ResultSet} import scala.reflect.ClassTag import scala.util.Using @@ -107,6 +108,60 @@ object Custom: def jsonArray(query: String, mapFunc: ResultSet => String): String = jsonArray(query, Nil, mapFunc) + /** + * Execute a query that writes a 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 + */ + def writeJsonArray(query: String, parameters: Seq[Parameter[?]], writer: PrintWriter, conn: Connection, + mapFunc: ResultSet => String): Unit = + Using(Parameters.apply(conn, query, parameters)) { stmt => Results.writeJsonArray(writer, stmt, mapFunc) } + + /** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @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 + */ + def writeJsonArray(query: String, conn: Connection, writer: PrintWriter, mapFunc: ResultSet => String): Unit = + writeJsonArray(query, Nil, writer, conn, mapFunc) + + /** + * Execute a query that returns a JSON array of results (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 + */ + def writeJsonArray(query: String, parameters: Seq[Parameter[?]], writer: PrintWriter, + mapFunc: ResultSet => String): Unit = + Using(Configuration.dbConn()) { conn => writeJsonArray(query, parameters, writer, conn, mapFunc) } + + /** + * Execute a query that returns a JSON array of results (creates connection) + * + * @param query The query to retrieve the results + * @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 + */ + def writeJsonArray(query: String, writer: PrintWriter, mapFunc: ResultSet => String): Unit = + writeJsonArray(query, Nil, writer, mapFunc) + /** * Execute a query that returns one or no results * diff --git a/src/scala/src/main/scala/Results.scala b/src/scala/src/main/scala/Results.scala index 6845076..3b10978 100644 --- a/src/scala/src/main/scala/Results.scala +++ b/src/scala/src/main/scala/Results.scala @@ -3,6 +3,7 @@ package solutions.bitbadger.documents.scala import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.java.Results as CoreResults +import java.io.PrintWriter import java.sql.{PreparedStatement, ResultSet, SQLException} import scala.collection.mutable.ListBuffer import scala.reflect.ClassTag @@ -47,9 +48,8 @@ object Results: try val buffer = ListBuffer[Doc]() Using(stmt.executeQuery()) { rs => - while (rs.next()) { + while rs.next() do buffer.append(mapFunc(rs, tag)) - } } buffer.toList catch @@ -107,12 +107,37 @@ object Results: try val results = StringBuilder("[") Using(stmt.executeQuery()) { rs => - while (rs.next()) { + while rs.next() do if (results.length > 2) results.append(",") results.append(mapFunc(rs)) - } } results.append("]").toString() catch - case ex: SQLException => - throw DocumentException("Error retrieving documents from query: ${ex.message}", ex) + case 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) + */ + def writeJsonArray(writer: PrintWriter, stmt: PreparedStatement, mapFunc: ResultSet => String): Unit = + try + writer.write("[") + Using(stmt.executeQuery()) { rs => + var isFirst = true + while rs.next() do + if isFirst then + isFirst = false + else + writer.write(",") + writer.write(mapFunc(rs)) + } + writer.write("]") + catch + case ex: SQLException => throw DocumentException("Error writing documents from query: ${ex.message}", ex) + diff --git a/src/scala/src/main/scala/extensions/package.scala b/src/scala/src/main/scala/extensions/package.scala index e51dd15..f9fab6b 100644 --- a/src/scala/src/main/scala/extensions/package.scala +++ b/src/scala/src/main/scala/extensions/package.scala @@ -3,6 +3,7 @@ package solutions.bitbadger.documents.scala.extensions import solutions.bitbadger.documents.{DocumentIndex, Field, FieldMatch, Parameter} import solutions.bitbadger.documents.scala.* +import java.io.PrintWriter import java.sql.{Connection, ResultSet} import scala.reflect.ClassTag @@ -58,6 +59,32 @@ extension (conn: Connection) def customJsonArray(query: String, mapFunc: ResultSet => String): String = Custom.jsonArray(query, mapFunc) + /** + * Execute a query that writes a 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 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 + */ + def writeCustomJsonArray(query: String, parameters: Seq[Parameter[?]], writer: PrintWriter, + mapFunc: ResultSet => String): Unit = + Custom.writeJsonArray(query, parameters, writer, conn, mapFunc) + + /** + * Execute a query that writes a JSON array of results to the given `PrintWriter` + * + * @param query The query to retrieve the results + * @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 + */ + def writeCustomJsonArray(query: String, writer: PrintWriter, mapFunc: ResultSet => String): Unit = + Custom.writeJsonArray(query, writer, mapFunc) + /** * Execute a query that returns one or no results * diff --git a/src/scala/src/test/scala/integration/CustomFunctions.scala b/src/scala/src/test/scala/integration/CustomFunctions.scala index 32748fe..b4acc2d 100644 --- a/src/scala/src/test/scala/integration/CustomFunctions.scala +++ b/src/scala/src/test/scala/integration/CustomFunctions.scala @@ -7,6 +7,7 @@ import solutions.bitbadger.documents.scala.extensions.* import solutions.bitbadger.documents.scala.tests.TEST_TABLE import solutions.bitbadger.documents.{Configuration, Field, Parameter, ParameterType} +import java.io.{PrintWriter, StringWriter} import scala.jdk.CollectionConverters.* object CustomFunctions: @@ -29,19 +30,43 @@ object CustomFunctions: def jsonArraySingle(db: ThrowawayDatabase): Unit = db.conn.insert(TEST_TABLE, ArrayDocument("one", "2" :: "3" :: Nil)) - assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), + assertEquals(JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), db.conn.customJsonArray(FindQuery.all(TEST_TABLE), Nil, Results.jsonFromData), "A single document list was not represented correctly") def jsonArrayMany(db: ThrowawayDatabase): Unit = ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } - assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"first\",\"values\":[\"a\",\"b\",\"c\"]}," - + "{\"id\":\"second\",\"values\":[\"c\",\"d\",\"e\"]}," - + "{\"id\":\"third\",\"values\":[\"x\",\"y\",\"z\"]}]"), + assertEquals(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) + QueryUtils.orderBy((Field.named("id") :: Nil).asJava), Nil, Results.jsonFromData), "A multiple document list was not represented correctly") + def writeJsonArrayEmpty(db: ThrowawayDatabase): Unit = + 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), Nil, writer, Results.jsonFromData) + assertEquals("[]", output.toString, "An empty list was not represented correctly") + + def writeJsonArraySingle(db: ThrowawayDatabase): Unit = + db.conn.insert(TEST_TABLE, ArrayDocument("one", "2" :: "3" :: Nil)) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), Nil, writer, Results.jsonFromData) + assertEquals(JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), output.toString, + "A single document list was not represented correctly") + + def writeJsonArrayMany(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE) + QueryUtils.orderBy((Field.named("id") :: Nil).asJava), Nil, + 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") + def singleNone(db: ThrowawayDatabase): Unit = assertTrue(db.conn.customSingle[JsonDocument](FindQuery.all(TEST_TABLE), Results.fromData).isEmpty, "There should not have been a document returned") diff --git a/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala index 9c0c131..e1a6cc1 100644 --- a/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala +++ b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala @@ -32,6 +32,21 @@ class PostgreSQLCustomIT: def jsonArrayMany(): Unit = Using(PgDB()) { db => CustomFunctions.jsonArrayMany(db) } + @Test + @DisplayName("writeJsonArray succeeds with empty array") + def writeJsonArrayEmpty(): Unit = + Using(PgDB()) { db => CustomFunctions.writeJsonArrayEmpty(db) } + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + def writeJsonArraySingle(): Unit = + Using(PgDB()) { db => CustomFunctions.writeJsonArraySingle(db) } + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + def writeJsonArrayMany(): Unit = + Using(PgDB()) { db => CustomFunctions.writeJsonArrayMany(db) } + @Test @DisplayName("single succeeds when document not found") def singleNone(): Unit = diff --git a/src/scala/src/test/scala/integration/SQLiteCustomIT.scala b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala index dfbc0ca..e93bdb3 100644 --- a/src/scala/src/test/scala/integration/SQLiteCustomIT.scala +++ b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala @@ -32,6 +32,21 @@ class SQLiteCustomIT: def jsonArrayMany(): Unit = Using(SQLiteDB()) { db => CustomFunctions.jsonArrayMany(db) } + @Test + @DisplayName("writeJsonArray succeeds with empty array") + def writeJsonArrayEmpty(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.writeJsonArrayEmpty(db) } + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + def writeJsonArraySingle(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.writeJsonArraySingle(db) } + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + def writeJsonArrayMany(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.writeJsonArrayMany(db) } + @Test @DisplayName("single succeeds when document not found") def singleNone(): Unit =