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 228 additions and 80 deletions
Showing only changes of commit 31817d5cec - Show all commits

View File

@ -254,7 +254,7 @@ object Query {
* @param docId The ID of the document (optional, used for type checking)
* @return A query to determine document existence by ID
*/
fun <TKey> byId(tableName: String, docId: TKey?) =
fun <TKey> byId(tableName: String, docId: TKey? = null) =
exists(tableName, Where.byId(docId = docId))
/**
@ -265,7 +265,7 @@ object Query {
* @param howMatched How fields should be compared (optional, defaults to ALL)
* @return A query to determine document existence for the given fields
*/
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch?) =
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
exists(tableName, Where.byFields(fields, howMatched))
/**

View File

@ -63,38 +63,50 @@ class QueryTest {
@DisplayName("Where.byFields generates multiple fields w/ default match (PostgreSQL)")
fun whereByFieldsMultipleDefaultPostgres() {
Configuration.connectionString = pg
assertEquals("data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three",
assertEquals(
"data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three",
Query.Where.byFields(
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))))
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))
)
)
}
@Test
@DisplayName("Where.byFields generates multiple fields w/ default match (SQLite)")
fun whereByFieldsMultipleDefaultSQLite() {
Configuration.connectionString = lite
assertEquals("data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three",
assertEquals(
"data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three",
Query.Where.byFields(
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))))
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))
)
)
}
@Test
@DisplayName("Where.byFields generates multiple fields w/ ANY match (PostgreSQL)")
fun whereByFieldsMultipleAnyPostgres() {
Configuration.connectionString = pg
assertEquals("data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three",
assertEquals(
"data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three",
Query.Where.byFields(
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
FieldMatch.ANY))
FieldMatch.ANY
)
)
}
@Test
@DisplayName("Where.byFields generates multiple fields w/ ANY match (SQLite)")
fun whereByFieldsMultipleAnySQLite() {
Configuration.connectionString = lite
assertEquals("data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three",
assertEquals(
"data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three",
Query.Where.byFields(
listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
FieldMatch.ANY))
FieldMatch.ANY
)
)
}
@Test
@ -201,34 +213,46 @@ class QueryTest {
@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"))))
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"))))
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))
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))
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 ~~~
@ -236,8 +260,10 @@ class QueryTest {
@Test
@DisplayName("Definition.ensureTableFor generates correctly")
fun ensureTableFor() =
assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
Query.Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly")
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)")
@ -262,39 +288,50 @@ class QueryTest {
@Test
@DisplayName("Definition.ensureKey generates correctly with schema")
fun ensureKeyWithSchema() =
assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))",
assertEquals(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))",
Query.Definition.ensureKey("test.table", Dialect.POSTGRESQL),
"CREATE INDEX for key statement with schema not constructed correctly")
"CREATE INDEX for key statement with schema not constructed correctly"
)
@Test
@DisplayName("Definition.ensureKey generates correctly without schema")
fun ensureKeyWithoutSchema() =
assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))",
assertEquals(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))",
Query.Definition.ensureKey(tbl, Dialect.SQLITE),
"CREATE INDEX for key statement without schema not constructed correctly")
"CREATE INDEX for key statement without schema not constructed correctly"
)
@Test
@DisplayName("Definition.ensureIndexOn generates multiple fields and directions")
fun ensureIndexOnMultipleFields() =
assertEquals(
"CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)",
Query.Definition.ensureIndexOn("test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"),
Dialect.POSTGRESQL),
"CREATE INDEX for multiple field statement not constructed correctly")
Query.Definition.ensureIndexOn(
"test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"),
Dialect.POSTGRESQL
),
"CREATE INDEX for multiple field statement not constructed correctly"
)
@Test
@DisplayName("Definition.ensureIndexOn generates nested PostgreSQL field")
fun ensureIndexOnNestedPostgres() =
assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))",
assertEquals(
"CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))",
Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL),
"CREATE INDEX for nested PostgreSQL field incorrect")
"CREATE INDEX for nested PostgreSQL field incorrect"
)
@Test
@DisplayName("Definition.ensureIndexOn generates nested SQLite field")
fun ensureIndexOnNestedSQLite() =
assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))",
assertEquals(
"CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))",
Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
"CREATE INDEX for nested SQLite field incorrect")
"CREATE INDEX for nested SQLite field incorrect"
)
// ~~~ root functions ~~~
@ -319,7 +356,8 @@ class QueryTest {
assertEquals(
"INSERT INTO $tbl VALUES (:data::jsonb || ('{\"id\":' " +
"|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $tbl) || '}')::jsonb)",
Query.insert(tbl, AutoId.NUMBER))
Query.insert(tbl, AutoId.NUMBER)
)
}
@Test
@ -329,7 +367,8 @@ class QueryTest {
assertEquals(
"INSERT INTO $tbl VALUES (json_set(:data, '$.id', " +
"(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $tbl)))",
Query.insert(tbl, AutoId.NUMBER))
Query.insert(tbl, AutoId.NUMBER)
)
}
@Test
@ -337,8 +376,10 @@ class QueryTest {
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.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""),
"Query start not correct (actual: $query)"
)
assertTrue(query.endsWith("\"}')"), "Query end not correct")
}
@ -347,8 +388,10 @@ class QueryTest {
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.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"),
"Query start not correct (actual: $query)"
)
assertTrue(query.endsWith("'))"), "Query end not correct")
}
@ -359,12 +402,16 @@ class QueryTest {
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.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""),
"Query start not correct (actual: $query)"
)
assertTrue(query.endsWith("\"}')"), "Query end not correct")
assertEquals(8,
assertEquals(
8,
query.replace("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "").length,
"Random string length incorrect")
"Random string length incorrect"
)
} finally {
Configuration.idStringLength = 16
}
@ -375,12 +422,16 @@ class QueryTest {
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.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,
assertEquals(
Configuration.idStringLength,
query.replace("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length,
"Random string length incorrect")
"Random string length incorrect"
)
}
@Test
@ -395,7 +446,8 @@ class QueryTest {
Configuration.connectionString = pg
assertEquals(
"INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data",
Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly")
Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly"
)
}
@Test
@ -407,26 +459,32 @@ class QueryTest {
@DisplayName("Count.byFields generates correctly (PostgreSQL)")
fun countByFieldsPostgres() {
Configuration.connectionString = pg
assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
assertEquals(
"SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
Query.Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))),
"Count query not constructed correctly")
"Count query not constructed correctly"
)
}
@Test
@DisplayName("Count.byFields generates correctly (PostgreSQL)")
fun countByFieldsSQLite() {
Configuration.connectionString = lite
assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
assertEquals(
"SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
Query.Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))),
"Count query not constructed correctly")
"Count query not constructed correctly"
)
}
@Test
@DisplayName("Count.byContains generates correctly (PostgreSQL)")
fun countByContainsPostgres() {
Configuration.connectionString = pg
assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", Query.Count.byContains(tbl),
"Count query not constructed correctly")
assertEquals(
"SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", Query.Count.byContains(tbl),
"Count query not constructed correctly"
)
}
@Test
@ -440,8 +498,10 @@ class QueryTest {
@DisplayName("Count.byJsonPath generates correctly (PostgreSQL)")
fun countByJsonPathPostgres() {
Configuration.connectionString = pg
assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)",
Query.Count.byJsonPath(tbl), "Count query not constructed correctly")
assertEquals(
"SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)",
Query.Count.byJsonPath(tbl), "Count query not constructed correctly"
)
}
@Test
@ -451,11 +511,81 @@ class QueryTest {
assertThrows<DocumentException> { Query.Count.byJsonPath(tbl) }
}
// @Test
// @DisplayName("exists generates correctly")
// fun exists() =
// assertEquals("SELECT EXISTS (SELECT 1 FROM $tbl WHERE turkey) AS it", Query.exists(tbl, "turkey"),
// "Exists query not constructed correctly")
@Test
@DisplayName("Exists.byId generates correctly (PostgreSQL)")
fun existsByIdPostgres() {
Configuration.connectionString = pg
assertEquals(
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it",
Query.Exists.byId<String>(tbl), "Exists query not constructed correctly"
)
}
@Test
@DisplayName("Exists.byId generates correctly (SQLite)")
fun existsByIdSQLite() {
Configuration.connectionString = lite
assertEquals(
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it",
Query.Exists.byId<String>(tbl), "Exists query not constructed correctly"
)
}
@Test
@DisplayName("Exists.byFields generates correctly (PostgreSQL)")
fun existsByFieldsPostgres() {
Configuration.connectionString = pg
assertEquals(
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE (data->>'it')::numeric = :test) AS it",
Query.Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))),
"Exists query not constructed correctly"
)
}
@Test
@DisplayName("Exists.byFields generates correctly (SQLite)")
fun existsByFieldsSQLite() {
Configuration.connectionString = lite
assertEquals(
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'it' = :test) AS it",
Query.Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))),
"Exists query not constructed correctly"
)
}
@Test
@DisplayName("Exists.byContains generates correctly (PostgreSQL)")
fun existsByContainsPostgres() {
Configuration.connectionString = pg
assertEquals(
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE data @> :criteria) AS it", Query.Exists.byContains(tbl),
"Exists query not constructed correctly"
)
}
@Test
@DisplayName("Exists.byContains fails (SQLite)")
fun existsByContainsSQLite() {
Configuration.connectionString = lite
assertThrows<DocumentException> { Query.Exists.byContains(tbl) }
}
@Test
@DisplayName("Exists.byJsonPath generates correctly (PostgreSQL)")
fun existsByJsonPathPostgres() {
Configuration.connectionString = pg
assertEquals(
"SELECT EXISTS (SELECT 1 FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)) AS it",
Query.Exists.byJsonPath(tbl), "Exists query not constructed correctly"
)
}
@Test
@DisplayName("Exists.byJsonPath fails (SQLite)")
fun existsByJsonPathSQLite() {
Configuration.connectionString = lite
assertThrows<DocumentException> { Query.Exists.byJsonPath(tbl) }
}
@Test
@DisplayName("Find.all generates correctly")
@ -482,56 +612,74 @@ class QueryTest {
@Test
@DisplayName("orderBy generates single, no direction for PostgreSQL")
fun orderBySinglePostgres() =
assertEquals(" ORDER BY data->>'TestField'",
Query.orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly")
assertEquals(
" ORDER BY data->>'TestField'",
Query.orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly"
)
@Test
@DisplayName("orderBy generates single, no direction for SQLite")
fun orderBySingleSQLite() =
assertEquals(" ORDER BY data->>'TestField'", Query.orderBy(listOf(Field.named("TestField")), Dialect.SQLITE),
"ORDER BY not constructed correctly")
assertEquals(
" ORDER BY data->>'TestField'", Query.orderBy(listOf(Field.named("TestField")), Dialect.SQLITE),
"ORDER BY not constructed correctly"
)
@Test
@DisplayName("orderBy generates multiple with direction for PostgreSQL")
fun orderByMultiplePostgres() =
assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
assertEquals(
" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
Query.orderBy(
listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
Dialect.POSTGRESQL),
"ORDER BY not constructed correctly")
Dialect.POSTGRESQL
),
"ORDER BY not constructed correctly"
)
@Test
@DisplayName("orderBy generates multiple with direction for SQLite")
fun orderByMultipleSQLite() =
assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
assertEquals(
" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
Query.orderBy(
listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
Dialect.SQLITE),
"ORDER BY not constructed correctly")
Dialect.SQLITE
),
"ORDER BY not constructed correctly"
)
@Test
@DisplayName("orderBy generates numeric ordering PostgreSQL")
fun orderByNumericPostgres() =
assertEquals(" ORDER BY (data->>'Test')::numeric",
Query.orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly")
assertEquals(
" ORDER BY (data->>'Test')::numeric",
Query.orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly"
)
@Test
@DisplayName("orderBy generates numeric ordering for SQLite")
fun orderByNumericSQLite() =
assertEquals(" ORDER BY data->>'Test'", Query.orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE),
"ORDER BY not constructed correctly")
assertEquals(
" ORDER BY data->>'Test'", Query.orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE),
"ORDER BY not constructed correctly"
)
@Test
@DisplayName("orderBy generates case-insensitive ordering for PostgreSQL")
fun orderByCIPostgres() =
assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
assertEquals(
" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
Query.orderBy(listOf(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL),
"ORDER BY not constructed correctly")
"ORDER BY not constructed correctly"
)
@Test
@DisplayName("orderBy generates case-insensitive ordering for SQLite")
fun orderByCISQLite() =
assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
assertEquals(
" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
Query.orderBy(listOf(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE),
"ORDER BY not constructed correctly")
"ORDER BY not constructed correctly"
)
}