Initial Development #1

Merged
danieljsummers merged 88 commits from v1-rc into main 2025-04-16 01:29:20 +00:00
2 changed files with 218 additions and 13 deletions
Showing only changes of commit cefd2daa52 - Show all commits

View File

@ -77,12 +77,12 @@ object Query {
* Create a query on JSON fields
*
* @param statement The SQL statement to be run against matching fields
* @param howMatched Whether to match any or all of the field conditions
* @param fields The field conditions to be matched
* @param howMatched Whether to match any or all of the field conditions (optional; default ALL)
* @return A query addressing documents by field matching conditions
*/
fun byFields(statement: String, howMatched: FieldMatch, fields: Collection<Field<*>>) =
Query.statementWhere(statement, Where.byFields(fields, howMatched))
fun byFields(statement: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
statementWhere(statement, Where.byFields(fields, howMatched))
/**
* Functions to create queries to define tables and indexes
@ -117,10 +117,8 @@ object Query {
* @param tableName The name of the table, possibly with a schema
* @return A pair with the first item as the schema and the second as the table name
*/
private fun splitSchemaAndTable(tableName: String): Pair<String, String> {
val parts = tableName.split('.')
return if (parts.size == 1) Pair("", tableName) else Pair(parts[0], parts[1])
}
private fun splitSchemaAndTable(tableName: String) =
tableName.split('.').let { if (it.size == 1) Pair("", tableName) else Pair(it[0], it[1]) }
/**
* SQL statement to create an index on one or more fields in a JSON document

View File

@ -3,7 +3,9 @@ package solutions.bitbadger.documents
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class QueryTest {
@ -130,6 +132,105 @@ class QueryTest {
assertEquals("data->>'id' = :key", Query.Where.byId<String>(":key"))
}
@Test
@DisplayName("Where.jsonContains generates defaults (PostgreSQL)")
fun whereJsonContainsDefaultPostgres() {
Configuration.connectionString = pg
assertEquals("data @> :criteria", Query.Where.jsonContains())
}
@Test
@DisplayName("Where.jsonContains generates named parameter (PostgreSQL)")
fun whereJsonContainsNamedPostgres() {
Configuration.connectionString = pg
assertEquals("data @> :it", Query.Where.jsonContains(":it"))
}
@Test
@DisplayName("Where.jsonContains fails (SQLite)")
fun whereJsonContainsFailsSQLite() {
Configuration.connectionString = lite
assertThrows<DocumentException> { Query.Where.jsonContains() }
}
@Test
@DisplayName("Where.jsonPathMatch generates defaults (PostgreSQL)")
fun whereJsonPathMatchDefaultPostgres() {
Configuration.connectionString = pg
assertEquals("jsonb_path_exists(data, :path::jsonpath)", Query.Where.jsonPathMatches())
}
@Test
@DisplayName("Where.jsonPathMatch generates named parameter (PostgreSQL)")
fun whereJsonPathMatchNamedPostgres() {
Configuration.connectionString = pg
assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Query.Where.jsonPathMatches(":jp"))
}
@Test
@DisplayName("Where.jsonPathMatch fails (SQLite)")
fun whereJsonPathFailsSQLite() {
Configuration.connectionString = lite
assertThrows<DocumentException> { Query.Where.jsonPathMatches() }
}
// ~~~ root functions ~~~
@Test
@DisplayName("byId generates a numeric ID query (PostgreSQL)")
fun byIdNumericPostgres() {
Configuration.connectionString = pg
assertEquals("test WHERE (data->>'id')::numeric = :id", Query.byId("test", 9))
}
@Test
@DisplayName("byId generates an alphanumeric ID query (PostgreSQL)")
fun byIdAlphaPostgres() {
Configuration.connectionString = pg
assertEquals("unit WHERE data->>'id' = :id", Query.byId("unit", "18"))
}
@Test
@DisplayName("byId generates ID query (SQLite)")
fun byIdSQLite() {
Configuration.connectionString = lite
assertEquals("yo WHERE data->>'id' = :id", Query.byId("yo", 27))
}
@Test
@DisplayName("byFields generates default field query (PostgreSQL)")
fun byFieldsMultipleDefaultPostgres() {
Configuration.connectionString = pg
assertEquals("this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value",
Query.byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value"))))
}
@Test
@DisplayName("byFields generates default field query (SQLite)")
fun byFieldsMultipleDefaultSQLite() {
Configuration.connectionString = lite
assertEquals("this WHERE data->>'a' = :the_a AND data->>'b' = :b_value",
Query.byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value"))))
}
@Test
@DisplayName("byFields generates ANY field query (PostgreSQL)")
fun byFieldsMultipleAnyPostgres() {
Configuration.connectionString = pg
assertEquals("that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value",
Query.byFields("that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")),
FieldMatch.ANY))
}
@Test
@DisplayName("byFields generates ANY field query (SQLite)")
fun byFieldsMultipleAnySQLite() {
Configuration.connectionString = lite
assertEquals("that WHERE data->>'a' = :the_a OR data->>'b' = :b_value",
Query.byFields("that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")),
FieldMatch.ANY))
}
// ~~~ Definition ~~~
@Test
@ -138,6 +239,26 @@ class QueryTest {
assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
Query.Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly")
@Test
@DisplayName("Definition.ensureTable generates correctly (PostgreSQL)")
fun ensureTablePostgres() {
Configuration.connectionString = pg
assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data JSONB NOT NULL)", Query.Definition.ensureTable(tbl))
}
@Test
@DisplayName("Definition.ensureTable generates correctly (SQLite)")
fun ensureTableSQLite() {
Configuration.connectionString = lite
assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data TEXT NOT NULL)", Query.Definition.ensureTable(tbl))
}
@Test
@DisplayName("Definition.ensureTable fails when no dialect is set")
fun ensureTableFailsUnknown() {
assertThrows<DocumentException> { Query.Definition.ensureTable(tbl) }
}
@Test
@DisplayName("Definition.ensureKey generates correctly with schema")
fun ensureKeyWithSchema() =
@ -175,11 +296,97 @@ class QueryTest {
Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
"CREATE INDEX for nested SQLite field incorrect")
// ~~~ root functions ~~~
@Test
@DisplayName("insert generates correctly")
fun insert() {
@DisplayName("insert generates no auto ID (PostgreSQL)")
fun insertNoAutoPostgres() {
Configuration.connectionString = pg
assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl), "INSERT statement not constructed correctly")
assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl))
}
@Test
@DisplayName("insert generates no auto ID (SQLite)")
fun insertNoAutoSQLite() {
Configuration.connectionString = lite
assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl))
}
@Test
@DisplayName("insert generates auto number (PostgreSQL)")
fun insertAutoNumberPostgres() {
Configuration.connectionString = pg
assertEquals(
"INSERT INTO $tbl VALUES (:data::jsonb || ('{\"id\":' " +
"|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $tbl) || '}')::jsonb)",
Query.insert(tbl, AutoId.NUMBER))
}
@Test
@DisplayName("insert generates auto number (SQLite)")
fun insertAutoNumberSQLite() {
Configuration.connectionString = lite
assertEquals(
"INSERT INTO $tbl VALUES (json_set(:data, '$.id', " +
"(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $tbl)))",
Query.insert(tbl, AutoId.NUMBER))
}
@Test
@DisplayName("insert generates auto UUID (PostgreSQL)")
fun insertAutoUUIDPostgres() {
Configuration.connectionString = pg
val query = Query.insert(tbl, AutoId.UUID)
assertTrue(query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""),
"Query start not correct (actual: $query)")
assertTrue(query.endsWith("\"}')"), "Query end not correct")
}
@Test
@DisplayName("insert generates auto UUID (SQLite)")
fun insertAutoUUIDSQLite() {
Configuration.connectionString = lite
val query = Query.insert(tbl, AutoId.UUID)
assertTrue(query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"),
"Query start not correct (actual: $query)")
assertTrue(query.endsWith("'))"), "Query end not correct")
}
@Test
@DisplayName("insert generates auto random string (PostgreSQL)")
fun insertAutoRandomPostgres() {
try {
Configuration.connectionString = pg
Configuration.idStringLength = 8
val query = Query.insert(tbl, AutoId.RANDOM_STRING)
assertTrue(query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""),
"Query start not correct (actual: $query)")
assertTrue(query.endsWith("\"}')"), "Query end not correct")
assertEquals(8,
query.replace("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "").length,
"Random string length incorrect")
} finally {
Configuration.idStringLength = 16
}
}
@Test
@DisplayName("insert generates auto random string (SQLite)")
fun insertAutoRandomSQLite() {
Configuration.connectionString = lite
val query = Query.insert(tbl, AutoId.RANDOM_STRING)
assertTrue(query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"),
"Query start not correct (actual: $query)")
assertTrue(query.endsWith("'))"), "Query end not correct")
assertEquals(Configuration.idStringLength,
query.replace("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length,
"Random string length incorrect")
}
@Test
@DisplayName("insert fails when no dialect is set")
fun insertFailsUnknown() {
assertThrows<DocumentException> { Query.insert(tbl) }
}
@Test