From 781df81ce2d17d6f4018f17dbabf7ed5bd1c7a29 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Mar 2025 22:47:15 -0400 Subject: [PATCH] WIP on Scala/Groovy query tests --- .../documents/groovy/AutoIdTest.groovy | 2 +- .../groovy/query/CountQueryTest.groovy | 86 ++++++++++ .../groovy/query/DefinitionQueryTest.groovy | 128 ++++++++++++++ .../groovy/query/DeleteQueryTest.groovy | 94 +++++++++++ .../groovy/query/DocumentQueryTest.groovy | 135 +++++++++++++++ .../groovy/query/FindQueryTest.groovy | 101 +++++++++++ .../groovy/query/PatchQueryTest.groovy | 97 +++++++++++ .../groovy/query/QueryUtilsTest.groovy | 159 ++++++++++++++++++ .../documents/scala/AutoIdSpec.scala | 10 +- .../documents/scala/ConfigurationSpec.scala | 2 +- .../bitbadger/documents/scala/FieldSpec.scala | 92 +++++----- .../documents/scala/ParameterSpec.scala | 2 +- .../scala/query/CountQuerySpec.scala | 56 ++++++ .../scala/query/DefinitionQuerySpec.scala | 81 +++++++++ .../scala/query/DeleteQuerySpec.scala | 61 +++++++ .../scala/query/DocumentQuerySpec.scala | 85 ++++++++++ .../scala/query/ExistsQuerySpec.scala | 64 +++++++ .../documents/scala/query/FindQuerySpec.scala | 67 ++++++++ .../scala/query/PatchQuerySpec.scala | 63 +++++++ .../scala/query/QueryUtilsSpec.scala | 103 ++++++++++++ 20 files changed, 1434 insertions(+), 54 deletions(-) create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy index 52c79e2..6d7e0d9 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.groovy import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import solutions.bitbadger.documents.AutoId -import solutions.bitbadger.documents.DocumentException +//import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.groovy.support.* import static groovy.test.GroovyAssert.* diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy new file mode 100644 index 0000000..54d3d96 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy @@ -0,0 +1,86 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Count` object + */ +@DisplayName('JVM | Groovy | Query | CountQuery') +class CountQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('all generates correctly') + void all() { + assertEquals('Count query not constructed correctly', "SELECT COUNT(*) AS it FROM $TEST_TABLE".toString(), + CountQuery.all(TEST_TABLE)) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Count query not constructed correctly', + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0".toString(), + CountQuery.byFields(TEST_TABLE, List.of(Field.equal('test', '', ':field0')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Count query not constructed correctly', + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0".toString(), + CountQuery.byFields(TEST_TABLE, List.of(Field.equal('test', '', ':field0')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Count query not constructed correctly', + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria".toString(), + CountQuery.byContains(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byContains fails | SQLite') +// void byContainsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { CountQuery.byContains(TEST_TABLE) } +// } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals('Count query not constructed correctly', + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + CountQuery.byJsonPath(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { CountQuery.byJsonPath(TEST_TABLE) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy new file mode 100644 index 0000000..bba2783 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy @@ -0,0 +1,128 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Dialect +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.query.DefinitionQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Definition` object + */ +@DisplayName('JVM | Groovy | Query | DefinitionQuery') +class DefinitionQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('ensureTableFor generates correctly') + void ensureTableFor() { + assertEquals('CREATE TABLE statement not constructed correctly', + 'CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)', + DefinitionQuery.ensureTableFor('my.table', 'JSONB')) + } + + @Test + @DisplayName('ensureTable generates correctly | PostgreSQL') + void ensureTablePostgres() { + ForceDialect.postgres() + assertEquals("CREATE TABLE IF NOT EXISTS $TEST_TABLE (data JSONB NOT NULL)".toString(), + DefinitionQuery.ensureTable(TEST_TABLE)) + } + + @Test + @DisplayName('ensureTable generates correctly | SQLite') + void ensureTableSQLite() { + ForceDialect.sqlite() + assertEquals("CREATE TABLE IF NOT EXISTS $TEST_TABLE (data TEXT NOT NULL)".toString(), + DefinitionQuery.ensureTable(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('ensureTable fails when no dialect is set') +// void ensureTableFailsUnknown() { +// assertThrows(DocumentException) { DefinitionQuery.ensureTable(TEST_TABLE) } +// } + + @Test + @DisplayName('ensureKey generates correctly with schema') + void ensureKeyWithSchema() { + assertEquals('CREATE INDEX for key statement with schema not constructed correctly', + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", + DefinitionQuery.ensureKey('test.table', Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('ensureKey generates correctly without schema') + void ensureKeyWithoutSchema() { + assertEquals('CREATE INDEX for key statement without schema not constructed correctly', + "CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))".toString(), + DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE)) + } + + @Test + @DisplayName('ensureIndexOn generates multiple fields and directions') + void ensureIndexOnMultipleFields() { + assertEquals('CREATE INDEX for multiple field statement not constructed correctly', + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" + .toString(), + DefinitionQuery.ensureIndexOn('test.table', 'gibberish', List.of('taco', 'guac DESC', 'salsa ASC'), + Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('ensureIndexOn generates nested field | PostgreSQL') + void ensureIndexOnNestedPostgres() { + assertEquals('CREATE INDEX for nested PostgreSQL field incorrect', + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data#>>'{a,b,c}'))".toString(), + DefinitionQuery.ensureIndexOn(TEST_TABLE, 'nest', List.of('a.b.c'), Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('ensureIndexOn generates nested field | SQLite') + void ensureIndexOnNestedSQLite() { + assertEquals('CREATE INDEX for nested SQLite field incorrect', + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data->'a'->'b'->>'c'))".toString(), + DefinitionQuery.ensureIndexOn(TEST_TABLE, 'nest', List.of('a.b.c'), Dialect.SQLITE)) + } + + @Test + @DisplayName('ensureDocumentIndexOn generates Full | PostgreSQL') + void ensureDocumentIndexOnFullPostgres() { + ForceDialect.postgres() + assertEquals('CREATE INDEX for full document index incorrect', + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)".toString(), + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL)) + } + + @Test + @DisplayName('ensureDocumentIndexOn generates Optimized | PostgreSQL') + void ensureDocumentIndexOnOptimizedPostgres() { + ForceDialect.postgres() + assertEquals('CREATE INDEX for optimized document index incorrect', + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data jsonb_path_ops)" + .toString(), + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('ensureDocumentIndexOn fails | SQLite') +// void ensureDocumentIndexOnFailsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy new file mode 100644 index 0000000..06fe0b7 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy @@ -0,0 +1,94 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Delete` object + */ +@DisplayName('JVM | Groovy | Query | DeleteQuery') +class DeleteQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), DeleteQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), DeleteQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data->>'a' = :b".toString(), + DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data->>'a' = :b".toString(), + DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data @> :criteria".toString(), DeleteQuery.byContains(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byContains fails | SQLite') +// void byContainsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { DeleteQuery.byContains(TEST_TABLE) } +// } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + DeleteQuery.byJsonPath(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { DeleteQuery.byJsonPath(TEST_TABLE) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy new file mode 100644 index 0000000..1d7cc15 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy @@ -0,0 +1,135 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.query.DocumentQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Document` object + */ +@DisplayName('JVM | Groovy | Query | DocumentQuery') +class DocumentQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('insert generates no auto ID | PostgreSQL') + void insertNoAutoPostgres() { + ForceDialect.postgres() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data)".toString(), DocumentQuery.insert(TEST_TABLE)) + } + + @Test + @DisplayName('insert generates no auto ID | SQLite') + void insertNoAutoSQLite() { + ForceDialect.sqlite() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data)".toString(), DocumentQuery.insert(TEST_TABLE)) + } + + @Test + @DisplayName('insert generates auto number | PostgreSQL') + void insertAutoNumberPostgres() { + ForceDialect.postgres() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || ('{\"id\":' || (SELECT ".toString() + + "COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $TEST_TABLE) || '}')::jsonb)".toString(), + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER)) + } + + @Test + @DisplayName('insert generates auto number | SQLite') + void insertAutoNumberSQLite() { + ForceDialect.sqlite() + assertEquals("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', ".toString() + + "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $TEST_TABLE)))".toString(), + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER)) + } + + @Test + @DisplayName('insert generates auto UUID | PostgreSQL') + void insertAutoUUIDPostgres() { + ForceDialect.postgres() + def query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + assertTrue("Query start not correct (actual: $query)", + query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"")) + assertTrue('Query end not correct', query.endsWith("\"}')")) + } + + @Test + @DisplayName('insert generates auto UUID | SQLite') + void insertAutoUUIDSQLite() { + ForceDialect.sqlite() + def query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + assertTrue("Query start not correct (actual: $query)", + query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '")) + assertTrue('Query end not correct', query.endsWith("'))")) + } + + @Test + @DisplayName('insert generates auto random string | PostgreSQL') + void insertAutoRandomPostgres() { + try { + ForceDialect.postgres() + Configuration.idStringLength = 8 + def query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + assertTrue("Query start not correct (actual: $query)", + query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"")) + assertTrue('Query end not correct', query.endsWith("\"}')")) + assertEquals('Random string length incorrect', 8, + query.replace("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", '') + .replace("\"}')", '').length()) + } finally { + Configuration.idStringLength = 16 + } + } + + @Test + @DisplayName('insert generates auto random string | SQLite') + void insertAutoRandomSQLite() { + ForceDialect.sqlite() + def query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + assertTrue("Query start not correct (actual: $query)", + query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '")) + assertTrue('Query end not correct', query.endsWith("'))")) + assertEquals('Random string length incorrect', Configuration.idStringLength, + query.replace("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '", '').replace("'))", '') + .length()) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('insert fails when no dialect is set') +// void insertFailsUnknown() { +// assertThrows(DocumentException) { DocumentQuery.insert(TEST_TABLE) } +// } + + @Test + @DisplayName('save generates correctly') + void save() { + ForceDialect.postgres() + assertEquals('INSERT ON CONFLICT UPDATE statement not constructed correctly', + "INSERT INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data" + .toString(), + DocumentQuery.save(TEST_TABLE)) + } + + @Test + @DisplayName('update generates successfully') + void update() { + assertEquals('Update query not constructed correctly', "UPDATE $TEST_TABLE SET data = :data".toString(), + DocumentQuery.update(TEST_TABLE)) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy new file mode 100644 index 0000000..4f3502f --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy @@ -0,0 +1,101 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Find` object + */ +@DisplayName('JVM | Groovy | Query | FindQuery') +class FindQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('all generates correctly') + void all() { + assertEquals('Find query not constructed correctly', "SELECT data FROM $TEST_TABLE".toString(), + FindQuery.all(TEST_TABLE)) + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), FindQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), FindQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND (data->>'c')::numeric < :d".toString(), + FindQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b'), Field.less('c', 14, ':d')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND data->>'c' < :d".toString(), + FindQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b'), Field.less('c', 14, ':d')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data @> :criteria".toString(), FindQuery.byContains(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byContains fails | SQLite') +// void byContainsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { FindQuery.byContains(TEST_TABLE) } +// } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + FindQuery.byJsonPath(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { FindQuery.byJsonPath(TEST_TABLE) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy new file mode 100644 index 0000000..f34c7a7 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy @@ -0,0 +1,97 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.PatchQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Patch` object + */ +@DisplayName('JVM | Groovy | Query | PatchQuery') +class PatchQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id".toString(), + PatchQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id".toString(), + PatchQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y".toString(), + PatchQuery.byFields(TEST_TABLE, List.of(Field.equal('z', '', ':y')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y".toString(), + PatchQuery.byFields(TEST_TABLE, List.of(Field.equal('z', '', ':y')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria".toString(), + PatchQuery.byContains(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byContains fails | SQLite') +// void byContainsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { PatchQuery.byContains(TEST_TABLE) } +// } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + PatchQuery.byJsonPath(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { PatchQuery.byJsonPath(TEST_TABLE) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy new file mode 100644 index 0000000..069bbec --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy @@ -0,0 +1,159 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.query.QueryUtils +import solutions.bitbadger.documents.support.ForceDialect + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `QueryUtils` class + */ +@DisplayName('JVM | Groovy | Query | QueryUtils') +class QueryUtilsTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('statementWhere generates correctly') + void statementWhere() { + assertEquals('Statements not combined correctly', 'x WHERE y', QueryUtils.statementWhere('x', 'y')) + } + + @Test + @DisplayName('byId generates a numeric ID query | PostgreSQL') + void byIdNumericPostgres() { + ForceDialect.postgres() + assertEquals("test WHERE (data->>'id')::numeric = :id", QueryUtils.byId('test', 9)) + } + + @Test + @DisplayName('byId generates an alphanumeric ID query | PostgreSQL') + void byIdAlphaPostgres() { + ForceDialect.postgres() + assertEquals("unit WHERE data->>'id' = :id", QueryUtils.byId('unit', '18')) + } + + @Test + @DisplayName('byId generates ID query | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals("yo WHERE data->>'id' = :id", QueryUtils.byId('yo', 27)) + } + + @Test + @DisplayName('byFields generates default field query | PostgreSQL') + void byFieldsMultipleDefaultPostgres() { + ForceDialect.postgres() + assertEquals("this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value", + QueryUtils.byFields('this', List.of(Field.equal('a', '', ':the_a'), Field.equal('b', 0, ':b_value')))) + } + + @Test + @DisplayName('byFields generates default field query | SQLite') + void byFieldsMultipleDefaultSQLite() { + ForceDialect.sqlite() + assertEquals("this WHERE data->>'a' = :the_a AND data->>'b' = :b_value", + QueryUtils.byFields('this', List.of(Field.equal('a', '', ':the_a'), Field.equal('b', 0, ':b_value')))) + } + + @Test + @DisplayName('byFields generates ANY field query | PostgreSQL') + void byFieldsMultipleAnyPostgres() { + ForceDialect.postgres() + assertEquals("that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value", + QueryUtils.byFields('that', List.of(Field.equal('a', '', ':the_a'), Field.equal('b', 0, ':b_value')), + FieldMatch.ANY)) + } + + @Test + @DisplayName('byFields generates ANY field query | SQLite') + void byFieldsMultipleAnySQLite() { + ForceDialect.sqlite() + assertEquals("that WHERE data->>'a' = :the_a OR data->>'b' = :b_value", + QueryUtils.byFields('that', List.of(Field.equal('a', '', ':the_a'), Field.equal('b', 0, ':b_value')), + FieldMatch.ANY)) + } + + @Test + @DisplayName('orderBy generates for no fields') + void orderByNone() { + assertEquals('ORDER BY should have been blank (PostgreSQL)', '', QueryUtils.orderBy(List.of(), + Dialect.POSTGRESQL)) + assertEquals('ORDER BY should have been blank (SQLite)', '', QueryUtils.orderBy(List.of(), Dialect.SQLITE)) + } + + @Test + @DisplayName('orderBy generates single, no direction | PostgreSQL') + void orderBySinglePostgres() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY data->>'TestField'", + QueryUtils.orderBy(List.of(Field.named('TestField')), Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('orderBy generates single, no direction | SQLite') + void orderBySingleSQLite() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY data->>'TestField'", + QueryUtils.orderBy(List.of(Field.named('TestField')), Dialect.SQLITE)) + } + + @Test + @DisplayName('orderBy generates multiple with direction | PostgreSQL') + void orderByMultiplePostgres() { + assertEquals('ORDER BY not constructed correctly', + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + QueryUtils.orderBy(List.of(Field.named('Nested.Test.Field DESC'), Field.named('AnotherField'), + Field.named('It DESC')), + Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('orderBy generates multiple with direction | SQLite') + void orderByMultipleSQLite() { + assertEquals('ORDER BY not constructed correctly', + " ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + QueryUtils.orderBy(List.of(Field.named('Nested.Test.Field DESC'), Field.named('AnotherField'), + Field.named('It DESC')), + Dialect.SQLITE)) + } + + @Test + @DisplayName('orderBy generates numeric ordering | PostgreSQL') + void orderByNumericPostgres() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY (data->>'Test')::numeric", + QueryUtils.orderBy(List.of(Field.named('n:Test')), Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('orderBy generates numeric ordering | SQLite') + void orderByNumericSQLite() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY data->>'Test'", + QueryUtils.orderBy(List.of(Field.named('n:Test')), Dialect.SQLITE)) + } + + @Test + @DisplayName('orderBy generates case-insensitive ordering | PostgreSQL') + void orderByCIPostgres() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", + QueryUtils.orderBy(List.of(Field.named('i:Test.Field DESC NULLS FIRST')), Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('orderBy generates case-insensitive ordering | SQLite') + void orderByCISQLite() { + assertEquals('ORDER BY not constructed correctly', + " ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", + QueryUtils.orderBy(List.of(Field.named('i:Test.Field ASC NULLS LAST')), Dialect.SQLITE)) + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala index 63ca4d9..7653c91 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala @@ -29,10 +29,10 @@ class AutoIdSpec extends AnyFunSpec with Matchers { describe("needsAutoId") { it("fails for null document") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.DISABLED, null, "id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.DISABLED, null, "id") } it("fails for missing ID property") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } it("returns false if disabled") { AutoId.needsAutoId(AutoId.DISABLED, "", "") shouldBe false @@ -62,7 +62,7 @@ class AutoIdSpec extends AnyFunSpec with Matchers { AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id") shouldBe false } it("fails for Number strategy and non-number ID") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } it("returns true for UUID strategy and blank ID") { AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id") shouldBe true @@ -71,7 +71,7 @@ class AutoIdSpec extends AnyFunSpec with Matchers { AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id") shouldBe false } it("fails for UUID strategy and non-string ID") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } it("returns true for Random String strategy and blank ID") { AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id") shouldBe true @@ -80,7 +80,7 @@ class AutoIdSpec extends AnyFunSpec with Matchers { AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id") shouldBe false } it("fails for Random String strategy and non-string ID") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala index c42e21f..183f4b6 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala @@ -27,7 +27,7 @@ class ConfigurationSpec extends AnyFunSpec with Matchers { describe("dialect") { it("is derived from connection string") { try { - an [DocumentException] should be thrownBy Configuration.dialect() + a [DocumentException] should be thrownBy Configuration.dialect() Configuration.setConnectionString("jdbc:postgresql:db") Configuration.dialect() shouldEqual Dialect.POSTGRESQL } finally { diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala index a1123d1..d54390b 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala @@ -13,7 +13,7 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { describe("withParameterName") { it("fails for invalid name") { - an [DocumentException] should be thrownBy Field.equal("it", "").withParameterName("2424") + a [DocumentException] should be thrownBy Field.equal("it", "").withParameterName("2424") } it("works with colon prefix") { val field = Field.equal("abc", "22").withQualifier("me") @@ -49,36 +49,36 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { describe("path") { it("generates simple unqualified PostgreSQL field") { - Field.greaterOrEqual("SomethingCool", 18) - .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data->>'SomethingCool'" + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "data->>'SomethingCool'" } it("generates simple qualified PostgreSQL field") { - Field.less("SomethingElse", 9).withQualifier("this") - .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "this.data->>'SomethingElse'" + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "this.data->>'SomethingElse'" } it("generates nested unqualified PostgreSQL field") { - Field.equal("My.Nested.Field", "howdy") - .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data#>>'{My,Nested,Field}'" + Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "data#>>'{My,Nested,Field}'" } it("generates nested qualified PostgreSQL field") { - Field.equal("Nest.Away", "doc").withQualifier("bird") - .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "bird.data#>>'{Nest,Away}'" + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "bird.data#>>'{Nest,Away}'" } it("generates simple unqualified SQLite field") { - Field.greaterOrEqual("SomethingCool", 18) - .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->>'SomethingCool'" + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "data->>'SomethingCool'" } it("generates simple qualified SQLite field") { - Field.less("SomethingElse", 9).withQualifier("this") - .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "this.data->>'SomethingElse'" + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "this.data->>'SomethingElse'" } it("generates nested unqualified SQLite field") { - Field.equal("My.Nested.Field", "howdy") - .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->'My'->'Nested'->>'Field'" + Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "data->'My'->'Nested'->>'Field'" } it("generates nested qualified SQLite field") { - Field.equal("Nest.Away", "doc").withQualifier("bird") - .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "bird.data->'Nest'->>'Away'" + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "bird.data->'Nest'->>'Away'" } } @@ -105,8 +105,8 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { } it("generates BETWEEN w/o qualifier, alphanumeric range | PostgreSQL") { ForceDialect.postgres() - Field.between("city", "Atlanta", "Chicago", ":city") - .toWhere shouldEqual "data->>'city' BETWEEN :citymin AND :citymax" + Field.between("city", "Atlanta", "Chicago", ":city").toWhere shouldEqual + "data->>'city' BETWEEN :citymin AND :citymax" } it("generates BETWEEN w/o qualifier | SQLite") { ForceDialect.sqlite() @@ -114,43 +114,43 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { } it("generates BETWEEN w/ qualifier, numeric range | PostgreSQL") { ForceDialect.postgres() - Field.between("age", 13, 17, "@age").withQualifier("test") - .toWhere shouldEqual "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax" + Field.between("age", 13, 17, "@age").withQualifier("test").toWhere shouldEqual + "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax" } it("generates BETWEEN w/ qualifier, alphanumeric range | PostgreSQL") { ForceDialect.postgres() - Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit") - .toWhere shouldEqual "unit.data->>'city' BETWEEN :citymin AND :citymax" + Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere shouldEqual + "unit.data->>'city' BETWEEN :citymin AND :citymax" } it("generates BETWEEN w/ qualifier | SQLite") { ForceDialect.sqlite() - Field.between("age", 13, 17, "@age").withQualifier("my") - .toWhere shouldEqual "my.data->>'age' BETWEEN @agemin AND @agemax" + Field.between("age", 13, 17, "@age").withQualifier("my").toWhere shouldEqual + "my.data->>'age' BETWEEN @agemin AND @agemax" } it("generates IN/any, numeric values | PostgreSQL") { ForceDialect.postgres() - Field.any("even", List(2, 4, 6).asJava, ":nbr") - .toWhere shouldEqual "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)" + Field.any("even", List(2, 4, 6).asJava, ":nbr").toWhere shouldEqual + "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)" } it("generates IN/any, alphanumeric values | PostgreSQL") { ForceDialect.postgres() - Field.any("test", List("Atlanta", "Chicago").asJava, ":city") - .toWhere shouldEqual "data->>'test' IN (:city_0, :city_1)" + Field.any("test", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual + "data->>'test' IN (:city_0, :city_1)" } it("generates IN/any | SQLite") { ForceDialect.sqlite() - Field.any("test", List("Atlanta", "Chicago").asJava, ":city") - .toWhere shouldEqual "data->>'test' IN (:city_0, :city_1)" + Field.any("test", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual + "data->>'test' IN (:city_0, :city_1)" } it("generates inArray | PostgreSQL") { ForceDialect.postgres() - Field.inArray("even", "tbl", List(2, 4, 6, 8).asJava, ":it") - .toWhere shouldEqual "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]" + Field.inArray("even", "tbl", List(2, 4, 6, 8).asJava, ":it").toWhere shouldEqual + "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]" } it("generates inArray | SQLite") { ForceDialect.sqlite() - Field.inArray("test", "tbl", List("Atlanta", "Chicago").asJava, ":city") - .toWhere shouldEqual "EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))" + Field.inArray("test", "tbl", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual + "EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))" } it("generates others w/o qualifier | PostgreSQL") { ForceDialect.postgres() @@ -170,8 +170,8 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { } it("generates parameter w/ qualifier | PostgreSQL") { ForceDialect.postgres() - Field.lessOrEqual("le_field", 18, ":it").withQualifier("q") - .toWhere shouldEqual "(q.data->>'le_field')::numeric <= :it" + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere shouldEqual + "(q.data->>'le_field')::numeric <= :it" } it("generates parameter w/ qualifier | SQLite") { ForceDialect.sqlite() @@ -391,7 +391,7 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { describe("static constructors") { it("fail for invalid parameter name") { - an [DocumentException] should be thrownBy Field.equal("a", "b", "that ain't it, Jack...") + a [DocumentException] should be thrownBy Field.equal("a", "b", "that ain't it, Jack...") } } @@ -403,12 +403,12 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->>'Simple'" } it("creates a nested PostgreSQL SQL name") { - (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL) - shouldEqual "data#>>'{A,Long,Path,to,the,Property}'") + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "data#>>'{A,Long,Path,to,the,Property}'" } it("creates a nested SQLite SQL name") { - (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL) - shouldEqual "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'") + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'" } it("creates a simple PostgreSQL JSON name") { Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON) shouldEqual "data->'Simple'" @@ -417,12 +417,12 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON) shouldEqual "data->'Simple'" } it("creates a nested PostgreSQL JSON name") { - (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON) - shouldEqual "data#>'{A,Long,Path,to,the,Property}'") + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON) shouldEqual + "data#>'{A,Long,Path,to,the,Property}'" } it("creates a nested SQLite JSON name") { - (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON) - shouldEqual "data->'A'->'Long'->'Path'->'to'->'the'->'Property'") + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON) shouldEqual + "data->'A'->'Long'->'Path'->'to'->'the'->'Property'" } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala index dbda937..3e393b7 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala @@ -20,7 +20,7 @@ class ParameterSpec extends AnyFunSpec with Matchers { p.getValue should be (null) } it("fails with incorrect prefix") { - an [DocumentException] should be thrownBy Parameter("it", ParameterType.JSON, "") + a [DocumentException] should be thrownBy Parameter("it", ParameterType.JSON, "") } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala new file mode 100644 index 0000000..6a24e4c --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class CountQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("all") { + it("generates correctly") { + CountQuery.all(TEST_TABLE) shouldEqual s"SELECT COUNT(*) AS it FROM $TEST_TABLE" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + CountQuery.byFields(TEST_TABLE, List(Field.equal("test", "", ":field0")).asJava) shouldEqual + s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + CountQuery.byFields(TEST_TABLE, List(Field.equal("test", "", ":field0")).asJava) shouldEqual + s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + CountQuery.byContains(TEST_TABLE) shouldEqual s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy CountQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + CountQuery.byJsonPath(TEST_TABLE) shouldEqual + s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy CountQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala new file mode 100644 index 0000000..dbf6aad --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala @@ -0,0 +1,81 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{Dialect, DocumentException, DocumentIndex} +import solutions.bitbadger.documents.query.DefinitionQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class DefinitionQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("ensureTableFor") { + it("generates correctly") { + DefinitionQuery.ensureTableFor("my.table", "JSONB") shouldEqual + "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)" + } + } + + describe("ensureTable") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DefinitionQuery.ensureTable(TEST_TABLE) shouldEqual + s"CREATE TABLE IF NOT EXISTS $TEST_TABLE (data JSONB NOT NULL)" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + DefinitionQuery.ensureTable(TEST_TABLE) shouldEqual + s"CREATE TABLE IF NOT EXISTS $TEST_TABLE (data TEXT NOT NULL)" + } + it("fails when no dialect is set") { + a [DocumentException] should be thrownBy DefinitionQuery.ensureTable(TEST_TABLE) + } + } + + describe("ensureKey") { + it("generates correctly with schema") { + DefinitionQuery.ensureKey("test.table", Dialect.POSTGRESQL) shouldEqual + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))" + } + it("generates correctly without schema") { + DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE) shouldEqual + s"CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))" + } + } + + describe("ensureIndexOn") { + it("generates multiple fields and directions") { + DefinitionQuery.ensureIndexOn("test.table", "gibberish", List("taco", "guac DESC", "salsa ASC").asJava, + Dialect.POSTGRESQL) shouldEqual + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" + } + it("generates nested field | PostgreSQL") { + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List("a.b.c").asJava, Dialect.POSTGRESQL) shouldEqual + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data#>>'{a,b,c}'))" + } + it("generates nested field | SQLite") { + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List("a.b.c").asJava, Dialect.SQLITE) shouldEqual + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data->'a'->'b'->>'c'))" + } + } + + describe("ensureDocumentOn") { + it("generates Full | PostgreSQL") { + ForceDialect.postgres() + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) shouldEqual + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)" + } + it("generates Optimized | PostgreSQL") { + ForceDialect.postgres() + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED) shouldEqual + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data jsonb_path_ops)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala new file mode 100644 index 0000000..728d1cd --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala @@ -0,0 +1,61 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class DeleteQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("byId") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DeleteQuery.byId(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + DeleteQuery.byId(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DeleteQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b")).asJava) shouldEqual + s"DELETE FROM $TEST_TABLE WHERE data->>'a' = :b" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + DeleteQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b")).asJava) shouldEqual + s"DELETE FROM $TEST_TABLE WHERE data->>'a' = :b" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DeleteQuery.byContains(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data @> :criteria" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy DeleteQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DeleteQuery.byJsonPath(TEST_TABLE) shouldEqual + s"DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy DeleteQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala new file mode 100644 index 0000000..adbef90 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala @@ -0,0 +1,85 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{AutoId, Configuration, DocumentException} +import solutions.bitbadger.documents.query.DocumentQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +class DocumentQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("insert") { + it("generates no auto ID | PostgreSQL") { + ForceDialect.postgres() + DocumentQuery.insert(TEST_TABLE) shouldEqual s"INSERT INTO $TEST_TABLE VALUES (:data)" + } + it("generates no auto ID | SQLite") { + ForceDialect.sqlite() + DocumentQuery.insert(TEST_TABLE) shouldEqual s"INSERT INTO $TEST_TABLE VALUES (:data)" + } + it("generates auto number | PostgreSQL") { + ForceDialect.postgres() + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER) shouldEqual + s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || ('{\"id\":' " + + s"|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $TEST_TABLE) || '}')::jsonb)" + } + it("generates auto number | SQLite") { + ForceDialect.sqlite() + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER) shouldEqual + s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', " + + s"(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $TEST_TABLE)))" + } + it("generates auto UUID | PostgreSQL") { + ForceDialect.postgres() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + query should startWith (s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"") + query should endWith ("\"}')") + } + it("generates auto UUID | SQLite") { + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + query should startWith (s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '") + query should endWith ("'))") + } + it("generates auto random string | PostgreSQL") { + try { + ForceDialect.postgres() + Configuration.idStringLength = 8 + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + query should startWith (s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"") + query should endWith ("\"}')") + query.replace(s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "") + .length shouldEqual 8 + } finally { + Configuration.idStringLength = 16 + } + } + it("insert generates auto random string | SQLite") { + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + query should startWith (s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '") + query should endWith ("'))") + query.replace(s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '", "").replace("'))", "") + .length shouldEqual Configuration.idStringLength + } + it("fails when no dialect is set") { + a [DocumentException] should be thrownBy DocumentQuery.insert(TEST_TABLE) + } + } + + describe("save") { + it("generates correctly") { + ForceDialect.postgres() + DocumentQuery.save(TEST_TABLE) shouldEqual + s"INSERT INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data" + } + } + + describe("update") { + it("generates successfully") { + DocumentQuery.update(TEST_TABLE) shouldEqual s"UPDATE $TEST_TABLE SET data = :data" + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala new file mode 100644 index 0000000..d539543 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala @@ -0,0 +1,64 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.ExistsQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class ExistsQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("byId") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + ExistsQuery.byId(TEST_TABLE) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + ExistsQuery.byId(TEST_TABLE) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + ExistsQuery.byFields(TEST_TABLE, List(Field.equal("it", 7, ":test")).asJava) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE (data->>'it')::numeric = :test) AS it" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + ExistsQuery.byFields(TEST_TABLE, List(Field.equal("it", 7, ":test")).asJava) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'it' = :test) AS it" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + ExistsQuery.byContains(TEST_TABLE) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data @> :criteria) AS it" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy ExistsQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + ExistsQuery.byJsonPath(TEST_TABLE) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)) AS it" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy ExistsQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala new file mode 100644 index 0000000..5d8a585 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala @@ -0,0 +1,67 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class FindQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("all") { + it("generates correctly") { + FindQuery.all(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE" + } + } + + describe("byId") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + FindQuery.byId(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + FindQuery.byId(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + FindQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b"), Field.less("c", 14, ":d")).asJava) shouldEqual + s"SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND (data->>'c')::numeric < :d" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + FindQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b"), Field.less("c", 14, ":d")).asJava) shouldEqual + s"SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND data->>'c' < :d" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + FindQuery.byContains(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data @> :criteria" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy FindQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + FindQuery.byJsonPath(TEST_TABLE) shouldEqual + s"SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy FindQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala new file mode 100644 index 0000000..a37a8b3 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.PatchQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class PatchQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("byId") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + PatchQuery.byId(TEST_TABLE) shouldEqual s"UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + PatchQuery.byId(TEST_TABLE) shouldEqual + s"UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + PatchQuery.byFields(TEST_TABLE, List(Field.equal("z", "", ":y")).asJava) shouldEqual + s"UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + PatchQuery.byFields(TEST_TABLE, List(Field.equal("z", "", ":y")).asJava) shouldEqual + s"UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + PatchQuery.byContains(TEST_TABLE) shouldEqual + s"UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy PatchQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + PatchQuery.byJsonPath(TEST_TABLE) shouldEqual + s"UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy PatchQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala new file mode 100644 index 0000000..0b58169 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala @@ -0,0 +1,103 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{Dialect, Field, FieldMatch} +import solutions.bitbadger.documents.query.QueryUtils +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect + +import scala.jdk.CollectionConverters.* + +class QueryUtilsSpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("statementWhere") { + it("generates correctly") { + QueryUtils.statementWhere("x", "y") shouldEqual "x WHERE y" + } + } + + describe("byId") { + it("generates a numeric ID query | PostgreSQL") { + ForceDialect.postgres() + QueryUtils.byId("test", 9) shouldEqual "test WHERE (data->>'id')::numeric = :id" + } + it("generates an alphanumeric ID query | PostgreSQL") { + ForceDialect.postgres() + QueryUtils.byId("unit", "18") shouldEqual "unit WHERE data->>'id' = :id" + } + it("generates ID query | SQLite") { + ForceDialect.sqlite() + QueryUtils.byId("yo", 27) shouldEqual "yo WHERE data->>'id' = :id" + } + } + + describe("byFields") { + it("generates default field query | PostgreSQL") { + ForceDialect.postgres() + QueryUtils.byFields("this", + List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava) shouldEqual + "this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value" + } + it("generates default field query | SQLite") { + ForceDialect.sqlite() + QueryUtils.byFields("this", + List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava) shouldEqual + "this WHERE data->>'a' = :the_a AND data->>'b' = :b_value" + } + it("generates ANY field query | PostgreSQL") { + ForceDialect.postgres() + QueryUtils.byFields("that", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava, + FieldMatch.ANY) shouldEqual + "that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value" + } + it("generates ANY field query | SQLite") { + ForceDialect.sqlite() + QueryUtils.byFields("that", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava, + FieldMatch.ANY) shouldEqual + "that WHERE data->>'a' = :the_a OR data->>'b' = :b_value" + } + } + + describe("orderBy") { + it("generates for no fields") { + QueryUtils.orderBy(List().asJava, Dialect.POSTGRESQL) shouldEqual "" + QueryUtils.orderBy(List().asJava, Dialect.SQLITE) shouldEqual "" + } + it("generates single, no direction | PostgreSQL") { + QueryUtils.orderBy(List(Field.named("TestField")).asJava, Dialect.POSTGRESQL) shouldEqual + " ORDER BY data->>'TestField'" + } + it("generates single, no direction | SQLite") { + QueryUtils.orderBy(List(Field.named("TestField")).asJava, Dialect.SQLITE) shouldEqual + " ORDER BY data->>'TestField'" + } + it("generates multiple with direction | PostgreSQL") { + QueryUtils.orderBy( + List(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")).asJava, + Dialect.POSTGRESQL) shouldEqual + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC" + } + it("generates multiple with direction | SQLite") { + QueryUtils.orderBy( + List(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")).asJava, + Dialect.SQLITE) shouldEqual + " ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC" + } + it("generates numeric ordering | PostgreSQL") { + QueryUtils.orderBy(List(Field.named("n:Test")).asJava, Dialect.POSTGRESQL) shouldEqual + " ORDER BY (data->>'Test')::numeric" + } + it("generates numeric ordering | SQLite") { + QueryUtils.orderBy(List(Field.named("n:Test")).asJava, Dialect.SQLITE) shouldEqual " ORDER BY data->>'Test'" + } + it("generates case-insensitive ordering | PostgreSQL") { + QueryUtils.orderBy(List(Field.named("i:Test.Field DESC NULLS FIRST")).asJava, Dialect.POSTGRESQL) shouldEqual + " ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST" + } + it("generates case-insensitive ordering | SQLite") { + QueryUtils.orderBy(List(Field.named("i:Test.Field ASC NULLS LAST")).asJava, Dialect.SQLITE) shouldEqual + " ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST" + } + } +}