Initial Development #1
@ -46,7 +46,7 @@ object Query {
|
|||||||
fun jsonContains(parameterName: String = ":criteria") =
|
fun jsonContains(parameterName: String = ":criteria") =
|
||||||
when (Configuration.dialect("create containment WHERE clause")) {
|
when (Configuration.dialect("create containment WHERE clause")) {
|
||||||
Dialect.POSTGRESQL -> "data @> $parameterName"
|
Dialect.POSTGRESQL -> "data @> $parameterName"
|
||||||
Dialect.SQLITE -> throw DocumentException("JSON containment is not supported")
|
Dialect.SQLITE -> throw DocumentException("JSON containment is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,7 +59,7 @@ object Query {
|
|||||||
fun jsonPathMatches(parameterName: String = ":path") =
|
fun jsonPathMatches(parameterName: String = ":path") =
|
||||||
when (Configuration.dialect("create JSON path match WHERE clause")) {
|
when (Configuration.dialect("create JSON path match WHERE clause")) {
|
||||||
Dialect.POSTGRESQL -> "jsonb_path_exists(data, $parameterName::jsonpath)"
|
Dialect.POSTGRESQL -> "jsonb_path_exists(data, $parameterName::jsonpath)"
|
||||||
Dialect.SQLITE -> throw DocumentException("JSON path match is not supported")
|
Dialect.SQLITE -> throw DocumentException("JSON path match is not supported")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,12 +77,12 @@ object Query {
|
|||||||
* Create a query on JSON fields
|
* Create a query on JSON fields
|
||||||
*
|
*
|
||||||
* @param statement The SQL statement to be run against matching 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 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
|
* @return A query addressing documents by field matching conditions
|
||||||
*/
|
*/
|
||||||
fun byFields(statement: String, howMatched: FieldMatch, fields: Collection<Field<*>>) =
|
fun byFields(statement: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||||
Query.statementWhere(statement, Where.byFields(fields, howMatched))
|
statementWhere(statement, Where.byFields(fields, howMatched))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functions to create queries to define tables and indexes
|
* Functions to create queries to define tables and indexes
|
||||||
@ -108,7 +108,7 @@ object Query {
|
|||||||
fun ensureTable(tableName: String) =
|
fun ensureTable(tableName: String) =
|
||||||
when (Configuration.dialect("create table creation query")) {
|
when (Configuration.dialect("create table creation query")) {
|
||||||
Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB")
|
Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB")
|
||||||
Dialect.SQLITE -> ensureTableFor(tableName, "TEXT")
|
Dialect.SQLITE -> ensureTableFor(tableName, "TEXT")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,10 +117,8 @@ object Query {
|
|||||||
* @param tableName The name of the table, possibly with a schema
|
* @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
|
* @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> {
|
private fun splitSchemaAndTable(tableName: String) =
|
||||||
val parts = tableName.split('.')
|
tableName.split('.').let { if (it.size == 1) Pair("", tableName) else Pair(it[0], it[1]) }
|
||||||
return if (parts.size == 1) Pair("", tableName) else Pair(parts[0], parts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQL statement to create an index on one or more fields in a JSON document
|
* SQL statement to create an index on one or more fields in a JSON document
|
||||||
|
@ -3,7 +3,9 @@ package solutions.bitbadger.documents
|
|||||||
import org.junit.jupiter.api.AfterEach
|
import org.junit.jupiter.api.AfterEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class QueryTest {
|
class QueryTest {
|
||||||
|
|
||||||
@ -130,6 +132,105 @@ class QueryTest {
|
|||||||
assertEquals("data->>'id' = :key", Query.Where.byId<String>(":key"))
|
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 ~~~
|
// ~~~ Definition ~~~
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -138,6 +239,26 @@ class QueryTest {
|
|||||||
assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
|
assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
|
||||||
Query.Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly")
|
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
|
@Test
|
||||||
@DisplayName("Definition.ensureKey generates correctly with schema")
|
@DisplayName("Definition.ensureKey generates correctly with schema")
|
||||||
fun ensureKeyWithSchema() =
|
fun ensureKeyWithSchema() =
|
||||||
@ -175,11 +296,97 @@ class QueryTest {
|
|||||||
Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
|
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 ~~~
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("insert generates correctly")
|
@DisplayName("insert generates no auto ID (PostgreSQL)")
|
||||||
fun insert() {
|
fun insertNoAutoPostgres() {
|
||||||
Configuration.connectionString = pg
|
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
|
@Test
|
||||||
|
Loading…
x
Reference in New Issue
Block a user