From 2b86864f82fe5f4119140b6f4d33c17b042f7ad5 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Mar 2025 17:16:45 -0400 Subject: [PATCH] Finish root ns Groovy/Scala tests --- .../documents/groovy/DocumentIndexTest.groovy | 26 + .../documents/groovy/FieldTest.groovy | 600 ++++++++++++++++++ .../bitbadger/documents/groovy/OpTest.groovy | 80 +++ .../documents/groovy/ParameterNameTest.groovy | 30 + .../documents/groovy/ParameterTest.groovy | 41 ++ src/jvm/src/test/kotlin/FieldTest.kt | 2 +- .../documents/scala/AutoIdSpec.scala | 47 +- .../documents/scala/ClearConfiguration.scala | 11 + .../documents/scala/ConfigurationSpec.scala | 13 +- .../documents/scala/DialectSpec.scala | 11 +- .../documents/scala/DocumentIndexSpec.scala | 17 + .../documents/scala/FieldMatchSpec.scala | 15 +- .../bitbadger/documents/scala/FieldSpec.scala | 428 +++++++++++++ .../bitbadger/documents/scala/OpSpec.scala | 44 ++ .../documents/scala/ParameterNameSpec.scala | 23 + .../documents/scala/ParameterSpec.scala | 26 + 16 files changed, 1370 insertions(+), 44 deletions(-) create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy new file mode 100644 index 0000000..03eb50d --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentIndex + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `DocumentIndex` enum + */ +@DisplayName('JVM | Groovy | DocumentIndex') +class DocumentIndexTest { + + @Test + @DisplayName('FULL uses proper SQL') + void fullSQL() { + assertEquals('The SQL for Full is incorrect', '', DocumentIndex.FULL.sql) + } + + @Test + @DisplayName('OPTIMIZED uses proper SQL') + void optimizedSQL() { + assertEquals('The SQL for Optimized is incorrect', ' jsonb_path_ops', DocumentIndex.OPTIMIZED.sql) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy new file mode 100644 index 0000000..e03e0e9 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy @@ -0,0 +1,600 @@ +package solutions.bitbadger.documents.groovy + +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.Field +import solutions.bitbadger.documents.FieldFormat +import solutions.bitbadger.documents.Op +import solutions.bitbadger.documents.support.ForceDialect + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Field` class + */ +@DisplayName('JVM | Groovy | Field') +class FieldTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + // ~~~ INSTANCE METHODS ~~~ + +// TODO: fix java.base open issue +// @Test +// @DisplayName('withParameterName fails for invalid name') +// void withParamNameFails() { +// assertThrows(DocumentException) { Field.equal('it', '').withParameterName('2424') } +// } + + @Test + @DisplayName('withParameterName works with colon prefix') + void withParamNameColon() { + def field = Field.equal('abc', '22').withQualifier('me') + def withParam = field.withParameterName(':test') + assertNotSame('A new Field instance should have been created', field, withParam) + assertEquals('Name should have been preserved', field.name, withParam.name) + assertEquals('Comparison should have been preserved', field.comparison, withParam.comparison) + assertEquals('Parameter name not set correctly', ':test', withParam.parameterName) + assertEquals('Qualifier should have been preserved', field.qualifier, withParam.qualifier) + } + + @Test + @DisplayName('withParameterName works with at-sign prefix') + void withParamNameAtSign() { + def field = Field.equal('def', '44') + def withParam = field.withParameterName('@unit') + assertNotSame('A new Field instance should have been created', field, withParam) + assertEquals('Name should have been preserved', field.name, withParam.name) + assertEquals('Comparison should have been preserved', field.comparison, withParam.comparison) + assertEquals('Parameter name not set correctly', '@unit', withParam.parameterName) + assertEquals('Qualifier should have been preserved', field.qualifier, withParam.qualifier) + } + + @Test + @DisplayName('withQualifier sets qualifier correctly') + void withQualifier() { + def field = Field.equal('j', 'k') + def withQual = field.withQualifier('test') + assertNotSame('A new Field instance should have been created', field, withQual) + assertEquals('Name should have been preserved', field.name, withQual.name) + assertEquals('Comparison should have been preserved', field.comparison, withQual.comparison) + assertEquals('Parameter Name should have been preserved', field.parameterName, withQual.parameterName) + assertEquals('Qualifier not set correctly', 'test', withQual.qualifier) + } + + @Test + @DisplayName('path generates for simple unqualified PostgreSQL field') + void pathPostgresSimpleUnqualified() { + assertEquals('Path not correct', "data->>'SomethingCool'", + Field.greaterOrEqual('SomethingCool', 18).path(Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for simple qualified PostgreSQL field') + void pathPostgresSimpleQualified() { + assertEquals('Path not correct', "this.data->>'SomethingElse'", + Field.less('SomethingElse', 9).withQualifier('this').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for nested unqualified PostgreSQL field') + void pathPostgresNestedUnqualified() { + assertEquals('Path not correct', "data#>>'{My,Nested,Field}'", + Field.equal('My.Nested.Field', 'howdy').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for nested qualified PostgreSQL field') + void pathPostgresNestedQualified() { + assertEquals('Path not correct', "bird.data#>>'{Nest,Away}'", + Field.equal('Nest.Away', 'doc').withQualifier('bird').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for simple unqualified SQLite field') + void pathSQLiteSimpleUnqualified() { + assertEquals('Path not correct', "data->>'SomethingCool'", + Field.greaterOrEqual('SomethingCool', 18).path(Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for simple qualified SQLite field') + void pathSQLiteSimpleQualified() { + assertEquals('Path not correct', "this.data->>'SomethingElse'", + Field.less('SomethingElse', 9).withQualifier('this').path(Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for nested unqualified SQLite field') + void pathSQLiteNestedUnqualified() { + assertEquals('Path not correct', "data->'My'->'Nested'->>'Field'", + Field.equal('My.Nested.Field', 'howdy').path(Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for nested qualified SQLite field') + void pathSQLiteNestedQualified() { + assertEquals('Path not correct', "bird.data->'Nest'->>'Away'", + Field.equal('Nest.Away', 'doc').withQualifier('bird').path(Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('toWhere generates for exists w/o qualifier | PostgreSQL') + void toWhereExistsNoQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'that_field' IS NOT NULL", + Field.exists('that_field').toWhere()) + } + + @Test + @DisplayName('toWhere generates for exists w/o qualifier | SQLite') + void toWhereExistsNoQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'that_field' IS NOT NULL", + Field.exists('that_field').toWhere()) + } + + @Test + @DisplayName('toWhere generates for not-exists w/o qualifier | PostgreSQL') + void toWhereNotExistsNoQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'a_field' IS NULL", + Field.notExists('a_field').toWhere()) + } + + @Test + @DisplayName('toWhere generates for not-exists w/o qualifier | SQLite') + void toWhereNotExistsNoQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'a_field' IS NULL", + Field.notExists('a_field').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier, numeric range | PostgreSQL') + void toWhereBetweenNoQualNumericPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', + "(data->>'age')::numeric BETWEEN @agemin AND @agemax", Field.between('age', 13, 17, '@age').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier, alphanumeric range | PostgreSQL') + void toWhereBetweenNoQualAlphaPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'city' BETWEEN :citymin AND :citymax", + Field.between('city', 'Atlanta', 'Chicago', ':city').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier | SQLite') + void toWhereBetweenNoQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'age' BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier, numeric range | PostgreSQL') + void toWhereBetweenQualNumericPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', + "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').withQualifier('test').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier, alphanumeric range | PostgreSQL') + void toWhereBetweenQualAlphaPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "unit.data->>'city' BETWEEN :citymin AND :citymax", + Field.between('city', 'Atlanta', 'Chicago', ':city').withQualifier('unit').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier | SQLite') + void toWhereBetweenQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "my.data->>'age' BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').withQualifier('my').toWhere()) + } + + @Test + @DisplayName('toWhere generates for IN/any, numeric values | PostgreSQL') + void toWhereAnyNumericPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', + "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", + Field.any('even', List.of(2, 4, 6), ':nbr').toWhere()) + } + + @Test + @DisplayName('toWhere generates for IN/any, alphanumeric values | PostgreSQL') + void toWhereAnyAlphaPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'test' IN (:city_0, :city_1)", + Field.any('test', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + } + + @Test + @DisplayName('toWhere generates for IN/any | SQLite') + void toWhereAnySQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'test' IN (:city_0, :city_1)", + Field.any('test', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + } + + @Test + @DisplayName('toWhere generates for inArray | PostgreSQL') + void toWhereInArrayPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]", + Field.inArray('even', 'tbl', List.of(2, 4, 6, 8), ':it').toWhere()) + } + + @Test + @DisplayName('toWhere generates for inArray | SQLite') + void toWhereInArraySQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', + "EXISTS (SELECT 1 FROM json_each(tbl.data, '\$.test') WHERE value IN (:city_0, :city_1))", + Field.inArray('test', 'tbl', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + } + + @Test + @DisplayName('toWhere generates for others w/o qualifier | PostgreSQL') + void toWhereOtherNoQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'some_field' = :value", + Field.equal('some_field', '', ':value').toWhere()) + } + + @Test + @DisplayName('toWhere generates for others w/o qualifier | SQLite') + void toWhereOtherNoQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'some_field' = :value", + Field.equal('some_field', '', ':value').toWhere()) + } + + @Test + @DisplayName('toWhere generates no-parameter w/ qualifier | PostgreSQL') + void toWhereNoParamWithQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "test.data->>'no_field' IS NOT NULL", + Field.exists('no_field').withQualifier('test').toWhere()) + } + + @Test + @DisplayName('toWhere generates no-parameter w/ qualifier | SQLite') + void toWhereNoParamWithQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "test.data->>'no_field' IS NOT NULL", + Field.exists('no_field').withQualifier('test').toWhere()) + } + + @Test + @DisplayName('toWhere generates parameter w/ qualifier | PostgreSQL') + void toWhereParamWithQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "(q.data->>'le_field')::numeric <= :it", + Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere()) + } + + @Test + @DisplayName('toWhere generates parameter w/ qualifier | SQLite') + void toWhereParamWithQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "q.data->>'le_field' <= :it", + Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere()) + } + + // ~~~ STATIC CONSTRUCTOR TESTS ~~~ + + @Test + @DisplayName('equal constructs a field w/o parameter name') + void equalCtor() { + def field = Field.equal('Test', 14) + assertEquals('Field name not filled correctly', 'Test', field.name) + assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 14, field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('equal constructs a field w/ parameter name') + void equalParameterCtor() { + def field = Field.equal('Test', 14, ':w') + assertEquals('Field name not filled correctly', 'Test', field.name) + assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 14, field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':w', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('greater constructs a field w/o parameter name') + void greaterCtor() { + def field = Field.greater('Great', 'night') + assertEquals('Field name not filled correctly', 'Great', field.name) + assertEquals('Field comparison operation not filled correctly', Op.GREATER, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'night', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('greater constructs a field w/ parameter name') + void greaterParameterCtor() { + def field = Field.greater('Great', 'night', ':yeah') + assertEquals('Field name not filled correctly', 'Great', field.name) + assertEquals('Field comparison operation not filled correctly', Op.GREATER, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'night', field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':yeah', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('greaterOrEqual constructs a field w/o parameter name') + void greaterOrEqualCtor() { + def field = Field.greaterOrEqual('Nice', 88L) + assertEquals('Field name not filled correctly', 'Nice', field.name) + assertEquals('Field comparison operation not filled correctly', Op.GREATER_OR_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 88L, field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('greaterOrEqual constructs a field w/ parameter name') + void greaterOrEqualParameterCtor() { + def field = Field.greaterOrEqual('Nice', 88L, ':nice') + assertEquals('Field name not filled correctly', 'Nice', field.name) + assertEquals('Field comparison operation not filled correctly', Op.GREATER_OR_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 88L, field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':nice', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('less constructs a field w/o parameter name') + void lessCtor() { + def field = Field.less('Lesser', 'seven') + assertEquals('Field name not filled correctly', 'Lesser', field.name) + assertEquals('Field comparison operation not filled correctly', Op.LESS, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'seven', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('less constructs a field w/ parameter name') + void lessParameterCtor() { + def field = Field.less('Lesser', 'seven', ':max') + assertEquals('Field name not filled correctly', 'Lesser', field.name) + assertEquals('Field comparison operation not filled correctly', Op.LESS, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'seven', field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':max', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('lessOrEqual constructs a field w/o parameter name') + void lessOrEqualCtor() { + def field = Field.lessOrEqual('Nobody', 'KNOWS') + assertEquals('Field name not filled correctly', 'Nobody', field.name) + assertEquals('Field comparison operation not filled correctly', Op.LESS_OR_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'KNOWS', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('lessOrEqual constructs a field w/ parameter name') + void lessOrEqualParameterCtor() { + def field = Field.lessOrEqual('Nobody', 'KNOWS', ':nope') + assertEquals('Field name not filled correctly', 'Nobody', field.name) + assertEquals('Field comparison operation not filled correctly', Op.LESS_OR_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'KNOWS', field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':nope', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('notEqual constructs a field w/o parameter name') + void notEqualCtor() { + def field = Field.notEqual('Park', 'here') + assertEquals('Field name not filled correctly', 'Park', field.name) + assertEquals('Field comparison operation not filled correctly', Op.NOT_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'here', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('notEqual constructs a field w/ parameter name') + void notEqualParameterCtor() { + def field = Field.notEqual('Park', 'here', ':now') + assertEquals('Field name not filled correctly', 'Park', field.name) + assertEquals('Field comparison operation not filled correctly', Op.NOT_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'here', field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':now', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('between constructs a field w/o parameter name') + void betweenCtor() { + def field = Field.between('Age', 18, 49) + assertEquals('Field name not filled correctly', 'Age', field.name) + assertEquals('Field comparison operation not filled correctly', Op.BETWEEN, field.comparison.op) + assertEquals('Field comparison min value not filled correctly', 18, field.comparison.value.first) + assertEquals('Field comparison max value not filled correctly', 49, field.comparison.value.second, ) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('between constructs a field w/ parameter name') + void betweenParameterCtor() { + def field = Field.between('Age', 18, 49, ':limit') + assertEquals('Field name not filled correctly', 'Age', field.name) + assertEquals('Field comparison operation not filled correctly', Op.BETWEEN, field.comparison.op) + assertEquals('Field comparison min value not filled correctly', 18, field.comparison.value.first) + assertEquals('Field comparison max value not filled correctly', 49, field.comparison.value.second) + assertEquals('Field parameter name not filled correctly', ':limit', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('any constructs a field w/o parameter name') + void anyCtor() { + def field = Field.any('Here', List.of(8, 16, 32)) + assertEquals('Field name not filled correctly', 'Here', field.name) + assertEquals('Field comparison operation not filled correctly', Op.IN, field.comparison.op) + assertEquals('Field comparison value not filled correctly', List.of(8, 16, 32), field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('any constructs a field w/ parameter name') + void anyParameterCtor() { + def field = Field.any('Here', List.of(8, 16, 32), ':list') + assertEquals('Field name not filled correctly', 'Here', field.name) + assertEquals('Field comparison operation not filled correctly', Op.IN, field.comparison.op) + assertEquals('Field comparison value not filled correctly', List.of(8, 16, 32), field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':list', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('inArray constructs a field w/o parameter name') + void inArrayCtor() { + def field = Field.inArray('ArrayField', 'table', List.of('z')) + assertEquals('Field name not filled correctly', 'ArrayField', field.name) + assertEquals('Field comparison operation not filled correctly', Op.IN_ARRAY, field.comparison.op) + assertEquals('Field comparison table not filled correctly', 'table', field.comparison.value.first) + assertEquals('Field comparison values not filled correctly', List.of('z'), field.comparison.value.second) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('inArray constructs a field w/ parameter name') + void inArrayParameterCtor() { + def field = Field.inArray('ArrayField', 'table', List.of('z'), ':a') + assertEquals('Field name not filled correctly', 'ArrayField', field.name) + assertEquals('Field comparison operation not filled correctly', Op.IN_ARRAY, field.comparison.op) + assertEquals('Field comparison table not filled correctly', 'table', field.comparison.value.first) + assertEquals('Field comparison values not filled correctly', List.of('z'), field.comparison.value.second) + assertEquals('Field parameter name not filled correctly', ':a', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('exists constructs a field') + void existsCtor() { + def field = Field.exists 'Groovy' + assertEquals('Field name not filled correctly', 'Groovy', field.name) + assertEquals('Field comparison operation not filled correctly', Op.EXISTS, field.comparison.op) + assertEquals('Field comparison value not filled correctly', '', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('notExists constructs a field') + void notExistsCtor() { + def field = Field.notExists 'Groovy' + assertEquals('Field name not filled correctly', 'Groovy', field.name) + assertEquals('Field comparison operation not filled correctly', Op.NOT_EXISTS, field.comparison.op) + assertEquals('Field comparison value not filled correctly', '', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('named constructs a field') + void namedCtor() { + def field = Field.named('Tacos') + assertEquals('Field name not filled correctly', 'Tacos', field.name) + assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', '', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + +// TODO: fix java.base open issue +// @Test +// @DisplayName('static constructors fail for invalid parameter name') +// void staticCtorsFailOnParamName() { +// assertThrows(DocumentException) { Field.equal('a', 'b', "that ain't it, Jack...") } +// } + + @Test + @DisplayName('nameToPath creates a simple PostgreSQL SQL name') + void nameToPathPostgresSimpleSQL() { + assertEquals('Path not constructed correctly', "data->>'Simple'", + Field.nameToPath('Simple', Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('nameToPath creates a simple SQLite SQL name') + void nameToPathSQLiteSimpleSQL() { + assertEquals('Path not constructed correctly', "data->>'Simple'", + Field.nameToPath('Simple', Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('nameToPath creates a nested PostgreSQL SQL name') + void nameToPathPostgresNestedSQL() { + assertEquals('Path not constructed correctly', "data#>>'{A,Long,Path,to,the,Property}'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('nameToPath creates a nested SQLite SQL name') + void nameToPathSQLiteNestedSQL() { + assertEquals('Path not constructed correctly', "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('nameToPath creates a simple PostgreSQL JSON name') + void nameToPathPostgresSimpleJSON() { + assertEquals('Path not constructed correctly', "data->'Simple'", + Field.nameToPath('Simple', Dialect.POSTGRESQL, FieldFormat.JSON)) + } + + @Test + @DisplayName('nameToPath creates a simple SQLite JSON name') + void nameToPathSQLiteSimpleJSON() { + assertEquals('Path not constructed correctly', "data->'Simple'", + Field.nameToPath('Simple', Dialect.SQLITE, FieldFormat.JSON)) + } + + @Test + @DisplayName('nameToPath creates a nested PostgreSQL JSON name') + void nameToPathPostgresNestedJSON() { + assertEquals('Path not constructed correctly', "data#>'{A,Long,Path,to,the,Property}'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.POSTGRESQL, FieldFormat.JSON)) + } + + @Test + @DisplayName('nameToPath creates a nested SQLite JSON name') + void nameToPathSQLiteNestedJSON() { + assertEquals('Path not constructed correctly', "data->'A'->'Long'->'Path'->'to'->'the'->'Property'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.SQLITE, FieldFormat.JSON)) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy new file mode 100644 index 0000000..47118f0 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy @@ -0,0 +1,80 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Op + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Op` enum + */ +@DisplayName('JVM | Groovy | Op') +class OpTest { + + @Test + @DisplayName('EQUAL uses proper SQL') + void equalSQL() { + assertEquals('The SQL for equal is incorrect', '=', Op.EQUAL.sql) + } + + @Test + @DisplayName('GREATER uses proper SQL') + void greaterSQL() { + assertEquals('The SQL for greater is incorrect', '>', Op.GREATER.sql) + } + + @Test + @DisplayName('GREATER_OR_EQUAL uses proper SQL') + void greaterOrEqualSQL() { + assertEquals('The SQL for greater-or-equal is incorrect', '>=', Op.GREATER_OR_EQUAL.sql) + } + + @Test + @DisplayName('LESS uses proper SQL') + void lessSQL() { + assertEquals('The SQL for less is incorrect', '<', Op.LESS.sql) + } + + @Test + @DisplayName('LESS_OR_EQUAL uses proper SQL') + void lessOrEqualSQL() { + assertEquals('The SQL for less-or-equal is incorrect', '<=', Op.LESS_OR_EQUAL.sql) + } + + @Test + @DisplayName('NOT_EQUAL uses proper SQL') + void notEqualSQL() { + assertEquals('The SQL for not-equal is incorrect', '<>', Op.NOT_EQUAL.sql) + } + + @Test + @DisplayName('BETWEEN uses proper SQL') + void betweenSQL() { + assertEquals('The SQL for between is incorrect', 'BETWEEN', Op.BETWEEN.sql) + } + + @Test + @DisplayName('IN uses proper SQL') + void inSQL() { + assertEquals('The SQL for in is incorrect', 'IN', Op.IN.sql) + } + + @Test + @DisplayName('IN_ARRAY uses proper SQL') + void inArraySQL() { + assertEquals('The SQL for in-array is incorrect', '??|', Op.IN_ARRAY.sql) + } + + @Test + @DisplayName('EXISTS uses proper SQL') + void existsSQL() { + assertEquals('The SQL for exists is incorrect', 'IS NOT NULL', Op.EXISTS.sql) + } + + @Test + @DisplayName('NOT_EXISTS uses proper SQL') + void notExistsSQL() { + assertEquals('The SQL for not-exists is incorrect', 'IS NULL', Op.NOT_EXISTS.sql) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy new file mode 100644 index 0000000..8beb54b --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy @@ -0,0 +1,30 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.ParameterName +import static groovy.test.GroovyAssert.assertEquals +/** + * Unit tests for the `ParameterName` class + */ +@DisplayName('JVM | Groovy | ParameterName') +class ParameterNameTest { + + @Test + @DisplayName('derive works when given existing names') + void withExisting() { + def names = new ParameterName() + assertEquals('Name should have been :taco', ':taco', names.derive(':taco')) + assertEquals('Counter should not have advanced for named field', ':field0', names.derive(null)) + } + + @Test + @DisplayName('derive works when given all anonymous fields') + void allAnonymous() { + def names = new ParameterName() + assertEquals('Anonymous field name should have been returned', ':field0', names.derive(null)) + assertEquals('Counter should have advanced from previous call', ':field1', names.derive(null)) + assertEquals('Counter should have advanced from previous call', ':field2', names.derive(null)) + assertEquals('Counter should have advanced from previous call', ':field3', names.derive(null)) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy new file mode 100644 index 0000000..1cd06ab --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy @@ -0,0 +1,41 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Parameter` class + */ +@DisplayName('JVM | Groovy | Parameter') +class ParameterTest { + + @Test + @DisplayName("Construction with colon-prefixed name") + void ctorWithColon() { + def p = new Parameter(":test", ParameterType.STRING, "ABC") + assertEquals("Parameter name was incorrect", ":test", p.name) + assertEquals("Parameter type was incorrect", ParameterType.STRING, p.type) + assertEquals("Parameter value was incorrect", "ABC", p.value) + } + + @Test + @DisplayName("Construction with at-sign-prefixed name") + void ctorWithAtSign() { + def p = new Parameter("@yo", ParameterType.NUMBER, null) + assertEquals("Parameter name was incorrect", "@yo", p.name) + assertEquals("Parameter type was incorrect", ParameterType.NUMBER, p.type) + assertNull("Parameter value was incorrect", p.value) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName("Construction fails with incorrect prefix") +// void ctorFailsForPrefix() { +// assertThrows(DocumentException) { new Parameter("it", ParameterType.JSON, "") } +// } +} diff --git a/src/jvm/src/test/kotlin/FieldTest.kt b/src/jvm/src/test/kotlin/FieldTest.kt index 4544d20..d7a616a 100644 --- a/src/jvm/src/test/kotlin/FieldTest.kt +++ b/src/jvm/src/test/kotlin/FieldTest.kt @@ -452,7 +452,7 @@ class FieldTest { assertEquals(Op.BETWEEN, field.comparison.op, "Field comparison operation not filled correctly") assertEquals(18, field.comparison.value.first, "Field comparison min value not filled correctly") assertEquals(49, field.comparison.value.second, "Field comparison max value not filled correctly") - assertEquals(":limit", field.parameterName, "The parameter name should have been null") + assertEquals(":limit", field.parameterName, "Field parameter name not filled correctly") assertNull(field.qualifier, "The qualifier should have been null") } 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 ecfa9a2..63ca4d9 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 @@ -1,85 +1,86 @@ package solutions.bitbadger.documents.scala import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import solutions.bitbadger.documents.scala.support.{ByteIdClass, IntIdClass, LongIdClass, ShortIdClass, StringIdClass} import solutions.bitbadger.documents.{AutoId, DocumentException} -class AutoIdSpec extends AnyFunSpec { +class AutoIdSpec extends AnyFunSpec with Matchers { describe("generateUUID") { it("generates a UUID string") { - assert(32 == AutoId.generateUUID().length) + AutoId.generateUUID().length shouldEqual 32 } } describe("generateRandomString") { it("generates a random hex character string of an even length") { - assert(8 == AutoId.generateRandomString(8).length) + AutoId.generateRandomString(8).length shouldEqual 8 } it("generates a random hex character string of an odd length") { - assert(11 == AutoId.generateRandomString(11).length) + AutoId.generateRandomString(11).length shouldEqual 11 } it("generates different random hex character strings") { val result1 = AutoId.generateRandomString(16) val result2 = AutoId.generateRandomString(16) - assert(result1 != result2) + result1 should not be theSameInstanceAs (result2) } } describe("needsAutoId") { it("fails for null document") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.DISABLED, null, "id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.DISABLED, null, "id") } it("fails for missing ID property") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } it("returns false if disabled") { - assert(!AutoId.needsAutoId(AutoId.DISABLED, "", "")) + AutoId.needsAutoId(AutoId.DISABLED, "", "") shouldBe false } it("returns true for Number strategy and byte ID of 0") { - assert(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id")) + AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id") shouldBe true } it("returns false for Number strategy and byte ID of non-0") { - assert(!AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id")) + AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id") shouldBe false } it("returns true for Number strategy and short ID of 0") { - assert(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id")) + AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id") shouldBe true } it("returns false for Number strategy and short ID of non-0") { - assert(!AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id")) + AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id") shouldBe false } it("returns true for Number strategy and int ID of 0") { - assert(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id")) + AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id") shouldBe true } it("returns false for Number strategy and int ID of non-0") { - assert(!AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id")) + AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id") shouldBe false } it("returns true for Number strategy and long ID of 0") { - assert(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id")) + AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id") shouldBe true } it("returns false for Number strategy and long ID of non-0") { - assert(!AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id")) + AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id") shouldBe false } it("fails for Number strategy and non-number ID") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } it("returns true for UUID strategy and blank ID") { - assert(AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id")) + AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id") shouldBe true } it("returns false for UUID strategy and non-blank ID") { - assert(!AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id")) + AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id") shouldBe false } it("fails for UUID strategy and non-string ID") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } it("returns true for Random String strategy and blank ID") { - assert(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id")) + AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id") shouldBe true } it("returns false for Random String strategy and non-blank ID") { - assert(!AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id")) + AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id") shouldBe false } it("fails for Random String strategy and non-string ID") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala new file mode 100644 index 0000000..b2b64cf --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.{BeforeAndAfterEach, Suite} +import solutions.bitbadger.documents.support.ForceDialect + +trait ClearConfiguration extends BeforeAndAfterEach { this: Suite => + + override def afterEach(): Unit = + try super.afterEach () + finally ForceDialect.none() +} 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 ae08e39..c42e21f 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 @@ -1,34 +1,35 @@ package solutions.bitbadger.documents.scala import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import solutions.bitbadger.documents.{AutoId, Configuration, Dialect, DocumentException} -class ConfigurationSpec extends AnyFunSpec { +class ConfigurationSpec extends AnyFunSpec with Matchers { describe("idField") { it("defaults to `id`") { - assert("id" == Configuration.idField) + Configuration.idField shouldEqual "id" } } describe("autoIdStrategy") { it("defaults to `DISABLED`") { - assert(AutoId.DISABLED == Configuration.autoIdStrategy) + Configuration.autoIdStrategy shouldEqual AutoId.DISABLED } } describe("idStringLength") { it("defaults to 16") { - assert(16 == Configuration.idStringLength) + Configuration.idStringLength shouldEqual 16 } } describe("dialect") { it("is derived from connection string") { try { - assertThrows[DocumentException] { Configuration.dialect() } + an [DocumentException] should be thrownBy Configuration.dialect() Configuration.setConnectionString("jdbc:postgresql:db") - assert(Dialect.POSTGRESQL == Configuration.dialect()) + Configuration.dialect() shouldEqual Dialect.POSTGRESQL } finally { Configuration.setConnectionString(null) } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala index 1e56812..7321fe0 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala @@ -1,16 +1,17 @@ package solutions.bitbadger.documents.scala import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import solutions.bitbadger.documents.{Dialect, DocumentException} -class DialectSpec extends AnyFunSpec { +class DialectSpec extends AnyFunSpec with Matchers { describe("deriveFromConnectionString") { it("derives PostgreSQL correctly") { - assert(Dialect.POSTGRESQL == Dialect.deriveFromConnectionString("jdbc:postgresql:db")) + Dialect.deriveFromConnectionString("jdbc:postgresql:db") shouldEqual Dialect.POSTGRESQL } it("derives SQLite correctly") { - assert(Dialect.SQLITE == Dialect.deriveFromConnectionString("jdbc:sqlite:memory")) + Dialect.deriveFromConnectionString("jdbc:sqlite:memory") shouldEqual Dialect.SQLITE } it("fails when the connection string is unknown") { try { @@ -18,8 +19,8 @@ class DialectSpec extends AnyFunSpec { fail("Dialect derivation should have failed") } catch { case ex: DocumentException => - assert(ex.getMessage != null) - assert(ex.getMessage.contains("[SQL Server]")) + ex.getMessage should not be null + ex.getMessage should include ("[SQL Server]") } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala new file mode 100644 index 0000000..3e21f8e --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala @@ -0,0 +1,17 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.DocumentIndex + +class DocumentIndexSpec extends AnyFunSpec with Matchers { + + describe("sql") { + it("returns blank for FULL") { + DocumentIndex.FULL.getSql shouldEqual "" + } + it("returns jsonb_path_ops for OPTIMIZED") { + DocumentIndex.OPTIMIZED.getSql shouldEqual " jsonb_path_ops" + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala index 25d5625..07d0843 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala @@ -1,23 +1,20 @@ package solutions.bitbadger.documents.scala import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import solutions.bitbadger.documents.FieldMatch /** * Unit tests for the `FieldMatch` enum */ -class FieldMatchSpec extends AnyFunSpec { +class FieldMatchSpec extends AnyFunSpec with Matchers { describe("sql") { - describe("ANY") { - it("should use OR") { - assert("OR" == FieldMatch.ANY.getSql) - } + it("returns OR for ANY") { + FieldMatch.ANY.getSql shouldEqual "OR" } - describe("ALL") { - it("should use AND") { - assert("AND" == FieldMatch.ALL.getSql) - } + it("returns AND for ALL") { + FieldMatch.ALL.getSql shouldEqual "AND" } } } 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 new file mode 100644 index 0000000..a1123d1 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala @@ -0,0 +1,428 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.{Dialect, DocumentException, Field, FieldFormat, Op} + +import scala.jdk.CollectionConverters.* + +class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { + + // ~~~ INSTANCE METHODS ~~~ + + describe("withParameterName") { + it("fails for invalid name") { + an [DocumentException] should be thrownBy Field.equal("it", "").withParameterName("2424") + } + it("works with colon prefix") { + val field = Field.equal("abc", "22").withQualifier("me") + val withParam = field.withParameterName(":test") + withParam should not be theSameInstanceAs (field) + withParam.getName shouldEqual field.getName + withParam.getComparison shouldEqual field.getComparison + withParam.getParameterName shouldEqual ":test" + withParam.getQualifier shouldEqual field.getQualifier + } + it("works with at-sign prefix") { + val field = Field.equal("def", "44") + val withParam = field.withParameterName("@unit") + withParam should not be theSameInstanceAs (field) + withParam.getName shouldEqual field.getName + withParam.getComparison shouldEqual field.getComparison + withParam.getParameterName shouldEqual "@unit" + withParam.getQualifier shouldEqual field.getQualifier + } + } + + describe("withQualifier") { + it("sets qualifier correctly") { + val field = Field.equal("j", "k") + val withQual = field.withQualifier("test") + withQual should not be theSameInstanceAs (field) + withQual.getName shouldEqual field.getName + withQual.getComparison shouldEqual field.getComparison + withQual.getParameterName shouldEqual field.getParameterName + withQual.getQualifier shouldEqual "test" + } + } + + describe("path") { + it("generates simple unqualified PostgreSQL field") { + 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'" + } + it("generates nested unqualified PostgreSQL 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}'" + } + it("generates simple unqualified SQLite field") { + 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'" + } + it("generates nested unqualified SQLite 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'" + } + } + + describe("toWhere") { + it("generates exists w/o qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.exists("that_field").toWhere shouldEqual "data->>'that_field' IS NOT NULL" + } + it("generates exists w/o qualifier | SQLite") { + ForceDialect.sqlite() + Field.exists("that_field").toWhere shouldEqual "data->>'that_field' IS NOT NULL" + } + it("generates not-exists w/o qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.notExists("a_field").toWhere shouldEqual "data->>'a_field' IS NULL" + } + it("generates not-exists w/o qualifier | SQLite") { + ForceDialect.sqlite() + Field.notExists("a_field").toWhere shouldEqual "data->>'a_field' IS NULL" + } + it("generates BETWEEN w/o qualifier, numeric range | PostgreSQL") { + ForceDialect.postgres() + Field.between("age", 13, 17, "@age").toWhere shouldEqual "(data->>'age')::numeric BETWEEN @agemin AND @agemax" + } + 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" + } + it("generates BETWEEN w/o qualifier | SQLite") { + ForceDialect.sqlite() + Field.between("age", 13, 17, "@age").toWhere shouldEqual "data->>'age' BETWEEN @agemin AND @agemax" + } + 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" + } + 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" + } + 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" + } + 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)" + } + 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)" + } + it("generates IN/any | SQLite") { + ForceDialect.sqlite() + 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]" + } + 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))" + } + it("generates others w/o qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.equal("some_field", "", ":value").toWhere shouldEqual "data->>'some_field' = :value" + } + it("generates others w/o qualifier | SQLite") { + ForceDialect.sqlite() + Field.equal("some_field", "", ":value").toWhere shouldEqual "data->>'some_field' = :value" + } + it("generates no-parameter w/ qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.exists("no_field").withQualifier("test").toWhere shouldEqual "test.data->>'no_field' IS NOT NULL" + } + it("generates no-parameter w/ qualifier | SQLite") { + ForceDialect.sqlite() + Field.exists("no_field").withQualifier("test").toWhere shouldEqual "test.data->>'no_field' IS NOT NULL" + } + it("generates parameter w/ qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q") + .toWhere shouldEqual "(q.data->>'le_field')::numeric <= :it" + } + it("generates parameter w/ qualifier | SQLite") { + ForceDialect.sqlite() + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere shouldEqual "q.data->>'le_field' <= :it" + } + } + + // ~~~ STATIC CONSTRUCTOR TESTS ~~~ + + describe("equal") { + it("constructs a field w/o parameter name") { + val field = Field.equal("Test", 14) + field.getName shouldEqual "Test" + field.getComparison.getOp shouldEqual Op.EQUAL + field.getComparison.getValue shouldEqual 14 + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.equal("Test", 14, ":w") + field.getName shouldEqual "Test" + field.getComparison.getOp shouldEqual Op.EQUAL + field.getComparison.getValue shouldEqual 14 + field.getParameterName shouldEqual ":w" + field.getQualifier should be (null) + } + } + + describe("greater") { + it("constructs a field w/o parameter name") { + val field = Field.greater("Great", "night") + field.getName shouldEqual "Great" + field.getComparison.getOp shouldEqual Op.GREATER + field.getComparison.getValue shouldEqual "night" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.greater("Great", "night", ":yeah") + field.getName shouldEqual "Great" + field.getComparison.getOp shouldEqual Op.GREATER + field.getComparison.getValue shouldEqual "night" + field.getParameterName shouldEqual ":yeah" + field.getQualifier should be (null) + } + } + + describe("greaterOrEqual") { + it("constructs a field w/o parameter name") { + val field = Field.greaterOrEqual("Nice", 88L) + field.getName shouldEqual "Nice" + field.getComparison.getOp shouldEqual Op.GREATER_OR_EQUAL + field.getComparison.getValue shouldEqual 88L + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.greaterOrEqual("Nice", 88L, ":nice") + field.getName shouldEqual "Nice" + field.getComparison.getOp shouldEqual Op.GREATER_OR_EQUAL + field.getComparison.getValue shouldEqual 88L + field.getParameterName shouldEqual ":nice" + field.getQualifier should be (null) + } + } + + describe("less") { + it("constructs a field w/o parameter name") { + val field = Field.less("Lesser", "seven") + field.getName shouldEqual "Lesser" + field.getComparison.getOp shouldEqual Op.LESS + field.getComparison.getValue shouldEqual "seven" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.less("Lesser", "seven", ":max") + field.getName shouldEqual "Lesser" + field.getComparison.getOp shouldEqual Op.LESS + field.getComparison.getValue shouldEqual "seven" + field.getParameterName shouldEqual ":max" + field.getQualifier should be (null) + } + } + + describe("lessOrEqual") { + it("constructs a field w/o parameter name") { + val field = Field.lessOrEqual("Nobody", "KNOWS") + field.getName shouldEqual "Nobody" + field.getComparison.getOp shouldEqual Op.LESS_OR_EQUAL + field.getComparison.getValue shouldEqual "KNOWS" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.lessOrEqual("Nobody", "KNOWS", ":nope") + field.getName shouldEqual "Nobody" + field.getComparison.getOp shouldEqual Op.LESS_OR_EQUAL + field.getComparison.getValue shouldEqual "KNOWS" + field.getParameterName shouldEqual ":nope" + field.getQualifier should be (null) + } + } + + describe("notEqual") { + it("constructs a field w/o parameter name") { + val field = Field.notEqual("Park", "here") + field.getName shouldEqual "Park" + field.getComparison.getOp shouldEqual Op.NOT_EQUAL + field.getComparison.getValue shouldEqual "here" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.notEqual("Park", "here", ":now") + field.getName shouldEqual "Park" + field.getComparison.getOp shouldEqual Op.NOT_EQUAL + field.getComparison.getValue shouldEqual "here" + field.getParameterName shouldEqual ":now" + field.getQualifier should be (null) + } + } + + describe("between") { + it("constructs a field w/o parameter name") { + val field = Field.between("Age", 18, 49) + field.getName shouldEqual "Age" + field.getComparison.getOp shouldEqual Op.BETWEEN + field.getComparison.getValue.getFirst shouldEqual 18 + field.getComparison.getValue.getSecond shouldEqual 49 + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.between("Age", 18, 49, ":limit") + field.getName shouldEqual "Age" + field.getComparison.getOp shouldEqual Op.BETWEEN + field.getComparison.getValue.getFirst shouldEqual 18 + field.getComparison.getValue.getSecond shouldEqual 49 + field.getParameterName shouldEqual ":limit" + field.getQualifier should be (null) + } + } + + describe("any") { + it("constructs a field w/o parameter name") { + val field = Field.any("Here", List(8, 16, 32).asJava) + field.getName shouldEqual "Here" + field.getComparison.getOp shouldEqual Op.IN + field.getComparison.getValue should be (List(8, 16, 32).asJava) + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.any("Here", List(8, 16, 32).asJava, ":list") + field.getName shouldEqual "Here" + field.getComparison.getOp shouldEqual Op.IN + field.getComparison.getValue should be (List(8, 16, 32).asJava) + field.getParameterName shouldEqual ":list" + field.getQualifier should be (null) + } + } + + describe("inArray") { + it("constructs a field w/o parameter name") { + val field = Field.inArray("ArrayField", "table", List("z").asJava) + field.getName shouldEqual "ArrayField" + field.getComparison.getOp shouldEqual Op.IN_ARRAY + field.getComparison.getValue.getFirst shouldEqual "table" + field.getComparison.getValue.getSecond should be (List("z").asJava) + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.inArray("ArrayField", "table", List("z").asJava, ":a") + field.getName shouldEqual "ArrayField" + field.getComparison.getOp shouldEqual Op.IN_ARRAY + field.getComparison.getValue.getFirst shouldEqual "table" + field.getComparison.getValue.getSecond should be (List("z").asJava) + field.getParameterName shouldEqual ":a" + field.getQualifier should be (null) + } + } + + describe("exists") { + it("constructs a field") { + val field = Field.exists("Groovy") + field.getName shouldEqual "Groovy" + field.getComparison.getOp shouldEqual Op.EXISTS + field.getComparison.getValue shouldEqual "" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + } + + describe("notExists") { + it("constructs a field") { + val field = Field.notExists("Groovy") + field.getName shouldEqual "Groovy" + field.getComparison.getOp shouldEqual Op.NOT_EXISTS + field.getComparison.getValue shouldEqual "" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + } + + describe("named") { + it("named constructs a field") { + val field = Field.named("Tacos") + field.getName shouldEqual "Tacos" + field.getComparison.getOp shouldEqual Op.EQUAL + field.getComparison.getValue shouldEqual "" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + } + + describe("static constructors") { + it("fail for invalid parameter name") { + an [DocumentException] should be thrownBy Field.equal("a", "b", "that ain't it, Jack...") + } + } + + describe("nameToPath") { + it("creates a simple PostgreSQL SQL name") { + Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data->>'Simple'" + } + it("creates a simple SQLite SQL name") { + 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}'") + } + 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'") + } + it("creates a simple PostgreSQL JSON name") { + Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON) shouldEqual "data->'Simple'" + } + it("creates a simple SQLite JSON name") { + 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}'") + } + 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'") + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala new file mode 100644 index 0000000..aea074f --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala @@ -0,0 +1,44 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.Op + +class OpSpec extends AnyFunSpec with Matchers { + + describe("sql") { + it("returns = for EQUAL") { + Op.EQUAL.getSql shouldEqual "=" + } + it("returns > for GREATER") { + Op.GREATER.getSql shouldEqual ">" + } + it("returns >= for GREATER_OR_EQUAL") { + Op.GREATER_OR_EQUAL.getSql shouldEqual ">=" + } + it("returns < for LESS") { + Op.LESS.getSql shouldEqual "<" + } + it("returns <= for LESS_OR_EQUAL") { + Op.LESS_OR_EQUAL.getSql shouldEqual "<=" + } + it("returns <> for NOT_EQUAL") { + Op.NOT_EQUAL.getSql shouldEqual "<>" + } + it("returns BETWEEN for BETWEEN") { + Op.BETWEEN.getSql shouldEqual "BETWEEN" + } + it("returns IN for IN") { + Op.IN.getSql shouldEqual "IN" + } + it("returns ??| for IN_ARRAY") { + Op.IN_ARRAY.getSql shouldEqual "??|" + } + it("returns IS NOT NULL for EXISTS") { + Op.EXISTS.getSql shouldEqual "IS NOT NULL" + } + it("returns IS NULL for NOT_EXISTS") { + Op.NOT_EXISTS.getSql shouldEqual "IS NULL" + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala new file mode 100644 index 0000000..35188e8 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala @@ -0,0 +1,23 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.ParameterName + +class ParameterNameSpec extends AnyFunSpec with Matchers { + + describe("derive") { + it("works when given existing names") { + val names = new ParameterName() + names.derive(":taco") shouldEqual ":taco" + names.derive(null) shouldEqual ":field0" + } + it("works when given all anonymous fields") { + val names = new ParameterName() + names.derive(null) shouldEqual ":field0" + names.derive(null) shouldEqual ":field1" + names.derive(null) shouldEqual ":field2" + names.derive(null) shouldEqual ":field3" + } + } +} 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 new file mode 100644 index 0000000..dbda937 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Parameter, ParameterType} + +class ParameterSpec extends AnyFunSpec with Matchers { + + describe("constructor") { + it("succeeds with colon-prefixed name") { + val p = new Parameter(":test", ParameterType.STRING, "ABC") + p.getName shouldEqual ":test" + p.getType shouldEqual ParameterType.STRING + p.getValue shouldEqual "ABC" + } + it("succeeds with at-sign-prefixed name") { + val p = Parameter("@yo", ParameterType.NUMBER, null) + p.getName shouldEqual "@yo" + p.getType shouldEqual ParameterType.NUMBER + p.getValue should be (null) + } + it("fails with incorrect prefix") { + an [DocumentException] should be thrownBy Parameter("it", ParameterType.JSON, "") + } + } +}