Add JSON string custom functions

This commit is contained in:
2025-03-27 20:26:51 -04:00
parent 3990b40c38
commit 18ba1be191
28 changed files with 1012 additions and 16 deletions

View File

@@ -2,7 +2,7 @@ package solutions.bitbadger.documents.kotlinx
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.java.Custom as JvmCustom
import solutions.bitbadger.documents.java.Custom as CoreCustom
import java.sql.Connection
import java.sql.ResultSet
@@ -41,6 +41,35 @@ object Custom {
mapFunc: (ResultSet) -> TDoc
) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) }
/**
* Execute a query that returns a JSON array of results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @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
*/
fun jsonArray(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
conn: Connection,
mapFunc: (ResultSet) -> String
) = CoreCustom.jsonArray(query, parameters, 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 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
*/
fun jsonArray(query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> String) =
CoreCustom.jsonArray(query, parameters, mapFunc)
/**
* Execute a query that returns one or no results
*
@@ -71,6 +100,35 @@ object Custom {
noinline mapFunc: (ResultSet) -> TDoc
) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) }
/**
* Execute a query that returns JSON for one or no documents
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param conn The connection over which the query should be executed
* @param mapFunc The mapping function between the document and the domain item
* @return The JSON for the document if found, an empty object (`{}`) if not
* @throws DocumentException If parameters are invalid
*/
fun jsonSingle(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
conn: Connection,
mapFunc: (ResultSet) -> String
) = CoreCustom.jsonSingle(query, parameters, conn, mapFunc)
/**
* Execute a query that returns JSON for one or no documents (creates connection)
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param mapFunc The mapping function between the document and the domain item
* @return The JSON for the document if found, an empty object (`{}`) if not
* @throws DocumentException If parameters are invalid
*/
fun jsonSingle(query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> String) =
CoreCustom.jsonSingle(query, parameters, mapFunc)
/**
* Execute a query that returns no results
*
@@ -79,7 +137,7 @@ object Custom {
* @param parameters Parameters to use for the query
*/
fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection) =
JvmCustom.nonQuery(query, parameters, conn)
CoreCustom.nonQuery(query, parameters, conn)
/**
* Execute a query that returns no results

View File

@@ -3,6 +3,7 @@ package solutions.bitbadger.documents.kotlinx
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.Dialect
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.java.Results as CoreResults
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException
@@ -73,4 +74,34 @@ object Results {
Dialect.POSTGRESQL -> rs.getBoolean("it")
Dialect.SQLITE -> toCount(rs) > 0L
}
/**
* Retrieve the JSON text of a document, specifying the field in which the document is found
*
* @param field The field name containing the JSON document
* @param rs A `ResultSet` set to the row with the document to be constructed
* @return The JSON text of the document
*/
fun jsonFromDocument(field: String, rs: ResultSet) =
CoreResults.jsonFromDocument(field, rs)
/**
* Retrieve the JSON text of a document, specifying the field in which the document is found
*
* @param rs A `ResultSet` set to the row with the document to be constructed
* @return The JSON text of the document
*/
fun jsonFromData(rs: ResultSet) =
CoreResults.jsonFromData(rs)
/**
* Create a JSON array of items for the results of the given command, using the specified mapping function
*
* @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)
*/
fun toJsonArray(stmt: PreparedStatement, mapFunc: (ResultSet) -> String) =
CoreResults.toJsonArray(stmt, mapFunc)
}

View File

@@ -19,6 +19,21 @@ inline fun <reified TDoc : Any> Connection.customList(
query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
) = Custom.list(query, parameters, this, mapFunc)
/**
* Execute a query that returns a JSON array of results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @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
*/
fun Connection.customJsonArray(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
mapFunc: (ResultSet) -> String
) = Custom.jsonArray(query, parameters, mapFunc)
/**
* Execute a query that returns one or no results
*
@@ -31,6 +46,21 @@ inline fun <reified TDoc : Any> Connection.customSingle(
query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
) = Custom.single(query, parameters, this, mapFunc)
/**
* Execute a query that returns JSON for one or no documents (creates connection)
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param mapFunc The mapping function between the document and the domain item
* @return The JSON for the document if found, an empty object (`{}`) if not
* @throws DocumentException If parameters are invalid
*/
fun Connection.customJsonSingle(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
mapFunc: (ResultSet) -> String
) = Custom.jsonSingle(query, parameters, mapFunc)
/**
* Execute a query that returns no results
*
@@ -239,8 +269,7 @@ inline fun <reified TDoc : Any> Connection.findByFields(
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) =
Find.byFields<TDoc>(tableName, fields, howMatched, orderBy, this)
) = Find.byFields<TDoc>(tableName, fields, howMatched, orderBy, this)
/**
* Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL only)
@@ -255,8 +284,7 @@ inline fun <reified TDoc : Any, reified TContains> Connection.findByContains(
tableName: String,
criteria: TContains,
orderBy: Collection<Field<*>>? = null
) =
Find.byContains<TDoc, TContains>(tableName, criteria, orderBy, this)
) = Find.byContains<TDoc, TContains>(tableName, criteria, orderBy, this)
/**
* Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only)
@@ -271,8 +299,7 @@ inline fun <reified TDoc : Any> Connection.findByJsonPath(
tableName: String,
path: String,
orderBy: Collection<Field<*>>? = null
) =
Find.byJsonPath<TDoc>(tableName, path, orderBy, this)
) = Find.byJsonPath<TDoc>(tableName, path, orderBy, this)
/**
* Retrieve the first document using a field comparison and optional ordering fields
@@ -288,8 +315,7 @@ inline fun <reified TDoc : Any> Connection.findFirstByFields(
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) =
Find.firstByFields<TDoc>(tableName, fields, howMatched, orderBy, this)
) = Find.firstByFields<TDoc>(tableName, fields, howMatched, orderBy, this)
/**
* Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only)
@@ -304,8 +330,7 @@ inline fun <reified TDoc : Any, reified TContains> Connection.findFirstByContain
tableName: String,
criteria: TContains,
orderBy: Collection<Field<*>>? = null
) =
Find.firstByContains<TDoc, TContains>(tableName, criteria, orderBy, this)
) = Find.firstByContains<TDoc, TContains>(tableName, criteria, orderBy, this)
/**
* Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only)
@@ -320,8 +345,7 @@ inline fun <reified TDoc : Any> Connection.findFirstByJsonPath(
tableName: String,
path: String,
orderBy: Collection<Field<*>>? = null
) =
Find.firstByJsonPath<TDoc>(tableName, path, orderBy, this)
) = Find.firstByJsonPath<TDoc>(tableName, path, orderBy, this)
// ~~~ DOCUMENT PATCH (PARTIAL UPDATE) QUERIES ~~~

View File

@@ -3,6 +3,7 @@ package solutions.bitbadger.documents.kotlinx.tests.integration
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.kotlinx.Results
import solutions.bitbadger.documents.kotlinx.extensions.*
import solutions.bitbadger.documents.kotlinx.tests.ArrayDocument
import solutions.bitbadger.documents.kotlinx.tests.JsonDocument
import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE
import solutions.bitbadger.documents.query.*
@@ -28,6 +29,36 @@ object CustomFunctions {
assertEquals(5, result.size, "There should have been 5 results")
}
fun jsonArrayEmpty(db: ThrowawayDatabase) {
assertEquals(0L, db.conn.countAll(TEST_TABLE), "The test table should be empty")
assertEquals(
"[]",
db.conn.customJsonArray(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData),
"An empty list was not represented correctly"
)
}
fun jsonArraySingle(db: ThrowawayDatabase) {
db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3")))
assertEquals(
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"
)
}
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\"]}]"),
db.conn.customJsonArray(FindQuery.all(TEST_TABLE) + orderBy(listOf(Field.named("id"))), listOf(),
Results::jsonFromData),
"A multiple document list was not represented correctly"
)
}
fun singleNone(db: ThrowawayDatabase) =
assertNull(
db.conn.customSingle(FindQuery.all(TEST_TABLE), mapFunc = Results::fromData),
@@ -42,6 +73,19 @@ object CustomFunctions {
)
}
fun jsonSingleNone(db: ThrowawayDatabase) =
assertEquals("{}", db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData),
"An empty document was not represented correctly")
fun jsonSingleOne(db: ThrowawayDatabase) {
db.conn.insert(TEST_TABLE, ArrayDocument("me", listOf("myself", "i")))
assertEquals(
JsonFunctions.maybeJsonB("{\"id\":\"me\",\"values\":[\"myself\",\"i\"]}"),
db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData),
"A single document was not represented correctly"
)
}
fun nonQueryChanges(db: ThrowawayDatabase) {
JsonDocument.load(db)
assertEquals(

View File

@@ -0,0 +1,20 @@
package solutions.bitbadger.documents.kotlinx.tests.integration
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.Dialect
object JsonFunctions {
/**
* PostgreSQL, when returning JSONB as a string, has spaces after commas and colons delineating fields and values.
* This function will do a crude string replacement to match the target string based on the dialect being tested.
*
* @param json The JSON which should be returned
* @return The actual expected JSON based on the database being tested
*/
fun maybeJsonB(json: String) =
when (Configuration.dialect()) {
Dialect.SQLITE -> json
Dialect.POSTGRESQL -> json.replace("\":\"", "\": \"").replace("\",\"", "\", \"").replace("\":[", "\": [")
}
}

View File

@@ -20,6 +20,21 @@ class PostgreSQLCustomIT {
fun listAll() =
PgDB().use(CustomFunctions::listAll)
@Test
@DisplayName("jsonArray succeeds with empty array")
fun jsonArrayEmpty() =
PgDB().use(CustomFunctions::jsonArrayEmpty)
@Test
@DisplayName("jsonArray succeeds with a single-item array")
fun jsonArraySingle() =
PgDB().use(CustomFunctions::jsonArraySingle)
@Test
@DisplayName("jsonArray succeeds with a multi-item array")
fun jsonArrayMany() =
PgDB().use(CustomFunctions::jsonArrayMany)
@Test
@DisplayName("single succeeds when document not found")
fun singleNone() =
@@ -30,6 +45,16 @@ class PostgreSQLCustomIT {
fun singleOne() =
PgDB().use(CustomFunctions::singleOne)
@Test
@DisplayName("jsonSingle succeeds when document not found")
fun jsonSingleNone() =
PgDB().use(CustomFunctions::jsonSingleNone)
@Test
@DisplayName("jsonSingle succeeds when a document is found")
fun jsonSingleOne() =
PgDB().use(CustomFunctions::jsonSingleOne)
@Test
@DisplayName("nonQuery makes changes")
fun nonQueryChanges() =

View File

@@ -19,6 +19,21 @@ class SQLiteCustomIT {
fun listAll() =
SQLiteDB().use(CustomFunctions::listAll)
@Test
@DisplayName("jsonArray succeeds with empty array")
fun jsonArrayEmpty() =
SQLiteDB().use(CustomFunctions::jsonArrayEmpty)
@Test
@DisplayName("jsonArray succeeds with a single-item array")
fun jsonArraySingle() =
SQLiteDB().use(CustomFunctions::jsonArraySingle)
@Test
@DisplayName("jsonArray succeeds with a multi-item array")
fun jsonArrayMany() =
SQLiteDB().use(CustomFunctions::jsonArrayMany)
@Test
@DisplayName("single succeeds when document not found")
fun singleNone() =
@@ -29,6 +44,16 @@ class SQLiteCustomIT {
fun singleOne() =
SQLiteDB().use(CustomFunctions::singleOne)
@Test
@DisplayName("jsonSingle succeeds when document not found")
fun jsonSingleNone() =
SQLiteDB().use(CustomFunctions::jsonSingleNone)
@Test
@DisplayName("jsonSingle succeeds when a document is found")
fun jsonSingleOne() =
SQLiteDB().use(CustomFunctions::jsonSingleOne)
@Test
@DisplayName("nonQuery makes changes")
fun nonQueryChanges() =