From d202c002b57f37d841bb51a0d5b2e84b8ead6100 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 16 Apr 2025 01:29:19 +0000 Subject: [PATCH] Initial Development (#1) This project now contains: - A generic JVM document library (with Kotlin extensions on the JDBC `Connection` object) - A Groovy library which adds extension methods to the `Connection` object - A Scala library, which uses native Scala collections and adds Scala-style extension methods to the `Connection` object - A Kotlin library which uses `kotlinx.serialization` (no reflection, reified generic types) along with `Connection` extensions Reviewed-on: https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents/pulls/1 --- .gitignore | 6 + .idea/.gitignore | 8 + .idea/codeStyles/Project.xml | 7 + .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/compiler.xml | 25 + .idea/encodings.xml | 33 + .idea/jarRepositories.xml | 25 + .idea/kotlinc.xml | 16 + .idea/libraries/KotlinJavaRuntime.xml | 23 + .idea/libraries/Maven__scala_sdk_3_0_0.xml | 26 + .idea/libraries/Maven__scala_sdk_3_1_3.xml | 26 + .idea/libraries/Maven__scala_sdk_3_3_3.xml | 25 + .idea/libraries/Maven__scala_sdk_3_5_2.xml | 26 + .idea/misc.xml | 23 + .idea/scala_compiler.xml | 7 + .idea/scala_settings.xml | 6 + .idea/vcs.xml | 6 + README.md | 52 +- pom.xml | 117 +++ solutions.bitbadger.documents.iml | 8 + src/core/core.iml | 8 + src/core/pom.xml | 182 ++++ src/core/src/main/java/module-info.java | 9 + src/core/src/main/kotlin/AutoId.kt | 85 ++ src/core/src/main/kotlin/Comparison.kt | 68 ++ src/core/src/main/kotlin/Configuration.kt | 67 ++ src/core/src/main/kotlin/Dialect.kt | 33 + src/core/src/main/kotlin/DocumentException.kt | 9 + src/core/src/main/kotlin/DocumentIndex.kt | 13 + .../src/main/kotlin/DocumentSerializer.kt | 24 + src/core/src/main/kotlin/Field.kt | 320 +++++++ src/core/src/main/kotlin/FieldFormat.kt | 12 + src/core/src/main/kotlin/FieldMatch.kt | 12 + src/core/src/main/kotlin/Op.kt | 39 + src/core/src/main/kotlin/Parameter.kt | 58 ++ src/core/src/main/kotlin/ParameterName.kt | 18 + src/core/src/main/kotlin/ParameterType.kt | 15 + src/core/src/main/kotlin/java/Count.kt | 147 +++ src/core/src/main/kotlin/java/Custom.kt | 281 ++++++ src/core/src/main/kotlin/java/Definition.kt | 92 ++ src/core/src/main/kotlin/java/Delete.kt | 122 +++ src/core/src/main/kotlin/java/Document.kt | 109 +++ .../src/main/kotlin/java/DocumentConfig.kt | 15 + src/core/src/main/kotlin/java/Exists.kt | 155 +++ src/core/src/main/kotlin/java/Find.kt | 502 ++++++++++ src/core/src/main/kotlin/java/Json.kt | 877 +++++++++++++++++ .../kotlin/java/NullDocumentSerializer.kt | 21 + src/core/src/main/kotlin/java/Parameters.kt | 131 +++ src/core/src/main/kotlin/java/Patch.kt | 155 +++ src/core/src/main/kotlin/java/RemoveFields.kt | 178 ++++ src/core/src/main/kotlin/java/Results.kt | 167 ++++ .../main/kotlin/java/extensions/Connection.kt | 885 ++++++++++++++++++ src/core/src/main/kotlin/query/CountQuery.kt | 62 ++ .../src/main/kotlin/query/DefinitionQuery.kt | 108 +++ src/core/src/main/kotlin/query/DeleteQuery.kt | 76 ++ .../src/main/kotlin/query/DocumentQuery.kt | 68 ++ src/core/src/main/kotlin/query/ExistsQuery.kt | 75 ++ src/core/src/main/kotlin/query/FindQuery.kt | 77 ++ src/core/src/main/kotlin/query/PatchQuery.kt | 77 ++ src/core/src/main/kotlin/query/Query.kt | 82 ++ .../main/kotlin/query/RemoveFieldsQuery.kt | 89 ++ src/core/src/main/kotlin/query/Where.kt | 74 ++ src/core/src/main/module-info.md | 19 + src/core/src/test/java/module-info.java | 20 + .../documents/core/tests/java/AutoIdTest.java | 216 +++++ .../core/tests/java/ByteIdClass.java | 18 + .../core/tests/java/ConfigurationTest.java | 48 + .../core/tests/java/CountQueryTest.java | 87 ++ .../core/tests/java/DefinitionQueryTest.java | 133 +++ .../core/tests/java/DeleteQueryTest.java | 94 ++ .../core/tests/java/DialectTest.java | 43 + .../core/tests/java/DocumentIndexTest.java | 26 + .../core/tests/java/DocumentQueryTest.java | 134 +++ .../core/tests/java/ExistsQueryTest.java | 97 ++ .../core/tests/java/FieldMatchTest.java | 26 + .../documents/core/tests/java/FieldTest.java | 636 +++++++++++++ .../core/tests/java/FindQueryTest.java | 102 ++ .../documents/core/tests/java/IntIdClass.java | 18 + .../core/tests/java/LongIdClass.java | 18 + .../documents/core/tests/java/OpTest.java | 80 ++ .../core/tests/java/ParameterNameTest.java | 32 + .../core/tests/java/ParameterTest.java | 40 + .../core/tests/java/ParametersTest.java | 121 +++ .../core/tests/java/PatchQueryTest.java | 97 ++ .../core/tests/java/QueryUtilsTest.java | 166 ++++ .../tests/java/RemoveFieldsQueryTest.java | 110 +++ .../core/tests/java/ShortIdClass.java | 18 + .../core/tests/java/StringIdClass.java | 18 + .../documents/core/tests/java/WhereTest.java | 172 ++++ .../tests/java/integration/ArrayDocument.java | 41 + .../java/integration/CountFunctions.java | 62 ++ .../java/integration/CustomFunctions.java | 146 +++ .../java/integration/DefinitionFunctions.java | 47 + .../java/integration/DeleteFunctions.java | 71 ++ .../java/integration/DocumentFunctions.java | 138 +++ .../java/integration/ExistsFunctions.java | 62 ++ .../tests/java/integration/FindFunctions.java | 279 ++++++ .../tests/java/integration/JsonDocument.java | 107 +++ .../tests/java/integration/JsonFunctions.java | 761 +++++++++++++++ .../tests/java/integration/NumIdDocument.java | 32 + .../java/integration/PatchFunctions.java | 85 ++ .../java/integration/PostgreSQLCountIT.java | 69 ++ .../java/integration/PostgreSQLCustomIT.java | 133 +++ .../integration/PostgreSQLDefinitionIT.java | 45 + .../java/integration/PostgreSQLDeleteIT.java | 77 ++ .../integration/PostgreSQLDocumentIT.java | 85 ++ .../java/integration/PostgreSQLExistsIT.java | 77 ++ .../java/integration/PostgreSQLFindIT.java | 269 ++++++ .../java/integration/PostgreSQLJsonIT.java | 477 ++++++++++ .../java/integration/PostgreSQLPatchIT.java | 77 ++ .../integration/PostgreSQLRemoveFieldsIT.java | 109 +++ .../integration/RemoveFieldsFunctions.java | 115 +++ .../tests/java/integration/SQLiteCountIT.java | 55 ++ .../java/integration/SQLiteCustomIT.java | 133 +++ .../java/integration/SQLiteDefinitionIT.java | 47 + .../java/integration/SQLiteDeleteIT.java | 63 ++ .../java/integration/SQLiteDocumentIT.java | 84 ++ .../java/integration/SQLiteExistsIT.java | 63 ++ .../tests/java/integration/SQLiteFindIT.java | 191 ++++ .../tests/java/integration/SQLiteJsonIT.java | 319 +++++++ .../tests/java/integration/SQLitePatchIT.java | 63 ++ .../integration/SQLiteRemoveFieldsIT.java | 79 ++ .../tests/java/integration/SubDocument.java | 32 + src/core/src/test/kotlin/AutoIdTest.kt | 167 ++++ src/core/src/test/kotlin/ComparisonTest.kt | 169 ++++ src/core/src/test/kotlin/ConfigurationTest.kt | 47 + src/core/src/test/kotlin/CountQueryTest.kt | 90 ++ .../src/test/kotlin/DefinitionQueryTest.kt | 137 +++ src/core/src/test/kotlin/DeleteQueryTest.kt | 103 ++ src/core/src/test/kotlin/DialectTest.kt | 44 + src/core/src/test/kotlin/DocumentIndexTest.kt | 25 + src/core/src/test/kotlin/DocumentQueryTest.kt | 152 +++ src/core/src/test/kotlin/ExistsQueryTest.kt | 102 ++ src/core/src/test/kotlin/FieldMatchTest.kt | 25 + src/core/src/test/kotlin/FieldTest.kt | 595 ++++++++++++ src/core/src/test/kotlin/FindQueryTest.kt | 107 +++ src/core/src/test/kotlin/ForceDialect.kt | 24 + src/core/src/test/kotlin/OpTest.kt | 79 ++ src/core/src/test/kotlin/ParameterNameTest.kt | 31 + src/core/src/test/kotlin/ParameterTest.kt | 41 + src/core/src/test/kotlin/ParametersTest.kt | 121 +++ src/core/src/test/kotlin/PatchQueryTest.kt | 101 ++ src/core/src/test/kotlin/QueryTest.kt | 178 ++++ .../src/test/kotlin/RemoveFieldsQueryTest.kt | 118 +++ src/core/src/test/kotlin/Types.kt | 63 ++ src/core/src/test/kotlin/WhereTest.kt | 177 ++++ .../test/kotlin/integration/CountFunctions.kt | 72 ++ .../kotlin/integration/CustomFunctions.kt | 177 ++++ .../kotlin/integration/DefinitionFunctions.kt | 45 + .../kotlin/integration/DeleteFunctions.kt | 68 ++ .../kotlin/integration/DocumentFunctions.kt | 132 +++ .../kotlin/integration/ExistsFunctions.kt | 65 ++ .../test/kotlin/integration/FindFunctions.kt | 334 +++++++ .../integration/JacksonDocumentSerializer.kt | 18 + .../test/kotlin/integration/JsonFunctions.kt | 715 ++++++++++++++ .../test/kotlin/integration/PatchFunctions.kt | 88 ++ src/core/src/test/kotlin/integration/PgDB.kt | 47 + .../kotlin/integration/PostgreSQLCountIT.kt | 46 + .../kotlin/integration/PostgreSQLCustomIT.kt | 87 ++ .../integration/PostgreSQLDefinitionIT.kt | 31 + .../kotlin/integration/PostgreSQLDeleteIT.kt | 51 + .../integration/PostgreSQLDocumentIT.kt | 56 ++ .../kotlin/integration/PostgreSQLExistsIT.kt | 51 + .../kotlin/integration/PostgreSQLFindIT.kt | 171 ++++ .../kotlin/integration/PostgreSQLJsonIT.kt | 301 ++++++ .../kotlin/integration/PostgreSQLPatchIT.kt | 51 + .../integration/PostgreSQLRemoveFieldsIT.kt | 71 ++ .../integration/RemoveFieldsFunctions.kt | 107 +++ .../test/kotlin/integration/SQLiteCountIT.kt | 40 + .../test/kotlin/integration/SQLiteCustomIT.kt | 86 ++ .../src/test/kotlin/integration/SQLiteDB.kt | 32 + .../kotlin/integration/SQLiteDefinitionIT.kt | 35 + .../test/kotlin/integration/SQLiteDeleteIT.kt | 45 + .../kotlin/integration/SQLiteDocumentIT.kt | 56 ++ .../test/kotlin/integration/SQLiteExistsIT.kt | 45 + .../test/kotlin/integration/SQLiteFindIT.kt | 127 +++ .../test/kotlin/integration/SQLiteJsonIT.kt | 211 +++++ .../test/kotlin/integration/SQLitePatchIT.kt | 45 + .../integration/SQLiteRemoveFieldsIT.kt | 55 ++ .../kotlin/integration/ThrowawayDatabase.kt | 29 + src/groovy/groovy.iml | 8 + src/groovy/pom.xml | 185 ++++ src/groovy/src/main/groovy/.gitkeep | 0 src/groovy/src/main/java/module-info.java | 7 + .../documents/groovy/NoClassesHere.java | 7 + .../documents/groovy/package-info.java | 11 + ...rg.codehaus.groovy.runtime.ExtensionModule | 3 + .../documents/groovy/tests/AutoIdTest.groovy | 163 ++++ .../documents/groovy/tests/ByteIdClass.groovy | 9 + .../groovy/tests/ConfigurationTest.groovy | 47 + .../groovy/tests/CountQueryTest.groovy | 81 ++ .../groovy/tests/DefinitionQueryTest.groovy | 127 +++ .../groovy/tests/DeleteQueryTest.groovy | 90 ++ .../documents/groovy/tests/DialectTest.groovy | 42 + .../groovy/tests/DocumentIndexTest.groovy | 26 + .../groovy/tests/DocumentQueryTest.groovy | 135 +++ .../groovy/tests/FieldMatchTest.groovy | 26 + .../documents/groovy/tests/FieldTest.groovy | 612 ++++++++++++ .../groovy/tests/FindQueryTest.groovy | 97 ++ .../groovy/tests/ForceDialect.groovy | 19 + .../documents/groovy/tests/IntIdClass.groovy | 9 + .../documents/groovy/tests/LongIdClass.groovy | 9 + .../documents/groovy/tests/OpTest.groovy | 80 ++ .../groovy/tests/ParameterNameTest.groovy | 32 + .../groovy/tests/ParameterTest.groovy | 40 + .../groovy/tests/ParametersTest.groovy | 119 +++ .../groovy/tests/PatchQueryTest.groovy | 91 ++ .../groovy/tests/QueryUtilsTest.groovy | 162 ++++ .../groovy/tests/RemoveFieldsQueryTest.groovy | 106 +++ .../groovy/tests/ShortIdClass.groovy | 9 + .../groovy/tests/StringIdClass.groovy | 9 + .../documents/groovy/tests/Types.groovy | 6 + .../documents/groovy/tests/WhereTest.groovy | 172 ++++ .../tests/integration/ArrayDocument.groovy | 18 + .../tests/integration/CountFunctions.groovy | 50 + .../tests/integration/CustomFunctions.groovy | 132 +++ .../integration/DefinitionFunctions.groovy | 41 + .../tests/integration/DeleteFunctions.groovy | 65 ++ .../integration/DocumentFunctions.groovy | 129 +++ .../tests/integration/ExistsFunctions.groovy | 55 ++ .../tests/integration/FindFunctions.groovy | 249 +++++ .../JacksonDocumentSerializer.groovy | 22 + .../tests/integration/JsonDocument.groovy | 43 + .../tests/integration/JsonFunctions.groovy | 738 +++++++++++++++ .../tests/integration/NumIdDocument.groovy | 11 + .../tests/integration/PatchFunctions.groovy | 75 ++ .../groovy/tests/integration/PgDB.groovy | 50 + .../integration/PostgreSQLCountIT.groovy | 53 ++ .../integration/PostgreSQLCustomIT.groovy | 101 ++ .../integration/PostgreSQLDefinitionIT.groovy | 35 + .../integration/PostgreSQLDeleteIT.groovy | 59 ++ .../integration/PostgreSQLDocumentIT.groovy | 65 ++ .../integration/PostgreSQLExistsIT.groovy | 59 ++ .../tests/integration/PostgreSQLFindIT.groovy | 203 ++++ .../tests/integration/PostgreSQLJsonIT.groovy | 359 +++++++ .../integration/PostgreSQLPatchIT.groovy | 59 ++ .../PostgreSQLRemoveFieldsIT.groovy | 83 ++ .../integration/RemoveFieldsFunctions.groovy | 104 ++ .../tests/integration/SQLiteCountIT.groovy | 48 + .../tests/integration/SQLiteCustomIT.groovy | 89 ++ .../groovy/tests/integration/SQLiteDB.groovy | 35 + .../integration/SQLiteDefinitionIT.groovy | 42 + .../tests/integration/SQLiteDeleteIT.groovy | 54 ++ .../tests/integration/SQLiteDocumentIT.groovy | 65 ++ .../tests/integration/SQLiteExistsIT.groovy | 54 ++ .../tests/integration/SQLiteFindIT.groovy | 154 +++ .../tests/integration/SQLiteJsonIT.groovy | 258 +++++ .../tests/integration/SQLitePatchIT.groovy | 54 ++ .../integration/SQLiteRemoveFieldsIT.groovy | 66 ++ .../tests/integration/SubDocument.groovy | 11 + .../integration/ThrowawayDatabase.groovy | 24 + src/groovy/src/test/java/module-info.java | 15 + src/kotlinx/pom.xml | 186 ++++ src/kotlinx/src/main/java/module-info.java | 10 + src/kotlinx/src/main/kotlin/Count.kt | 120 +++ src/kotlinx/src/main/kotlin/Custom.kt | 218 +++++ src/kotlinx/src/main/kotlin/Definition.kt | 71 ++ src/kotlinx/src/main/kotlin/Delete.kt | 95 ++ src/kotlinx/src/main/kotlin/Document.kt | 114 +++ src/kotlinx/src/main/kotlin/DocumentConfig.kt | 33 + src/kotlinx/src/main/kotlin/Exists.kt | 107 +++ src/kotlinx/src/main/kotlin/Find.kt | 417 +++++++++ src/kotlinx/src/main/kotlin/Json.kt | 731 +++++++++++++++ src/kotlinx/src/main/kotlin/Parameters.kt | 77 ++ src/kotlinx/src/main/kotlin/Patch.kt | 137 +++ src/kotlinx/src/main/kotlin/RemoveFields.kt | 124 +++ src/kotlinx/src/main/kotlin/Results.kt | 107 +++ .../src/main/kotlin/extensions/Connection.kt | 750 +++++++++++++++ src/kotlinx/src/main/module-info.md | 11 + src/kotlinx/src/test/java/module-info.java | 14 + .../src/test/kotlin/DocumentConfigTest.kt | 22 + src/kotlinx/src/test/kotlin/Types.kt | 68 ++ .../test/kotlin/integration/CountFunctions.kt | 72 ++ .../kotlin/integration/CustomFunctions.kt | 162 ++++ .../kotlin/integration/DefinitionFunctions.kt | 45 + .../kotlin/integration/DeleteFunctions.kt | 69 ++ .../kotlin/integration/DocumentFunctions.kt | 130 +++ .../kotlin/integration/ExistsFunctions.kt | 66 ++ .../test/kotlin/integration/FindFunctions.kt | 300 ++++++ .../test/kotlin/integration/JsonFunctions.kt | 719 ++++++++++++++ .../test/kotlin/integration/PatchFunctions.kt | 90 ++ .../src/test/kotlin/integration/PgDB.kt | 47 + .../kotlin/integration/PostgreSQLCountIT.kt | 46 + .../kotlin/integration/PostgreSQLCustomIT.kt | 87 ++ .../integration/PostgreSQLDefinitionIT.kt | 31 + .../kotlin/integration/PostgreSQLDeleteIT.kt | 51 + .../integration/PostgreSQLDocumentIT.kt | 56 ++ .../kotlin/integration/PostgreSQLExistsIT.kt | 51 + .../kotlin/integration/PostgreSQLFindIT.kt | 171 ++++ .../kotlin/integration/PostgreSQLJsonIT.kt | 301 ++++++ .../kotlin/integration/PostgreSQLPatchIT.kt | 51 + .../integration/PostgreSQLRemoveFieldsIT.kt | 71 ++ .../integration/RemoveFieldsFunctions.kt | 108 +++ .../test/kotlin/integration/SQLiteCountIT.kt | 40 + .../test/kotlin/integration/SQLiteCustomIT.kt | 86 ++ .../src/test/kotlin/integration/SQLiteDB.kt | 32 + .../kotlin/integration/SQLiteDefinitionIT.kt | 35 + .../test/kotlin/integration/SQLiteDeleteIT.kt | 45 + .../kotlin/integration/SQLiteDocumentIT.kt | 56 ++ .../test/kotlin/integration/SQLiteExistsIT.kt | 45 + .../test/kotlin/integration/SQLiteFindIT.kt | 127 +++ .../test/kotlin/integration/SQLiteJsonIT.kt | 211 +++++ .../test/kotlin/integration/SQLitePatchIT.kt | 45 + .../integration/SQLiteRemoveFieldsIT.kt | 55 ++ .../kotlin/integration/ThrowawayDatabase.kt | 24 + src/scala/pom.xml | 167 ++++ src/scala/scala.iml | 9 + src/scala/src/main/scala/Count.scala | 105 +++ src/scala/src/main/scala/Custom.scala | 360 +++++++ src/scala/src/main/scala/Definition.scala | 72 ++ src/scala/src/main/scala/Delete.scala | 98 ++ src/scala/src/main/scala/Document.scala | 72 ++ src/scala/src/main/scala/Exists.scala | 106 +++ src/scala/src/main/scala/Find.scala | 369 ++++++++ src/scala/src/main/scala/Json.scala | 688 ++++++++++++++ src/scala/src/main/scala/Parameters.scala | 86 ++ src/scala/src/main/scala/Patch.scala | 120 +++ src/scala/src/main/scala/RemoveFields.scala | 120 +++ src/scala/src/main/scala/Results.scala | 143 +++ .../src/main/scala/extensions/package.scala | 776 +++++++++++++++ src/scala/src/test/scala/AutoIdTest.scala | 126 +++ src/scala/src/test/scala/ByteIdClass.scala | 3 + .../src/test/scala/ConfigurationTest.scala | 33 + src/scala/src/test/scala/CountQueryTest.scala | 66 ++ .../src/test/scala/DefinitionQueryTest.scala | 104 ++ .../src/test/scala/DeleteQueryTest.scala | 74 ++ src/scala/src/test/scala/DialectTest.scala | 32 + .../src/test/scala/DocumentIndexTest.scala | 18 + .../src/test/scala/DocumentQueryTest.scala | 109 +++ .../src/test/scala/ExistsQueryTest.scala | 74 ++ src/scala/src/test/scala/FieldMatchTest.scala | 21 + src/scala/src/test/scala/FieldTest.scala | 537 +++++++++++ src/scala/src/test/scala/FindQueryTest.scala | 79 ++ src/scala/src/test/scala/ForceDialect.scala | 17 + src/scala/src/test/scala/IntIdClass.scala | 3 + src/scala/src/test/scala/LongIdClass.scala | 3 + src/scala/src/test/scala/OpTest.scala | 63 ++ .../src/test/scala/ParameterNameTest.scala | 24 + src/scala/src/test/scala/ParameterTest.scala | 29 + src/scala/src/test/scala/ParametersTest.scala | 100 ++ src/scala/src/test/scala/PatchQueryTest.scala | 72 ++ src/scala/src/test/scala/QueryUtilsTest.scala | 136 +++ .../test/scala/RemoveFieldsQueryTest.scala | 82 ++ src/scala/src/test/scala/ShortIdClass.scala | 3 + src/scala/src/test/scala/StringIdClass.scala | 3 + src/scala/src/test/scala/WhereTest.scala | 141 +++ .../scala/integration/ArrayDocument.scala | 11 + .../scala/integration/CountFunctions.scala | 42 + .../scala/integration/CustomFunctions.scala | 109 +++ .../integration/DefinitionFunctions.scala | 36 + .../scala/integration/DeleteFunctions.scala | 56 ++ .../scala/integration/DocumentFunctions.scala | 108 +++ .../scala/integration/ExistsFunctions.scala | 46 + .../scala/integration/FindFunctions.scala | 216 +++++ .../JacksonDocumentSerializer.scala | 17 + .../test/scala/integration/JsonDocument.scala | 34 + .../scala/integration/JsonFunctions.scala | 572 +++++++++++ .../scala/integration/NumIdDocument.scala | 3 + .../scala/integration/PatchFunctions.scala | 65 ++ .../src/test/scala/integration/PgDB.scala | 45 + .../scala/integration/PostgreSQLCountIT.scala | 43 + .../integration/PostgreSQLCustomIT.scala | 83 ++ .../integration/PostgreSQLDefinitionIT.scala | 31 + .../integration/PostgreSQLDeleteIT.scala | 51 + .../integration/PostgreSQLDocumentIT.scala | 56 ++ .../integration/PostgreSQLExistsIT.scala | 51 + .../scala/integration/PostgreSQLFindIT.scala | 171 ++++ .../scala/integration/PostgreSQLJsonIT.scala | 301 ++++++ .../scala/integration/PostgreSQLPatchIT.scala | 51 + .../PostgreSQLRemoveFieldsIT.scala | 71 ++ .../integration/RemoveFieldsFunctions.scala | 92 ++ .../scala/integration/SQLiteCountIT.scala | 35 + .../scala/integration/SQLiteCustomIT.scala | 83 ++ .../src/test/scala/integration/SQLiteDB.scala | 30 + .../integration/SQLiteDefinitionIT.scala | 30 + .../scala/integration/SQLiteDeleteIT.scala | 43 + .../scala/integration/SQLiteDocumentIT.scala | 56 ++ .../scala/integration/SQLiteExistsIT.scala | 43 + .../test/scala/integration/SQLiteFindIT.scala | 127 +++ .../test/scala/integration/SQLiteJsonIT.scala | 211 +++++ .../scala/integration/SQLitePatchIT.scala | 44 + .../integration/SQLiteRemoveFieldsIT.scala | 57 ++ .../test/scala/integration/SubDocument.scala | 3 + .../scala/integration/ThrowawayDatabase.scala | 28 + src/scala/src/test/scala/package.scala | 3 + 385 files changed, 41134 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/libraries/KotlinJavaRuntime.xml create mode 100644 .idea/libraries/Maven__scala_sdk_3_0_0.xml create mode 100644 .idea/libraries/Maven__scala_sdk_3_1_3.xml create mode 100644 .idea/libraries/Maven__scala_sdk_3_3_3.xml create mode 100644 .idea/libraries/Maven__scala_sdk_3_5_2.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/scala_compiler.xml create mode 100644 .idea/scala_settings.xml create mode 100644 .idea/vcs.xml create mode 100644 pom.xml create mode 100644 solutions.bitbadger.documents.iml create mode 100644 src/core/core.iml create mode 100644 src/core/pom.xml create mode 100644 src/core/src/main/java/module-info.java create mode 100644 src/core/src/main/kotlin/AutoId.kt create mode 100644 src/core/src/main/kotlin/Comparison.kt create mode 100644 src/core/src/main/kotlin/Configuration.kt create mode 100644 src/core/src/main/kotlin/Dialect.kt create mode 100644 src/core/src/main/kotlin/DocumentException.kt create mode 100644 src/core/src/main/kotlin/DocumentIndex.kt create mode 100644 src/core/src/main/kotlin/DocumentSerializer.kt create mode 100644 src/core/src/main/kotlin/Field.kt create mode 100644 src/core/src/main/kotlin/FieldFormat.kt create mode 100644 src/core/src/main/kotlin/FieldMatch.kt create mode 100644 src/core/src/main/kotlin/Op.kt create mode 100644 src/core/src/main/kotlin/Parameter.kt create mode 100644 src/core/src/main/kotlin/ParameterName.kt create mode 100644 src/core/src/main/kotlin/ParameterType.kt create mode 100644 src/core/src/main/kotlin/java/Count.kt create mode 100644 src/core/src/main/kotlin/java/Custom.kt create mode 100644 src/core/src/main/kotlin/java/Definition.kt create mode 100644 src/core/src/main/kotlin/java/Delete.kt create mode 100644 src/core/src/main/kotlin/java/Document.kt create mode 100644 src/core/src/main/kotlin/java/DocumentConfig.kt create mode 100644 src/core/src/main/kotlin/java/Exists.kt create mode 100644 src/core/src/main/kotlin/java/Find.kt create mode 100644 src/core/src/main/kotlin/java/Json.kt create mode 100644 src/core/src/main/kotlin/java/NullDocumentSerializer.kt create mode 100644 src/core/src/main/kotlin/java/Parameters.kt create mode 100644 src/core/src/main/kotlin/java/Patch.kt create mode 100644 src/core/src/main/kotlin/java/RemoveFields.kt create mode 100644 src/core/src/main/kotlin/java/Results.kt create mode 100644 src/core/src/main/kotlin/java/extensions/Connection.kt create mode 100644 src/core/src/main/kotlin/query/CountQuery.kt create mode 100644 src/core/src/main/kotlin/query/DefinitionQuery.kt create mode 100644 src/core/src/main/kotlin/query/DeleteQuery.kt create mode 100644 src/core/src/main/kotlin/query/DocumentQuery.kt create mode 100644 src/core/src/main/kotlin/query/ExistsQuery.kt create mode 100644 src/core/src/main/kotlin/query/FindQuery.kt create mode 100644 src/core/src/main/kotlin/query/PatchQuery.kt create mode 100644 src/core/src/main/kotlin/query/Query.kt create mode 100644 src/core/src/main/kotlin/query/RemoveFieldsQuery.kt create mode 100644 src/core/src/main/kotlin/query/Where.kt create mode 100644 src/core/src/main/module-info.md create mode 100644 src/core/src/test/java/module-info.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/AutoIdTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ByteIdClass.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ConfigurationTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/CountQueryTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DefinitionQueryTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DeleteQueryTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DialectTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentIndexTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentQueryTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ExistsQueryTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldMatchTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FindQueryTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/IntIdClass.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/LongIdClass.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/OpTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterNameTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParametersTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/PatchQueryTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/QueryUtilsTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/RemoveFieldsQueryTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ShortIdClass.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/StringIdClass.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/WhereTest.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ArrayDocument.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CountFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DefinitionFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DeleteFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DocumentFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ExistsFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/FindFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonDocument.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/NumIdDocument.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PatchFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCountIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDefinitionIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDeleteIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDocumentIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLExistsIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLFindIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLJsonIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLPatchIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLRemoveFieldsIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/RemoveFieldsFunctions.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCountIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDefinitionIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDeleteIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDocumentIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteExistsIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteFindIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteJsonIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLitePatchIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteRemoveFieldsIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SubDocument.java create mode 100644 src/core/src/test/kotlin/AutoIdTest.kt create mode 100644 src/core/src/test/kotlin/ComparisonTest.kt create mode 100644 src/core/src/test/kotlin/ConfigurationTest.kt create mode 100644 src/core/src/test/kotlin/CountQueryTest.kt create mode 100644 src/core/src/test/kotlin/DefinitionQueryTest.kt create mode 100644 src/core/src/test/kotlin/DeleteQueryTest.kt create mode 100644 src/core/src/test/kotlin/DialectTest.kt create mode 100644 src/core/src/test/kotlin/DocumentIndexTest.kt create mode 100644 src/core/src/test/kotlin/DocumentQueryTest.kt create mode 100644 src/core/src/test/kotlin/ExistsQueryTest.kt create mode 100644 src/core/src/test/kotlin/FieldMatchTest.kt create mode 100644 src/core/src/test/kotlin/FieldTest.kt create mode 100644 src/core/src/test/kotlin/FindQueryTest.kt create mode 100644 src/core/src/test/kotlin/ForceDialect.kt create mode 100644 src/core/src/test/kotlin/OpTest.kt create mode 100644 src/core/src/test/kotlin/ParameterNameTest.kt create mode 100644 src/core/src/test/kotlin/ParameterTest.kt create mode 100644 src/core/src/test/kotlin/ParametersTest.kt create mode 100644 src/core/src/test/kotlin/PatchQueryTest.kt create mode 100644 src/core/src/test/kotlin/QueryTest.kt create mode 100644 src/core/src/test/kotlin/RemoveFieldsQueryTest.kt create mode 100644 src/core/src/test/kotlin/Types.kt create mode 100644 src/core/src/test/kotlin/WhereTest.kt create mode 100644 src/core/src/test/kotlin/integration/CountFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/CustomFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/DefinitionFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/DeleteFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/DocumentFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/ExistsFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/FindFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/JacksonDocumentSerializer.kt create mode 100644 src/core/src/test/kotlin/integration/JsonFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/PatchFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/PgDB.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLDeleteIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLDocumentIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLExistsIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLFindIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLPatchIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt create mode 100644 src/core/src/test/kotlin/integration/RemoveFieldsFunctions.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteCountIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteCustomIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteDB.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteDefinitionIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteDeleteIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteDocumentIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteExistsIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteFindIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteJsonIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLitePatchIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt create mode 100644 src/core/src/test/kotlin/integration/ThrowawayDatabase.kt create mode 100644 src/groovy/groovy.iml create mode 100644 src/groovy/pom.xml create mode 100644 src/groovy/src/main/groovy/.gitkeep create mode 100644 src/groovy/src/main/java/module-info.java create mode 100644 src/groovy/src/main/java/solutions/bitbadger/documents/groovy/NoClassesHere.java create mode 100644 src/groovy/src/main/java/solutions/bitbadger/documents/groovy/package-info.java create mode 100644 src/groovy/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/AutoIdTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ByteIdClass.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ConfigurationTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/CountQueryTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DefinitionQueryTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DeleteQueryTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DialectTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentIndexTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentQueryTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldMatchTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FindQueryTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ForceDialect.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/IntIdClass.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/LongIdClass.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/OpTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterNameTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParametersTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/PatchQueryTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/QueryUtilsTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/RemoveFieldsQueryTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ShortIdClass.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/StringIdClass.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/Types.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/WhereTest.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ArrayDocument.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CountFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CustomFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DefinitionFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DeleteFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DocumentFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ExistsFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/FindFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JacksonDocumentSerializer.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/NumIdDocument.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PatchFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PgDB.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCountIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCustomIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDefinitionIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDeleteIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDocumentIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLExistsIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLFindIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLJsonIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLPatchIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLRemoveFieldsIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/RemoveFieldsFunctions.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCountIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCustomIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDB.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDefinitionIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDeleteIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDocumentIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteExistsIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteFindIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteJsonIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLitePatchIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteRemoveFieldsIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SubDocument.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ThrowawayDatabase.groovy create mode 100644 src/groovy/src/test/java/module-info.java create mode 100644 src/kotlinx/pom.xml create mode 100644 src/kotlinx/src/main/java/module-info.java create mode 100644 src/kotlinx/src/main/kotlin/Count.kt create mode 100644 src/kotlinx/src/main/kotlin/Custom.kt create mode 100644 src/kotlinx/src/main/kotlin/Definition.kt create mode 100644 src/kotlinx/src/main/kotlin/Delete.kt create mode 100644 src/kotlinx/src/main/kotlin/Document.kt create mode 100644 src/kotlinx/src/main/kotlin/DocumentConfig.kt create mode 100644 src/kotlinx/src/main/kotlin/Exists.kt create mode 100644 src/kotlinx/src/main/kotlin/Find.kt create mode 100644 src/kotlinx/src/main/kotlin/Json.kt create mode 100644 src/kotlinx/src/main/kotlin/Parameters.kt create mode 100644 src/kotlinx/src/main/kotlin/Patch.kt create mode 100644 src/kotlinx/src/main/kotlin/RemoveFields.kt create mode 100644 src/kotlinx/src/main/kotlin/Results.kt create mode 100644 src/kotlinx/src/main/kotlin/extensions/Connection.kt create mode 100644 src/kotlinx/src/main/module-info.md create mode 100644 src/kotlinx/src/test/java/module-info.java create mode 100644 src/kotlinx/src/test/kotlin/DocumentConfigTest.kt create mode 100644 src/kotlinx/src/test/kotlin/Types.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/CountFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/DefinitionFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/DeleteFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/DocumentFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/ExistsFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/FindFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PatchFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PgDB.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLCountIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLDeleteIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLDocumentIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLExistsIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLFindIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLPatchIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/RemoveFieldsFunctions.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteCountIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteDefinitionIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteDeleteIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteDocumentIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteExistsIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteFindIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLitePatchIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt create mode 100644 src/scala/pom.xml create mode 100644 src/scala/scala.iml create mode 100644 src/scala/src/main/scala/Count.scala create mode 100644 src/scala/src/main/scala/Custom.scala create mode 100644 src/scala/src/main/scala/Definition.scala create mode 100644 src/scala/src/main/scala/Delete.scala create mode 100644 src/scala/src/main/scala/Document.scala create mode 100644 src/scala/src/main/scala/Exists.scala create mode 100644 src/scala/src/main/scala/Find.scala create mode 100644 src/scala/src/main/scala/Json.scala create mode 100644 src/scala/src/main/scala/Parameters.scala create mode 100644 src/scala/src/main/scala/Patch.scala create mode 100644 src/scala/src/main/scala/RemoveFields.scala create mode 100644 src/scala/src/main/scala/Results.scala create mode 100644 src/scala/src/main/scala/extensions/package.scala create mode 100644 src/scala/src/test/scala/AutoIdTest.scala create mode 100644 src/scala/src/test/scala/ByteIdClass.scala create mode 100644 src/scala/src/test/scala/ConfigurationTest.scala create mode 100644 src/scala/src/test/scala/CountQueryTest.scala create mode 100644 src/scala/src/test/scala/DefinitionQueryTest.scala create mode 100644 src/scala/src/test/scala/DeleteQueryTest.scala create mode 100644 src/scala/src/test/scala/DialectTest.scala create mode 100644 src/scala/src/test/scala/DocumentIndexTest.scala create mode 100644 src/scala/src/test/scala/DocumentQueryTest.scala create mode 100644 src/scala/src/test/scala/ExistsQueryTest.scala create mode 100644 src/scala/src/test/scala/FieldMatchTest.scala create mode 100644 src/scala/src/test/scala/FieldTest.scala create mode 100644 src/scala/src/test/scala/FindQueryTest.scala create mode 100644 src/scala/src/test/scala/ForceDialect.scala create mode 100644 src/scala/src/test/scala/IntIdClass.scala create mode 100644 src/scala/src/test/scala/LongIdClass.scala create mode 100644 src/scala/src/test/scala/OpTest.scala create mode 100644 src/scala/src/test/scala/ParameterNameTest.scala create mode 100644 src/scala/src/test/scala/ParameterTest.scala create mode 100644 src/scala/src/test/scala/ParametersTest.scala create mode 100644 src/scala/src/test/scala/PatchQueryTest.scala create mode 100644 src/scala/src/test/scala/QueryUtilsTest.scala create mode 100644 src/scala/src/test/scala/RemoveFieldsQueryTest.scala create mode 100644 src/scala/src/test/scala/ShortIdClass.scala create mode 100644 src/scala/src/test/scala/StringIdClass.scala create mode 100644 src/scala/src/test/scala/WhereTest.scala create mode 100644 src/scala/src/test/scala/integration/ArrayDocument.scala create mode 100644 src/scala/src/test/scala/integration/CountFunctions.scala create mode 100644 src/scala/src/test/scala/integration/CustomFunctions.scala create mode 100644 src/scala/src/test/scala/integration/DefinitionFunctions.scala create mode 100644 src/scala/src/test/scala/integration/DeleteFunctions.scala create mode 100644 src/scala/src/test/scala/integration/DocumentFunctions.scala create mode 100644 src/scala/src/test/scala/integration/ExistsFunctions.scala create mode 100644 src/scala/src/test/scala/integration/FindFunctions.scala create mode 100644 src/scala/src/test/scala/integration/JacksonDocumentSerializer.scala create mode 100644 src/scala/src/test/scala/integration/JsonDocument.scala create mode 100644 src/scala/src/test/scala/integration/JsonFunctions.scala create mode 100644 src/scala/src/test/scala/integration/NumIdDocument.scala create mode 100644 src/scala/src/test/scala/integration/PatchFunctions.scala create mode 100644 src/scala/src/test/scala/integration/PgDB.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLCountIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLDefinitionIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLDeleteIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLDocumentIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLExistsIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLFindIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLPatchIT.scala create mode 100644 src/scala/src/test/scala/integration/PostgreSQLRemoveFieldsIT.scala create mode 100644 src/scala/src/test/scala/integration/RemoveFieldsFunctions.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteCountIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteCustomIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteDB.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteDefinitionIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteDeleteIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteDocumentIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteExistsIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteFindIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteJsonIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLitePatchIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteRemoveFieldsIT.scala create mode 100644 src/scala/src/test/scala/integration/SubDocument.scala create mode 100644 src/scala/src/test/scala/integration/ThrowawayDatabase.scala create mode 100644 src/scala/src/test/scala/package.scala diff --git a/.gitignore b/.gitignore index 0296a22..7fea862 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,9 @@ replay_pid* # Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects .kotlin/ + +# Temporary output directories +**/target + +# Maven Central Repo settings +settings.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..919ce1f --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..8788ef7 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..66a447d --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..b301a31 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..d1e0db8 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/KotlinJavaRuntime.xml b/.idea/libraries/KotlinJavaRuntime.xml new file mode 100644 index 0000000..cf8a559 --- /dev/null +++ b/.idea/libraries/KotlinJavaRuntime.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__scala_sdk_3_0_0.xml b/.idea/libraries/Maven__scala_sdk_3_0_0.xml new file mode 100644 index 0000000..d649b7c --- /dev/null +++ b/.idea/libraries/Maven__scala_sdk_3_0_0.xml @@ -0,0 +1,26 @@ + + + + Scala_3_0 + + + + + + + + + + + + + + + + file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-sbt-bridge/3.0.0/scala3-sbt-bridge-3.0.0.jar + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__scala_sdk_3_1_3.xml b/.idea/libraries/Maven__scala_sdk_3_1_3.xml new file mode 100644 index 0000000..17f32de --- /dev/null +++ b/.idea/libraries/Maven__scala_sdk_3_1_3.xml @@ -0,0 +1,26 @@ + + + + Scala_3_1 + + + + + + + + + + + + + + + + file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-sbt-bridge/3.1.3/scala3-sbt-bridge-3.1.3.jar + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__scala_sdk_3_3_3.xml b/.idea/libraries/Maven__scala_sdk_3_3_3.xml new file mode 100644 index 0000000..a753719 --- /dev/null +++ b/.idea/libraries/Maven__scala_sdk_3_3_3.xml @@ -0,0 +1,25 @@ + + + + Scala_3_3 + + + + + + + + + + + + + + + file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-sbt-bridge/3.3.3/scala3-sbt-bridge-3.3.3.jar + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Maven__scala_sdk_3_5_2.xml b/.idea/libraries/Maven__scala_sdk_3_5_2.xml new file mode 100644 index 0000000..edaffc7 --- /dev/null +++ b/.idea/libraries/Maven__scala_sdk_3_5_2.xml @@ -0,0 +1,26 @@ + + + + Scala_3_5 + + + + + + + + + + + + + + + + file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-sbt-bridge/3.5.2/scala3-sbt-bridge-3.5.2.jar + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..85ecaa5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/scala_compiler.xml b/.idea/scala_compiler.xml new file mode 100644 index 0000000..96c87c5 --- /dev/null +++ b/.idea/scala_compiler.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/scala_settings.xml b/.idea/scala_settings.xml new file mode 100644 index 0000000..4608fe0 --- /dev/null +++ b/.idea/scala_settings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 78c5f33..dac2e29 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,53 @@ # solutions.bitbadger.documents -Treat PostgreSQL and SQLite as document stores from Java and Kotlin \ No newline at end of file +Treat PostgreSQL and SQLite as document stores from Java, Kotlin, Scala, and Groovy + +## Examples + +```java +// Retrieve (find) all orders (Java) +public List findOrders(Connection conn) { + Find.all(/*table name*/ "order", /*type*/ Order.class, conn); +} +``` + +```kotlin +// Mark an order as fulfilled (Kotlin) +fun markFulfilled(orderId: Long, conn: Connection) = + conn.patchById( + /*table name*/ "order", + /*document ID*/ orderId, + /*patch object*/ mapOf("fulfilled" to true) + ) +``` + +```scala +// Delete orders marked as obsolete (Scala) +def deleteObsolete(Connection conn): + conn.deleteByFields(/*table name*/ "order", + /*field criteria*/ Field.equal("obsolete", true) :: Nil) +``` + +```groovy +// Remove the pending status from multiple orders (Groovy) +void clearPending(List orderIds, Connection conn) { + conn.removeFieldsByFields(/*table name*/ "order", + /*field criteria*/ List.of(Field.any("id", orderIds)), + /*fields to remove*/ List.of("pending")) +} +``` + +## Packages / Modules + +* The `core` module provides the base implementation and can be used from any JVM language. + * The `solutions.bitbadger.documents` package contains support types like `Configuration` and `Field`. + * The `solutions.bitbadger.documents.java` package contains document access functions and serialization config. + * The `solutions.bitbadger.documents.java.extensions` package contains extensions on the JDBC `Connection` object, callable as extension methods from Kotlin or as static functions from other languages. + +* The `groovy` module packages the extension methods so that Groovy can access them. No other packages will need to be imported; they will show up on any `Connection` instance. + +* The `kotlinx` module utilizes the kotlinx-serialization project for its JSON serialization, which requires a different serializer and different function/method signatures (`inline fun ...` vs. `fun ...`). + * `solutions.bitbadger.documents.kotlinx` and `solutions.bitbadger.documents.kotlinx.extensions` packages expose a similar API to their `java` counterparts, but one designed to be consumed from Kotlin. Generally, document retrieval functions will require a generic parameter instead of a `Class` parameter. + +* The `scala` module extends `core` by utilizing Scala's implicit `ClassTag`s to remove the `Class[T]` parameter. + * `solutions.bitbadger.documents.scala` and `solutions.bitbadger.documents.scala.extensions` packages expose the same API as their `java` counterparts, utilizing Scala collections and `Option`s instead of Java collections and `Optional`s. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..eb24808 --- /dev/null +++ b/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + solutions.bitbadger + documents + 1.0.0-RC1 + pom + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite + https://relationaldocs.bitbadger.solutions/jvm/ + + + + MIT License + https://www.opensource.org/licenses/mit-license.php + + + + + + Daniel J. Summers + daniel@bitbadger.solutions + Bit Badger Solutions + https://bitbadger.solutions + + + + + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents + + + + UTF-8 + official + 17 + ${java.version} + true + 2.1.20 + 1.8.0 + 3.5.2 + 4.0.26 + 3.5.2 + 3.5.2 + 2.18.2 + 3.46.1.2 + 42.7.5 + 3.6.0 + 3.3.1 + 3.11.2 + + + + ./src/core + ./src/groovy + ./src/kotlinx + ./src/scala + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.6 + + + sign-artifacts + verify + + sign + + + + + + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + org.xerial + sqlite-jdbc + ${sqlite.version} + test + + + org.slf4j + slf4j-simple + 2.0.16 + test + + + org.postgresql + postgresql + ${postgresql.version} + test + + + + \ No newline at end of file diff --git a/solutions.bitbadger.documents.iml b/solutions.bitbadger.documents.iml new file mode 100644 index 0000000..9a5cfce --- /dev/null +++ b/solutions.bitbadger.documents.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/core/core.iml b/src/core/core.iml new file mode 100644 index 0000000..2feb230 --- /dev/null +++ b/src/core/core.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/core/pom.xml b/src/core/pom.xml new file mode 100644 index 0000000..63f0086 --- /dev/null +++ b/src/core/pom.xml @@ -0,0 +1,182 @@ + + + 4.0.0 + + solutions.bitbadger + documents + 1.0.0-RC1 + ../../pom.xml + + + solutions.bitbadger.documents + core + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (Core Library) + https://relationaldocs.bitbadger.solutions/jvm/ + + + + MIT License + https://www.opensource.org/licenses/mit-license.php + + + + + + Daniel J. Summers + daniel@bitbadger.solutions + Bit Badger Solutions + https://bitbadger.solutions + + + + + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + process-sources + + compile + + + + ${project.basedir}/src/main/java + ${project.basedir}/src/main/kotlin + + + + + test-compile + process-test-sources + + test-compile + + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + + + + + + maven-surefire-plugin + ${surefire.version} + + + maven-failsafe-plugin + ${failsafe.version} + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + org.codehaus.mojo + build-helper-maven-plugin + ${buildHelperPlugin.version} + + + generate-sources + + add-source + + + + src/main/kotlin + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${sourcePlugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.jetbrains.dokka + dokka-maven-plugin + 2.0.0 + + true + ${project.basedir}/src/main/module-info.md + + + + package + + javadocJar + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + Deployment-core-${project.version} + central + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + + + + \ No newline at end of file diff --git a/src/core/src/main/java/module-info.java b/src/core/src/main/java/module-info.java new file mode 100644 index 0000000..63cd2b9 --- /dev/null +++ b/src/core/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module solutions.bitbadger.documents.core { + requires kotlin.stdlib; + requires kotlin.reflect; + requires java.sql; + exports solutions.bitbadger.documents; + exports solutions.bitbadger.documents.java; + exports solutions.bitbadger.documents.java.extensions; + exports solutions.bitbadger.documents.query; +} diff --git a/src/core/src/main/kotlin/AutoId.kt b/src/core/src/main/kotlin/AutoId.kt new file mode 100644 index 0000000..f94d7da --- /dev/null +++ b/src/core/src/main/kotlin/AutoId.kt @@ -0,0 +1,85 @@ +package solutions.bitbadger.documents + +import kotlin.jvm.Throws +import kotlin.reflect.full.* +import kotlin.reflect.jvm.isAccessible + +/** + * Strategies for automatic document IDs + */ +enum class AutoId { + /** No automatic IDs will be generated */ + DISABLED, + + /** Generate a `MAX`-plus-1 numeric ID */ + NUMBER, + + /** Generate a `UUID` string ID */ + UUID, + + /** Generate a random hex character string ID */ + RANDOM_STRING; + + companion object { + + /** + * Generate a `UUID` string + * + * @return A `UUID` string + */ + @JvmStatic + fun generateUUID(): String = + java.util.UUID.randomUUID().toString().replace("-", "") + + /** + * Generate a string of random hex characters + * + * @param length The length of the string (optional; defaults to configured length) + * @return A string of random hex characters of the requested length + */ + @JvmStatic + fun generateRandomString(length: Int? = null): String = + (length ?: Configuration.idStringLength).let { len -> + kotlin.random.Random.nextBytes((len + 2) / 2) + .joinToString("") { String.format("%02x", it) } + .substring(0, len) + } + + /** + * Determine if a document needs an automatic ID applied + * + * @param strategy The auto ID strategy for which the document is evaluated + * @param document The document whose need of an automatic ID should be determined + * @param idProp The name of the document property containing the ID + * @return `true` if the document needs an automatic ID, `false` if not + * @throws DocumentException If bad input prevents the determination + */ + @Throws(DocumentException::class) + @JvmStatic + fun needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean { + if (document == null) throw DocumentException("document cannot be null") + + if (strategy == DISABLED) return false + + val id = document!!::class.memberProperties.find { it.name == idProp }?.apply { isAccessible = true } + if (id == null) throw DocumentException("$idProp not found in document") + + if (strategy == NUMBER) { + return when (id.returnType) { + Byte::class.createType() -> id.call(document) == 0.toByte() + Short::class.createType() -> id.call(document) == 0.toShort() + Int::class.createType() -> id.call(document) == 0 + Long::class.createType() -> id.call(document) == 0.toLong() + else -> throw DocumentException("$idProp was not a number; cannot auto-generate number ID") + } + } + + val typ = id.returnType.toString() + if (typ.endsWith("String") || typ.endsWith("String!")) { + return id.call(document) == "" + } + + throw DocumentException("$idProp was not a string ($typ); cannot auto-generate UUID or random string") + } + } +} diff --git a/src/core/src/main/kotlin/Comparison.kt b/src/core/src/main/kotlin/Comparison.kt new file mode 100644 index 0000000..f187882 --- /dev/null +++ b/src/core/src/main/kotlin/Comparison.kt @@ -0,0 +1,68 @@ +package solutions.bitbadger.documents + +/** + * Information required to generate a JSON field comparison + */ +interface Comparison { + + /** The operation for the field comparison */ + val op: Op + + /** The value against which the comparison will be made */ + val value: T + + /** Whether the value should be considered numeric */ + val isNumeric: Boolean +} + +/** + * Function to determine if a value is numeric + * + * @param it The value in question + * @return True if it is a numeric type, false if not + */ +private fun isNumeric(it: T) = + it is Byte || it is Short || it is Int || it is Long + +/** + * A single-value comparison against a field in a JSON document + */ +class ComparisonSingle(override val op: Op, override val value: T) : Comparison { + + init { + when (op) { + Op.BETWEEN, Op.IN, Op.IN_ARRAY -> + throw DocumentException("Cannot use single comparison for multiple values") + else -> { } + } + } + + override val isNumeric = isNumeric(value) + + override fun toString() = + "$op $value" +} + +/** + * A range comparison against a field in a JSON document + */ +class ComparisonBetween(override val value: Pair) : Comparison> { + override val op = Op.BETWEEN + override val isNumeric = isNumeric(value.first) +} + +/** + * A check within a collection of values + */ +class ComparisonIn(override val value: Collection) : Comparison> { + override val op = Op.IN + override val isNumeric = !value.isEmpty() && isNumeric(value.elementAt(0)) +} + +/** + * A check within a collection of values against an array in a document + */ +class ComparisonInArray(override val value: Pair>) : Comparison>> { + override val op = Op.IN_ARRAY + override val isNumeric = false +} diff --git a/src/core/src/main/kotlin/Configuration.kt b/src/core/src/main/kotlin/Configuration.kt new file mode 100644 index 0000000..2aa3228 --- /dev/null +++ b/src/core/src/main/kotlin/Configuration.kt @@ -0,0 +1,67 @@ +package solutions.bitbadger.documents + +import java.sql.Connection +import java.sql.DriverManager + +/** + * Configuration for the document library + */ +object Configuration { + + /** The field in which a document's ID is stored */ + @JvmField + var idField = "id" + + /** The automatic ID strategy to use */ + @JvmField + var autoIdStrategy = AutoId.DISABLED + + /** The length of automatic random hex character string */ + @JvmField + var idStringLength = 16 + + /** The derived dialect value from the connection string */ + private var dialectValue: Dialect? = null + + /** The connection string for the JDBC connection */ + @JvmStatic + var connectionString: String? = null + /** + * Set a value for the connection string + * @param value The connection string to set + */ + set(value) { + field = value + dialectValue = if (value.isNullOrBlank()) null else Dialect.deriveFromConnectionString(value) + } + + /** + * Retrieve a new connection to the configured database + * + * @return A new connection to the configured database + * @throws DocumentException If the connection string is not set before calling this + */ + @Throws(DocumentException::class) + @JvmStatic + fun dbConn(): Connection { + if (connectionString == null) { + throw DocumentException("Please provide a connection string before attempting data access") + } + return DriverManager.getConnection(connectionString) + } + + /** + * The dialect in use + * + * @param process The process being attempted + * @return The dialect for the current connection + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun dialect(process: String? = null): Dialect = + dialectValue ?: throw DocumentException( + "Database mode not set" + if (process == null) "" else "; cannot $process" + ) +} diff --git a/src/core/src/main/kotlin/Dialect.kt b/src/core/src/main/kotlin/Dialect.kt new file mode 100644 index 0000000..1d0c32b --- /dev/null +++ b/src/core/src/main/kotlin/Dialect.kt @@ -0,0 +1,33 @@ +package solutions.bitbadger.documents + +import kotlin.jvm.Throws + +/** + * The SQL dialect to use when building queries + */ +enum class Dialect { + /** PostgreSQL */ + POSTGRESQL, + + /** SQLite */ + SQLITE; + + companion object { + + /** + * Derive the dialect from the given connection string + * + * @param connectionString The connection string from which the dialect will be derived + * @return The dialect for the connection string + * @throws DocumentException If the dialect cannot be determined + */ + @Throws(DocumentException::class) + @JvmStatic + fun deriveFromConnectionString(connectionString: String): Dialect = + when { + connectionString.contains(":sqlite:") -> SQLITE + connectionString.contains(":postgresql:") -> POSTGRESQL + else -> throw DocumentException("Cannot determine dialect from [$connectionString]") + } + } +} diff --git a/src/core/src/main/kotlin/DocumentException.kt b/src/core/src/main/kotlin/DocumentException.kt new file mode 100644 index 0000000..d415801 --- /dev/null +++ b/src/core/src/main/kotlin/DocumentException.kt @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents + +/** + * An exception caused by invalid operations in the document library + * + * @param message The message for the exception + * @param cause The underlying exception (optional) + */ +class DocumentException @JvmOverloads constructor(message: String, cause: Throwable? = null) : Exception(message, cause) diff --git a/src/core/src/main/kotlin/DocumentIndex.kt b/src/core/src/main/kotlin/DocumentIndex.kt new file mode 100644 index 0000000..3ce8743 --- /dev/null +++ b/src/core/src/main/kotlin/DocumentIndex.kt @@ -0,0 +1,13 @@ +package solutions.bitbadger.documents + +/** + * The type of index to generate for the document + */ +enum class DocumentIndex(val sql: String) { + + /** A GIN index with standard operations (all operators supported) */ + FULL(""), + + /** A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) */ + OPTIMIZED(" jsonb_path_ops") +} diff --git a/src/core/src/main/kotlin/DocumentSerializer.kt b/src/core/src/main/kotlin/DocumentSerializer.kt new file mode 100644 index 0000000..bbdee39 --- /dev/null +++ b/src/core/src/main/kotlin/DocumentSerializer.kt @@ -0,0 +1,24 @@ +package solutions.bitbadger.documents + +/** + * The interface for a document serializer/deserializer + */ +interface DocumentSerializer { + + /** + * Serialize a document to its JSON representation + * + * @param document The document to be serialized + * @return The JSON representation of the document + */ + fun serialize(document: TDoc): String + + /** + * Deserialize a document from its JSON representation + * + * @param json The JSON representation of the document + * @param clazz The class of the document to be deserialized + * @return The document instance represented by the given JSON string + */ + fun deserialize(json: String, clazz: Class): TDoc +} diff --git a/src/core/src/main/kotlin/Field.kt b/src/core/src/main/kotlin/Field.kt new file mode 100644 index 0000000..c4d017a --- /dev/null +++ b/src/core/src/main/kotlin/Field.kt @@ -0,0 +1,320 @@ +package solutions.bitbadger.documents + +import kotlin.jvm.Throws + +/** + * A field and its comparison + * + * @property name The name of the field in the JSON document + * @property comparison The comparison to apply against the field + * @property parameterName The name of the parameter to use in the query (optional, generated if missing) + * @property qualifier A table qualifier to use to address the `data` field (useful for multi-table queries) + */ +class Field private constructor( + val name: String, + val comparison: Comparison, + val parameterName: String? = null, + val qualifier: String? = null) { + + init { + if (parameterName != null && !parameterName.startsWith(':') && !parameterName.startsWith('@')) + throw DocumentException("Parameter Name must start with : or @ ($name)") + } + + /** + * Specify the parameter name for the field + * + * @param paramName The parameter name to use for this field + * @return A new `Field` with the parameter name specified + */ + fun withParameterName(paramName: String) = + Field(name, comparison, paramName, qualifier) + + /** + * Specify a qualifier (alias) for the document table + * + * @param alias The table alias for this field + * @return A new `Field` with the table qualifier specified + */ + fun withQualifier(alias: String) = + Field(name, comparison, parameterName, alias) + + /** + * Get the path for this field + * + * @param dialect The SQL dialect to use for the path to the JSON field + * @param format Whether the value should be retrieved as JSON or SQL (optional, default SQL) + * @return The path for the field + */ + @JvmOverloads + fun path(dialect: Dialect, format: FieldFormat = FieldFormat.SQL): String = + (if (qualifier == null) "" else "${qualifier}.") + nameToPath(name, dialect, format) + + /** Parameters to bind each value of `IN` and `IN_ARRAY` operations */ + private val inParameterNames: String + get() { + val values = if (comparison.op == Op.IN) { + comparison.value as Collection<*> + } else { + val parts = comparison.value as Pair<*, *> + parts.second as Collection<*> + } + return List(values.size) { idx -> "${parameterName}_$idx" }.joinToString(", ") + } + + /** + * Create a `WHERE` clause fragment for this field + * + * @return The `WHERE` clause for this field + * @throws DocumentException If the field has no parameter name or the database dialect has not been set + */ + @Throws(DocumentException::class) + fun toWhere(): String { + if (parameterName == null && !listOf(Op.EXISTS, Op.NOT_EXISTS).contains(comparison.op)) + throw DocumentException("Parameter for $name must be specified") + + val dialect = Configuration.dialect("make field WHERE clause") + val fieldName = path(dialect, if (comparison.op == Op.IN_ARRAY) FieldFormat.JSON else FieldFormat.SQL) + val fieldPath = when (dialect) { + Dialect.POSTGRESQL -> if (comparison.isNumeric) "($fieldName)::numeric" else fieldName + Dialect.SQLITE -> fieldName + } + val criteria = when (comparison.op) { + in listOf(Op.EXISTS, Op.NOT_EXISTS) -> "" + Op.BETWEEN -> " ${parameterName}min AND ${parameterName}max" + Op.IN -> " ($inParameterNames)" + Op.IN_ARRAY -> if (dialect == Dialect.POSTGRESQL) " ARRAY[$inParameterNames]" else "" + else -> " $parameterName" + } + + @Suppress("UNCHECKED_CAST") + return if (dialect == Dialect.SQLITE && comparison.op == Op.IN_ARRAY) { + val (table, _) = comparison.value as? Pair ?: throw DocumentException("InArray field invalid") + "EXISTS (SELECT 1 FROM json_each($table.data, '$.$name') WHERE value IN ($inParameterNames))" + } else { + "$fieldPath ${comparison.op.sql}$criteria" + } + } + + /** + * Append the parameters required for this field + * + * @param existing The existing parameters + * @return The collection with the necessary parameters appended + */ + fun appendParameter(existing: MutableCollection>): MutableCollection> { + val typ = if (comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING + when (comparison) { + is ComparisonBetween<*> -> { + existing.add(Parameter("${parameterName}min", typ, comparison.value.first)) + existing.add(Parameter("${parameterName}max", typ, comparison.value.second)) + } + + is ComparisonIn<*> -> { + comparison.value.forEachIndexed { index, item -> + existing.add(Parameter("${parameterName}_$index", typ, item)) + } + } + + is ComparisonInArray<*> -> { + val mkString = Configuration.dialect("append parameters for InArray") == Dialect.POSTGRESQL + comparison.value.second.forEachIndexed { index, item -> + if (mkString) { + existing.add(Parameter("${parameterName}_$index", ParameterType.STRING, "$item")) + } else { + existing.add(Parameter("${parameterName}_$index", typ, item)) + } + } + } + + else -> { + if (comparison.op != Op.EXISTS && comparison.op != Op.NOT_EXISTS) { + existing.add(Parameter(parameterName!!, typ, comparison.value)) + } + } + } + return existing + } + + override fun toString() = + "Field ${parameterName ?: ""} $comparison${qualifier?.let { " (qualifier $it)" } ?: ""}" + + companion object { + + /** + * Create a field equality comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun equal(name: String, value: T, paramName: String? = null) = + Field(name, ComparisonSingle(Op.EQUAL, value), paramName) + + /** + * Create a field greater-than comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun greater(name: String, value: T, paramName: String? = null) = + Field(name, ComparisonSingle(Op.GREATER, value), paramName) + + /** + * Create a field greater-than-or-equal-to comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun greaterOrEqual(name: String, value: T, paramName: String? = null) = + Field(name, ComparisonSingle(Op.GREATER_OR_EQUAL, value), paramName) + + /** + * Create a field less-than comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun less(name: String, value: T, paramName: String? = null) = + Field(name, ComparisonSingle(Op.LESS, value), paramName) + + /** + * Create a field less-than-or-equal-to comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun lessOrEqual(name: String, value: T, paramName: String? = null) = + Field(name, ComparisonSingle(Op.LESS_OR_EQUAL, value), paramName) + + /** + * Create a field inequality comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun notEqual(name: String, value: T, paramName: String? = null) = + Field(name, ComparisonSingle(Op.NOT_EQUAL, value), paramName) + + /** + * Create a field range comparison + * + * @param name The name of the field to be compared + * @param minValue The lower value for the comparison + * @param maxValue The upper value for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun between(name: String, minValue: T, maxValue: T, paramName: String? = null) = + Field(name, ComparisonBetween(Pair(minValue, maxValue)), paramName) + + /** + * Create a field where any values match (SQL `IN`) + * + * @param name The name of the field to be compared + * @param values The values for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun any(name: String, values: Collection, paramName: String? = null) = + Field(name, ComparisonIn(values), paramName) + + /** + * Create a field where values should exist in a document's array + * + * @param name The name of the field to be compared + * @param tableName The name of the document table + * @param values The values for the comparison + * @param paramName The parameter name for the field (optional, defaults to auto-generated) + * @return A `Field` with the given comparison + */ + @JvmStatic + @JvmOverloads + fun inArray(name: String, tableName: String, values: Collection, paramName: String? = null) = + Field(name, ComparisonInArray(Pair(tableName, values)), paramName) + + /** + * Create a field where a document field should exist + * + * @param name The name of the field whose existence should be checked + * @return A `Field` with the given comparison + */ + @JvmStatic + fun exists(name: String) = + Field(name, ComparisonSingle(Op.EXISTS, "")) + + /** + * Create a field where a document field should not exist + * + * @param name The name of the field whose existence should be checked + * @return A `Field` with the given comparison + */ + @JvmStatic + fun notExists(name: String) = + Field(name, ComparisonSingle(Op.NOT_EXISTS, "")) + + /** + * Create a field with a given named comparison (useful for ordering fields) + * + * @param name The name of the field + * @return A `Field` with the given name (comparison equal to an empty string) + */ + @JvmStatic + fun named(name: String) = + Field(name, ComparisonSingle(Op.EQUAL, "")) + + /** + * Convert a name to the SQL path for the given dialect + * + * @param name The field name to be translated + * @param dialect The database for which the path should be created + * @param format Whether the field should be retrieved as a JSON value or a SQL value + * @return The path to the JSON field + */ + @JvmStatic + fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String { + val path = StringBuilder("data") + val extra = if (format == FieldFormat.SQL) ">" else "" + if (name.indexOf('.') > -1) { + if (dialect == Dialect.POSTGRESQL) { + path.append("#>", extra, "'{", name.replace('.', ','), "}'") + } else { + val names = name.split('.').toMutableList() + val last = names.removeLast() + names.forEach { path.append("->'", it, "'") } + path.append("->", extra, "'", last, "'") + } + } else { + path.append("->", extra, "'", name, "'") + } + return path.toString() + } + } +} diff --git a/src/core/src/main/kotlin/FieldFormat.kt b/src/core/src/main/kotlin/FieldFormat.kt new file mode 100644 index 0000000..d323696 --- /dev/null +++ b/src/core/src/main/kotlin/FieldFormat.kt @@ -0,0 +1,12 @@ +package solutions.bitbadger.documents + +/** + * The data format for a document field retrieval + */ +enum class FieldFormat { + /** Retrieve the field as a SQL value (string in PostgreSQL, best guess in SQLite */ + SQL, + + /** Retrieve the field as a JSON value */ + JSON +} diff --git a/src/core/src/main/kotlin/FieldMatch.kt b/src/core/src/main/kotlin/FieldMatch.kt new file mode 100644 index 0000000..3af7230 --- /dev/null +++ b/src/core/src/main/kotlin/FieldMatch.kt @@ -0,0 +1,12 @@ +package solutions.bitbadger.documents + +/** + * How fields should be matched in by-field queries + */ +enum class FieldMatch(val sql: String) { + /** Match any of the field criteria (`OR`) */ + ANY("OR"), + + /** Match all the field criteria (`AND`) */ + ALL("AND"), +} diff --git a/src/core/src/main/kotlin/Op.kt b/src/core/src/main/kotlin/Op.kt new file mode 100644 index 0000000..45004f4 --- /dev/null +++ b/src/core/src/main/kotlin/Op.kt @@ -0,0 +1,39 @@ +package solutions.bitbadger.documents + +/** + * A comparison operator used for fields + */ +enum class Op(val sql: String) { + /** Compare using equality */ + EQUAL("="), + + /** Compare using greater-than */ + GREATER(">"), + + /** Compare using greater-than-or-equal-to */ + GREATER_OR_EQUAL(">="), + + /** Compare using less-than */ + LESS("<"), + + /** Compare using less-than-or-equal-to */ + LESS_OR_EQUAL("<="), + + /** Compare using inequality */ + NOT_EQUAL("<>"), + + /** Compare between two values */ + BETWEEN("BETWEEN"), + + /** Compare existence in a list of values */ + IN("IN"), + + /** Compare overlap between an array and a list of values */ + IN_ARRAY("??|"), + + /** Compare existence */ + EXISTS("IS NOT NULL"), + + /** Compare nonexistence */ + NOT_EXISTS("IS NULL") +} diff --git a/src/core/src/main/kotlin/Parameter.kt b/src/core/src/main/kotlin/Parameter.kt new file mode 100644 index 0000000..3e9c9a7 --- /dev/null +++ b/src/core/src/main/kotlin/Parameter.kt @@ -0,0 +1,58 @@ +package solutions.bitbadger.documents + +import java.sql.PreparedStatement +import java.sql.Types +import kotlin.jvm.Throws + +/** + * A parameter to use for a query + * + * @property name The name of the parameter (prefixed with a colon) + * @property type The type of this parameter + * @property value The value of the parameter + */ +class Parameter(val name: String, val type: ParameterType, val value: T) { + + init { + if (!name.startsWith(':') && !name.startsWith('@')) + throw DocumentException("Name must start with : or @ ($name)") + } + + /** + * Bind this parameter to a prepared statement at the given index + * + * @param stmt The prepared statement to which this parameter should be bound + * @param index The index (1-based) to which the parameter should be bound + * @throws DocumentException If a number parameter is given a non-numeric value + */ + @Throws(DocumentException::class) + fun bind(stmt: PreparedStatement, index: Int) { + when (type) { + ParameterType.NUMBER -> { + when (value) { + null -> stmt.setNull(index, Types.NULL) + is Byte -> stmt.setByte(index, value) + is Short -> stmt.setShort(index, value) + is Int -> stmt.setInt(index, value) + is Long -> stmt.setLong(index, value) + else -> throw DocumentException( + "Number parameter must be Byte, Short, Int, or Long (${value::class.simpleName})" + ) + } + } + + ParameterType.STRING -> { + when (value) { + null -> stmt.setNull(index, Types.NULL) + is String -> stmt.setString(index, value) + else -> stmt.setString(index, value.toString()) + } + } + + ParameterType.JSON -> stmt.setObject(index, value as String, Types.OTHER) + } + } + + override fun toString() = + "$type[$name] = $value" +} diff --git a/src/core/src/main/kotlin/ParameterName.kt b/src/core/src/main/kotlin/ParameterName.kt new file mode 100644 index 0000000..a090db0 --- /dev/null +++ b/src/core/src/main/kotlin/ParameterName.kt @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents + +/** + * Derive parameter names; each instance wraps a counter to provide names for anonymous fields + */ +class ParameterName { + + private var currentIdx = 0 + + /** + * Derive the parameter name from the current possibly-null string + * + * @param paramName The name of the parameter as specified by the field + * @return The name from the field, if present, or a derived name if missing + */ + fun derive(paramName: String?): String = + paramName ?: ":field${currentIdx++}" +} diff --git a/src/core/src/main/kotlin/ParameterType.kt b/src/core/src/main/kotlin/ParameterType.kt new file mode 100644 index 0000000..edf44b7 --- /dev/null +++ b/src/core/src/main/kotlin/ParameterType.kt @@ -0,0 +1,15 @@ +package solutions.bitbadger.documents + +/** + * The types of parameters supported by the document library + */ +enum class ParameterType { + /** The parameter value is some sort of number (`Byte`, `Short`, `Int`, or `Long`) */ + NUMBER, + + /** The parameter value is a string */ + STRING, + + /** The parameter should be JSON-encoded */ + JSON, +} diff --git a/src/core/src/main/kotlin/java/Count.kt b/src/core/src/main/kotlin/java/Count.kt new file mode 100644 index 0000000..bd599e9 --- /dev/null +++ b/src/core/src/main/kotlin/java/Count.kt @@ -0,0 +1,147 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.CountQuery +import java.sql.Connection +import kotlin.jvm.Throws + +/** + * Functions to count documents + */ +object Count { + + /** + * Count all documents in the table + * + * @param tableName The name of the table in which documents should be counted + * @param conn The connection over which documents should be counted + * @return A count of the documents in the table + * @throws DocumentException If any dependent process does + */ + @Throws(DocumentException::class) + @JvmStatic + fun all(tableName: String, conn: Connection) = + Custom.scalar(CountQuery.all(tableName), listOf(), Long::class.java, conn, Results::toCount) + + /** + * Count all documents in the table + * + * @param tableName The name of the table in which documents should be counted + * @return A count of the documents in the table + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun all(tableName: String) = + Configuration.dbConn().use { all(tableName, it) } + + /** + * Count documents using a field comparison + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the deletion should be executed + * @return A count of the matching documents in the table + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ): Long { + val named = Parameters.nameFields(fields) + return Custom.scalar( + CountQuery.byFields(tableName, named, howMatched), + Parameters.addFields(named), + Long::class.java, + conn, + Results::toCount + ) + } + + /** + * Count documents using a field comparison + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return A count of the matching documents in the table + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } + + /** + * Count documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the count should be executed + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, conn: Connection) = + Custom.scalar( + CountQuery.byContains(tableName), + listOf(Parameters.json(":criteria", criteria)), + Long::class.java, + conn, + Results::toCount + ) + + /** + * Count documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @return A count of the matching documents in the table + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Count documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @param conn The connection on which the count should be executed + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, conn: Connection) = + Custom.scalar( + CountQuery.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path)), + Long::class.java, + conn, + Results::toCount + ) + + /** + * Count documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @return A count of the matching documents in the table + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +} diff --git a/src/core/src/main/kotlin/java/Custom.kt b/src/core/src/main/kotlin/java/Custom.kt new file mode 100644 index 0000000..2220ece --- /dev/null +++ b/src/core/src/main/kotlin/java/Custom.kt @@ -0,0 +1,281 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Parameter +import java.io.PrintWriter +import java.sql.Connection +import java.sql.ResultSet +import java.sql.SQLException +import java.util.* +import kotlin.jvm.Throws + +/** + * Functions to run custom queries + */ +object Custom { + + /** + * Execute a query that returns a list of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param clazz The class of the document to be returned + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun list( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + conn: Connection, + mapFunc: (ResultSet, Class) -> TDoc + ) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, clazz, mapFunc) } + + /** + * Execute a query that returns a list of results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param clazz The class of the document to be returned + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun list( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + mapFunc: (ResultSet, Class) -> TDoc + ) = Configuration.dbConn().use { list(query, parameters, clazz, it, mapFunc) } + + /** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun jsonArray( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + mapFunc: (ResultSet) -> String + ) = Parameters.apply(conn, query, parameters).use { Results.toJsonArray(it, mapFunc) } + + /** + * Execute a query that returns a JSON array of results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun jsonArray(query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String) = + Configuration.dbConn().use { jsonArray(query, parameters, it, mapFunc) } + + /** + * Execute a query, writing its JSON array of results to the given `PrintWriter` + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + conn: Connection, + mapFunc: (ResultSet) -> String + ) = Parameters.apply(conn, query, parameters).use { Results.writeJsonArray(writer, it, mapFunc) } + + /** + * Execute a query, writing its JSON array of results to the given `PrintWriter` (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + mapFunc: (ResultSet) -> String + ) = Configuration.dbConn().use { writeJsonArray(query, parameters, writer, it, mapFunc) } + + /** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param clazz The class of the document to be returned + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return An `Optional` value, with the document if one matches the query + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun single( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + conn: Connection, + mapFunc: (ResultSet, Class) -> TDoc + ) = Optional.ofNullable(list("$query LIMIT 1", parameters, clazz, conn, mapFunc).singleOrNull()) + + /** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param clazz The class of the document to be returned + * @param mapFunc The mapping function between the document and the domain item + * @return The document if one matches the query, `null` otherwise + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun single( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + mapFunc: (ResultSet, Class) -> TDoc + ) = Configuration.dbConn().use { single(query, parameters, clazz, it, mapFunc) } + + /** + * Execute a query that returns JSON for one or no documents + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun jsonSingle( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + mapFunc: (ResultSet) -> String + ) = jsonArray("$query LIMIT 1", parameters, conn, mapFunc).let { + if (it == "[]") "{}" else it.substring(1, it.length - 1) + } + + /** + * Execute a query that returns JSON for one or no documents (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun jsonSingle(query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String) = + Configuration.dbConn().use { jsonSingle(query, parameters, it, mapFunc) } + + /** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param conn The connection over which the query should be executed + * @param parameters Parameters to use for the query + * @throws DocumentException If parameters are invalid or if the query fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) { + try { + Parameters.apply(conn, query, parameters).use { it.executeUpdate() } + } catch (ex: SQLException) { + throw DocumentException("Unable to execute non-query: ${ex.message}", ex) + } + } + + /** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @throws DocumentException If no connection string has been set, if parameters are invalid, or if the query fails + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun nonQuery(query: String, parameters: Collection> = listOf()) = + Configuration.dbConn().use { nonQuery(query, parameters, it) } + + /** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If parameters are invalid or if the query fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun scalar( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + conn: Connection, + mapFunc: (ResultSet, Class) -> T + ) = Parameters.apply(conn, query, parameters).use { stmt -> + try { + stmt.executeQuery().use { rs -> + rs.next() + mapFunc(rs, clazz) + } + } catch (ex: SQLException) { + throw DocumentException("Unable to retrieve scalar value: ${ex.message}", ex) + } + } + + /** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun scalar( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + mapFunc: (ResultSet, Class) -> T + ) = Configuration.dbConn().use { scalar(query, parameters, clazz, it, mapFunc) } +} diff --git a/src/core/src/main/kotlin/java/Definition.kt b/src/core/src/main/kotlin/java/Definition.kt new file mode 100644 index 0000000..baf7b9b --- /dev/null +++ b/src/core/src/main/kotlin/java/Definition.kt @@ -0,0 +1,92 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.query.DefinitionQuery +import java.sql.Connection +import kotlin.jvm.Throws + +/** + * Functions to define tables and indexes + */ +object Definition { + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + * @param conn The connection on which the query should be executed + * @throws DocumentException If the dialect is not configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun ensureTable(tableName: String, conn: Connection) = + Configuration.dialect("ensure $tableName exists").let { + Custom.nonQuery(DefinitionQuery.ensureTable(tableName, it), conn = conn) + Custom.nonQuery(DefinitionQuery.ensureKey(tableName, it), conn = conn) + } + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun ensureTable(tableName: String) = + Configuration.dbConn().use { ensureTable(tableName, it) } + + /** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed + * @param conn The connection on which the query should be executed + * @throws DocumentException If any dependent process does + */ + @Throws(DocumentException::class) + @JvmStatic + fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection, conn: Connection) = + Custom.nonQuery(DefinitionQuery.ensureIndexOn(tableName, indexName, fields), conn = conn) + + /** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed + * @throws DocumentException If no connection string has been set, or if any dependent process does + */ + @Throws(DocumentException::class) + @JvmStatic + fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection) = + Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) } + + /** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @param conn The connection on which the query should be executed + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) = + Custom.nonQuery(DefinitionQuery.ensureDocumentIndexOn(tableName, indexType), conn = conn) + + /** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex) = + Configuration.dbConn().use { ensureDocumentIndex(tableName, indexType, it) } +} diff --git a/src/core/src/main/kotlin/java/Delete.kt b/src/core/src/main/kotlin/java/Delete.kt new file mode 100644 index 0000000..969ac5e --- /dev/null +++ b/src/core/src/main/kotlin/java/Delete.kt @@ -0,0 +1,122 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.DeleteQuery +import java.sql.Connection +import kotlin.jvm.Throws + +/** + * Functions to delete documents + */ +object Delete { + + /** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, conn: Connection) = + Custom.nonQuery( + DeleteQuery.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + conn + ) + + /** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey) = + Configuration.dbConn().use { byId(tableName, docId, it) } + + /** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { + val named = Parameters.nameFields(fields) + Custom.nonQuery(DeleteQuery.byFields(tableName, named, howMatched), Parameters.addFields(named), conn) + } + + /** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } + + /** + * Delete documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, conn: Connection) = + Custom.nonQuery(DeleteQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), conn) + + /** + * Delete documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Delete documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, conn: Connection) = + Custom.nonQuery(DeleteQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), conn) + + /** + * Delete documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +} diff --git a/src/core/src/main/kotlin/java/Document.kt b/src/core/src/main/kotlin/java/Document.kt new file mode 100644 index 0000000..018b49c --- /dev/null +++ b/src/core/src/main/kotlin/java/Document.kt @@ -0,0 +1,109 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.DocumentQuery +import solutions.bitbadger.documents.query.Where +import solutions.bitbadger.documents.query.statementWhere +import java.sql.Connection +import kotlin.jvm.Throws + +/** + * Functions for manipulating documents + */ +object Document { + + /** + * Insert a new document + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + * @param conn The connection on which the query should be executed + * @throws DocumentException If IDs are misconfigured, or if the database command fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun insert(tableName: String, document: TDoc, conn: Connection) { + val strategy = Configuration.autoIdStrategy + val query = if (strategy == AutoId.DISABLED || !AutoId.needsAutoId(strategy, document, Configuration.idField)) { + DocumentQuery.insert(tableName) + } else { + DocumentQuery.insert(tableName, strategy) + } + Custom.nonQuery(query, listOf(Parameters.json(":data", document)), conn) + } + + /** + * Insert a new document + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + * @throws DocumentException If no connection string has been set; if IDs are misconfigured; or if the database + * command fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun insert(tableName: String, document: TDoc) = + Configuration.dbConn().use { insert(tableName, document, it) } + + /** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + * @param conn The connection on which the query should be executed + * @throws DocumentException If the database command fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun save(tableName: String, document: TDoc, conn: Connection) = + Custom.nonQuery(DocumentQuery.save(tableName), listOf(Parameters.json(":data", document)), conn) + + /** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + * @throws DocumentException If no connection string has been set, or if the database command fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun save(tableName: String, document: TDoc) = + Configuration.dbConn().use { save(tableName, document, it) } + + /** + * Update (replace) a document by its ID + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + * @param conn The connection on which the query should be executed + * @throws DocumentException If no dialect has been configured, or if the database command fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun update(tableName: String, docId: TKey, document: TDoc, conn: Connection) = + Custom.nonQuery( + statementWhere(DocumentQuery.update(tableName), Where.byId(":id", docId)), + Parameters.addFields( + listOf(Field.equal(Configuration.idField, docId, ":id")), + mutableListOf(Parameters.json(":data", document)) + ), + conn + ) + + /** + * Update (replace) a document by its ID + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + * @throws DocumentException If no connection string has been set, or if the database command fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun update(tableName: String, docId: TKey, document: TDoc) = + Configuration.dbConn().use { update(tableName, docId, document, it) } +} diff --git a/src/core/src/main/kotlin/java/DocumentConfig.kt b/src/core/src/main/kotlin/java/DocumentConfig.kt new file mode 100644 index 0000000..f817b4b --- /dev/null +++ b/src/core/src/main/kotlin/java/DocumentConfig.kt @@ -0,0 +1,15 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.DocumentSerializer + +/** + * Configuration for document serialization + */ +object DocumentConfig { + + /** + * The serializer to use for documents + */ + @JvmStatic + var serializer: DocumentSerializer = NullDocumentSerializer() +} diff --git a/src/core/src/main/kotlin/java/Exists.kt b/src/core/src/main/kotlin/java/Exists.kt new file mode 100644 index 0000000..bbfa151 --- /dev/null +++ b/src/core/src/main/kotlin/java/Exists.kt @@ -0,0 +1,155 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.ExistsQuery +import java.sql.Connection +import kotlin.jvm.Throws + +/** + * Functions to determine whether documents exist + */ +object Exists { + + /** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @param conn The connection on which the existence check should be executed + * @return True if the document exists, false if not + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, conn: Connection) = + Custom.scalar( + ExistsQuery.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + Boolean::class.java, + conn, + Results::toExists + ) + + /** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @return True if the document exists, false if not + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey) = + Configuration.dbConn().use { byId(tableName, docId, it) } + + /** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ): Boolean { + val named = Parameters.nameFields(fields) + return Custom.scalar( + ExistsQuery.byFields(tableName, named, howMatched), + Parameters.addFields(named), + Boolean::class.java, + conn, + Results::toExists + ) + } + + /** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return True if any matching documents exist, false if not + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } + + /** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, conn: Connection) = + Custom.scalar( + ExistsQuery.byContains(tableName), + listOf(Parameters.json(":criteria", criteria)), + Boolean::class.java, + conn, + Results::toExists + ) + + /** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @return True if any matching documents exist, false if not + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, conn: Connection) = + Custom.scalar( + ExistsQuery.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path)), + Boolean::class.java, + conn, + Results::toExists + ) + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @return True if any matching documents exist, false if not + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +} diff --git a/src/core/src/main/kotlin/java/Find.kt b/src/core/src/main/kotlin/java/Find.kt new file mode 100644 index 0000000..09d8bef --- /dev/null +++ b/src/core/src/main/kotlin/java/Find.kt @@ -0,0 +1,502 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.query.orderBy +import java.sql.Connection +import java.util.Optional +import kotlin.jvm.Throws + +/** + * Functions to find and retrieve documents + */ +object Find { + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents from the given table + * @throws DocumentException If query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun all(tableName: String, clazz: Class, orderBy: Collection>? = null, conn: Connection) = + Custom.list(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), clazz, conn, Results::fromData) + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents from the given table + * @throws DocumentException If no connection string has been set, or if query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun all(tableName: String, clazz: Class, orderBy: Collection>? = null) = + Configuration.dbConn().use { all(tableName, clazz, orderBy, it) } + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param clazz The class of the document to be returned + * @param conn The connection over which documents should be retrieved + * @return A list of documents from the given table + * @throws DocumentException If query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun all(tableName: String, clazz: Class, conn: Connection) = + all(tableName, clazz, null, conn) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @param clazz The class of the document to be returned + * @param conn The connection over which documents should be retrieved + * @return An `Optional` item with the document if it is found + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, clazz: Class, conn: Connection) = + Custom.single( + FindQuery.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + clazz, + conn, + Results::fromData + ) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @param clazz The class of the document to be returned + * @return An `Optional` item with the document if it is found + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, clazz: Class) = + Configuration.dbConn().use { byId(tableName, docId, clazz, it) } + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param clazz The class of the document to be returned + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun byFields( + tableName: String, + fields: Collection>, + clazz: Class, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ): List { + val named = Parameters.nameFields(fields) + return Custom.list( + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + Parameters.addFields(named), + clazz, + conn, + Results::fromData + ) + } + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param clazz The class of the document to be returned + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the field comparison + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields( + tableName: String, + fields: Collection>, + clazz: Class, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { byFields(tableName, fields, clazz, howMatched, orderBy, it) } + + /** + * Retrieve documents using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param clazz The class of the document to be returned + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun byFields( + tableName: String, + fields: Collection>, + clazz: Class, + howMatched: FieldMatch? = null, + conn: Connection + ) = + byFields(tableName, fields, clazz, howMatched, null, conn) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains( + tableName: String, + criteria: TContains, + clazz: Class, + orderBy: Collection>? = null, + conn: Connection + ) = + Custom.list( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + clazz, + conn, + Results::fromData + ) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON containment query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byContains( + tableName: String, + criteria: TContains, + clazz: Class, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { byContains(tableName, criteria, clazz, orderBy, it) } + + /** + * Retrieve documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param clazz The class of the document to be returned + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, clazz: Class, conn: Connection) = + byContains(tableName, criteria, clazz, null, conn) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath( + tableName: String, + path: String, + clazz: Class, + orderBy: Collection>? = null, + conn: Connection + ) = + Custom.list( + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameter(":path", ParameterType.STRING, path)), + clazz, + conn, + Results::fromData + ) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byJsonPath(tableName: String, path: String, clazz: Class, orderBy: Collection>? = null) = + Configuration.dbConn().use { byJsonPath(tableName, path, clazz, orderBy, it) } + + /** + * Retrieve documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param clazz The class of the document to be returned + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, clazz: Class, conn: Connection) = + byJsonPath(tableName, path, clazz, null, conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param clazz The class of the document to be returned + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return An `Optional` item, with the first document matching the field comparison if found + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByFields( + tableName: String, + fields: Collection>, + clazz: Class, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ): Optional { + val named = Parameters.nameFields(fields) + return Custom.single( + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + Parameters.addFields(named), + clazz, + conn, + Results::fromData + ) + } + + /** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param clazz The class of the document to be returned + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Optional` item, with the first document matching the field comparison if found + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun firstByFields( + tableName: String, + fields: Collection>, + clazz: Class, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { firstByFields(tableName, fields, clazz, howMatched, orderBy, it) } + + /** + * Retrieve the first document using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param clazz The class of the document to be returned + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @return An `Optional` item, with the first document matching the field comparison if found + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByFields( + tableName: String, + fields: Collection>, + clazz: Class, + howMatched: FieldMatch? = null, + conn: Connection + ) = + firstByFields(tableName, fields, clazz, howMatched, null, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return An `Optional` item, with the first document matching the JSON containment query if found + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByContains( + tableName: String, + criteria: TContains, + clazz: Class, + orderBy: Collection>? = null, + conn: Connection + ) = + Custom.single( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + clazz, + conn, + Results::fromData + ) + + /** + * Retrieve the first document using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param clazz The class of the document to be returned + * @param conn The connection over which documents should be retrieved + * @return An `Optional` item, with the first document matching the JSON containment query if found + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByContains( + tableName: String, + criteria: TContains, + clazz: Class, + conn: Connection + ) = + firstByContains(tableName, criteria, clazz, null, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Optional` item, with the first document matching the JSON containment query if found + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun firstByContains( + tableName: String, + criteria: TContains, + clazz: Class, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { firstByContains(tableName, criteria, clazz, orderBy, it) } + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return An `Optional` item, with the first document matching the JSON Path match query if found + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByJsonPath( + tableName: String, + path: String, + clazz: Class, + orderBy: Collection>? = null, + conn: Connection + ) = + Custom.single( + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameter(":path", ParameterType.STRING, path)), + clazz, + conn, + Results::fromData + ) + + /** + * Retrieve the first document using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param clazz The class of the document to be returned + * @param conn The connection over which documents should be retrieved + * @return An `Optional` item, with the first document matching the JSON Path match query if found + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByJsonPath(tableName: String, path: String, clazz: Class, conn: Connection) = + firstByJsonPath(tableName, path, clazz, null, conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Optional` item, with the first document matching the JSON Path match query if found + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun firstByJsonPath( + tableName: String, + path: String, + clazz: Class, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { firstByJsonPath(tableName, path, clazz, orderBy, it) } +} diff --git a/src/core/src/main/kotlin/java/Json.kt b/src/core/src/main/kotlin/java/Json.kt new file mode 100644 index 0000000..d41aa51 --- /dev/null +++ b/src/core/src/main/kotlin/java/Json.kt @@ -0,0 +1,877 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.query.orderBy +import java.io.PrintWriter +import java.sql.Connection +import kotlin.jvm.Throws + +/** + * Functions to find and retrieve documents, returning them as JSON strings + */ +object Json { + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents from the given table + * @throws DocumentException If query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun all(tableName: String, orderBy: Collection>? = null, conn: Connection) = + Custom.jsonArray( + FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(), + conn, + Results::jsonFromData + ) + + /** + * Retrieve all documents in the given table (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents from the given table + * @throws DocumentException If no connection string has been set, or if query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun all(tableName: String, orderBy: Collection>? = null) = + Configuration.dbConn().use { all(tableName, orderBy, it) } + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents from the given table + * @throws DocumentException If query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun all(tableName: String, conn: Connection) = + all(tableName, null, conn) + + /** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeAll(tableName: String, writer: PrintWriter, orderBy: Collection>? = null, conn: Connection) = + Custom.writeJsonArray( + FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(), + writer, + conn, + Results::jsonFromData + ) + + /** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun writeAll(tableName: String, writer: PrintWriter, orderBy: Collection>? = null) = + Configuration.dbConn().use { writeAll(tableName, writer, orderBy, it) } + + /** + * Write all documents in the given table to the given `PrintWriter` + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If query execution fails + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeAll(tableName: String, writer: PrintWriter, conn: Connection) = + writeAll(tableName, writer, null, conn) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @param conn The connection over which documents should be retrieved + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, conn: Connection) = + Custom.jsonSingle( + FindQuery.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + conn, + Results::jsonFromData + ) + + /** + * Retrieve a document by its ID (creates connection) + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey) = + Configuration.dbConn().use { byId(tableName, docId, it) } + + /** + * Write a document to the given `PrintWriter` by its ID (writes empty object if not found) + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeById(tableName: String, writer: PrintWriter, docId: TKey, conn: Connection) = + writer.write(byId(tableName, docId, conn)) + + /** + * Write a document to the given `PrintWriter` by its ID (writes empty object if not found; creates connection) + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeById(tableName: String, writer: PrintWriter, docId: TKey) = + Configuration.dbConn().use { writeById(tableName, writer, docId, it) } + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ): String { + val named = Parameters.nameFields(fields) + return Custom.jsonArray( + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + Parameters.addFields(named), + conn, + Results::jsonFromData + ) + } + + /** + * Retrieve documents using a field comparison, ordering results by the given fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { byFields(tableName, fields, howMatched, orderBy, it) } + + /** + * Retrieve documents using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) = + byFields(tableName, fields, howMatched, null, conn) + + /** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ) { + val named = Parameters.nameFields(fields) + Custom.writeJsonArray( + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + Parameters.addFields(named), + writer, + conn, + Results::jsonFromData + ) + } + + /** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields + * (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun writeByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { writeByFields(tableName, writer, fields, howMatched, orderBy, it) } + + /** + * Write documents to the given `PrintWriter` using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ) = writeByFields(tableName, writer, fields, howMatched, null, conn) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = Custom.jsonArray( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + conn, + Results::jsonFromData + ) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byContains(tableName: String, criteria: TContains, orderBy: Collection>? = null) = + Configuration.dbConn().use { byContains(tableName, criteria, orderBy, it) } + + /** + * Retrieve documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, conn: Connection) = + byContains(tableName, criteria, null, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = Custom.writeJsonArray( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + writer, + conn, + Results::jsonFromData + ) + + /** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun writeByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { writeByContains(tableName, writer, criteria, orderBy, it) } + + /** + * Write documents to the given `PrintWriter` using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeByContains(tableName: String, writer: PrintWriter, criteria: TContains, conn: Connection) = + writeByContains(tableName, writer, criteria, null, conn) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, orderBy: Collection>? = null, conn: Connection) = + Custom.jsonArray( + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameter(":path", ParameterType.STRING, path)), + conn, + Results::jsonFromData + ) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + Configuration.dbConn().use { byJsonPath(tableName, path, orderBy, it) } + + /** + * Retrieve documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, conn: Connection) = + byJsonPath(tableName, path, null, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null, + conn: Connection + ) = Custom.writeJsonArray( + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameter(":path", ParameterType.STRING, path)), + writer, + conn, + Results::jsonFromData + ) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun writeByJsonPath(tableName: String, writer: PrintWriter, path: String, orderBy: Collection>? = null) = + Configuration.dbConn().use { writeByJsonPath(tableName, writer, path, orderBy, it) } + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeByJsonPath(tableName: String, writer: PrintWriter, path: String, conn: Connection) = + writeByJsonPath(tableName, writer, path, null, conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ): String { + val named = Parameters.nameFields(fields) + return Custom.jsonSingle( + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + Parameters.addFields(named), + conn, + Results::jsonFromData + ) + } + + /** + * Retrieve the first document using a field comparison and optional ordering fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { firstByFields(tableName, fields, howMatched, orderBy, it) } + + /** + * Retrieve the first document using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ) = firstByFields(tableName, fields, howMatched, null, conn) + + /** + * Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeFirstByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ) = writer.write(firstByFields(tableName, fields, howMatched, orderBy, conn)) + + /** + * Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields + * (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun writeFirstByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { writeFirstByFields(tableName, writer, fields, howMatched, orderBy, it) } + + /** + * Write the first document to the given `PrintWriter` using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeFirstByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ) = writeFirstByFields(tableName, writer, fields, howMatched, null, conn) + + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = Custom.jsonSingle( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + conn, + Results::jsonFromData + ) + + /** + * Retrieve the first document using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByContains(tableName: String, criteria: TContains, conn: Connection) = + firstByContains(tableName, criteria, null, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun firstByContains(tableName: String, criteria: TContains, orderBy: Collection>? = null) = + Configuration.dbConn().use { firstByContains(tableName, criteria, orderBy, it) } + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeFirstByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = writer.write(firstByContains(tableName, criteria, orderBy, conn)) + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeFirstByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + conn: Connection + ) = writeFirstByContains(tableName, writer, criteria, null, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun writeFirstByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { writeFirstByContains(tableName, writer, criteria, orderBy, it) } + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByJsonPath(tableName: String, path: String, orderBy: Collection>? = null, conn: Connection) = + Custom.jsonSingle( + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameter(":path", ParameterType.STRING, path)), + conn, + Results::jsonFromData + ) + + /** + * Retrieve the first document using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun firstByJsonPath(tableName: String, path: String, conn: Connection) = + firstByJsonPath(tableName, path, null, conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun firstByJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + Configuration.dbConn().use { firstByJsonPath(tableName, path, orderBy, it) } + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeFirstByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null, + conn: Connection + ) = writer.write(firstByJsonPath(tableName, path, orderBy, conn)) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun writeFirstByJsonPath(tableName: String, writer: PrintWriter, path: String, conn: Connection) = + writeFirstByJsonPath(tableName, writer, path, null, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun writeFirstByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { writeFirstByJsonPath(tableName, writer, path, orderBy, it) } +} diff --git a/src/core/src/main/kotlin/java/NullDocumentSerializer.kt b/src/core/src/main/kotlin/java/NullDocumentSerializer.kt new file mode 100644 index 0000000..233a6ce --- /dev/null +++ b/src/core/src/main/kotlin/java/NullDocumentSerializer.kt @@ -0,0 +1,21 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.DocumentSerializer + +/** + * A serializer that tells the user to implement another one + * + * This is the default serializer, so the library itself does not have any firm dependency on any JSON serialization + * library. The tests for this library (will) have an example Jackson-based serializer. + */ +class NullDocumentSerializer : DocumentSerializer { + + override fun serialize(document: TDoc): String { + TODO("Replace this serializer in DocumentConfig.serializer") + } + + override fun deserialize(json: String, clazz: Class): TDoc { + TODO("Replace this serializer in DocumentConfig.serializer") + } + +} diff --git a/src/core/src/main/kotlin/java/Parameters.kt b/src/core/src/main/kotlin/java/Parameters.kt new file mode 100644 index 0000000..b45b64a --- /dev/null +++ b/src/core/src/main/kotlin/java/Parameters.kt @@ -0,0 +1,131 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.ParameterName +import java.sql.Connection +import java.sql.PreparedStatement +import java.sql.SQLException +import kotlin.jvm.Throws + +/** + * Functions to assist with the creation and implementation of parameters for SQL queries + * + * @author Daniel J. Summers + */ +object Parameters { + + /** + * Assign parameter names to any fields that do not have them assigned + * + * @param fields The collection of fields to be named + * @return The collection of fields with parameter names assigned + */ + @JvmStatic + fun nameFields(fields: Collection>): Collection> { + val name = ParameterName() + return fields.map { + if (it.parameterName.isNullOrEmpty() && !listOf(Op.EXISTS, Op.NOT_EXISTS).contains(it.comparison.op)) { + it.withParameterName(name.derive(null)) + } else { + it + } + } + } + + /** + * Create a parameter by encoding a JSON object + * + * @param name The parameter name + * @param value The object to be encoded as JSON + * @return A parameter with the value encoded + */ + @JvmStatic + fun json(name: String, value: T) = + Parameter(name, ParameterType.JSON, DocumentConfig.serializer.serialize(value)) + + /** + * Add field parameters to the given set of parameters + * + * @param fields The fields being compared in the query + * @param existing Any existing parameters for the query (optional, defaults to empty collection) + * @return A collection of parameters for the query + */ + @JvmStatic + fun addFields(fields: Collection>, existing: MutableCollection> = mutableListOf()) = + fields.fold(existing) { acc, field -> field.appendParameter(acc) } + + /** + * Replace the parameter names in the query with question marks + * + * @param query The query with named placeholders + * @param parameters The parameters for the query + * @return The query, with name parameters changed to `?`s + */ + @JvmStatic + fun replaceNamesInQuery(query: String, parameters: Collection>) = + parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") } + + /** + * Apply the given parameters to the given query, returning a prepared statement + * + * @param conn The active JDBC connection + * @param query The query + * @param parameters The parameters for the query + * @return A `PreparedStatement` with the parameter names replaced with `?` and parameter values bound + * @throws DocumentException If parameter names are invalid or number value types are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun apply(conn: Connection, query: String, parameters: Collection>): PreparedStatement { + + if (parameters.isEmpty()) return try { + conn.prepareStatement(query) + } catch (ex: SQLException) { + throw DocumentException("Error preparing no-parameter query: ${ex.message}", ex) + } + + val replacements = mutableListOf>>() + parameters.sortedByDescending { it.name.length }.forEach { + var startPos = query.indexOf(it.name) + while (startPos > -1) { + replacements.add(Pair(startPos, it)) + startPos = query.indexOf(it.name, startPos + it.name.length + 1) + } + } + + return try { + replaceNamesInQuery(query, parameters) + //.also(::println) + .let { conn.prepareStatement(it) } + .also { stmt -> + replacements.sortedBy { it.first } + .map { it.second } + .forEachIndexed { index, param -> param.bind(stmt, index + 1) } + } + } catch (ex: SQLException) { + throw DocumentException("Error creating query / binding parameters: ${ex.message}", ex) + } + } + + /** + * Create parameters for field names to be removed from a document + * + * @param names The names of the fields to be removed + * @param parameterName The parameter name to use for the query + * @return A list of parameters to use for building the query + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun fieldNames(names: Collection, parameterName: String = ":name"): MutableCollection> = + when (Configuration.dialect("generate field name parameters")) { + Dialect.POSTGRESQL -> mutableListOf( + Parameter(parameterName, ParameterType.STRING, names.joinToString(",").let { "{$it}" }) + ) + + Dialect.SQLITE -> names.mapIndexed { index, name -> + Parameter("$parameterName$index", ParameterType.STRING, name) + }.toMutableList() + } +} diff --git a/src/core/src/main/kotlin/java/Patch.kt b/src/core/src/main/kotlin/java/Patch.kt new file mode 100644 index 0000000..1a03d00 --- /dev/null +++ b/src/core/src/main/kotlin/java/Patch.kt @@ -0,0 +1,155 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.PatchQuery +import java.sql.Connection +import kotlin.jvm.Throws + +/** + * Functions to patch (partially update) documents + */ +object Patch { + + /** + * Patch a document by its ID + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, patch: TPatch, conn: Connection) = + Custom.nonQuery( + PatchQuery.byId(tableName, docId), + Parameters.addFields( + listOf(Field.equal(Configuration.idField, docId, ":id")), + mutableListOf(Parameters.json(":data", patch)) + ), + conn + ) + + /** + * Patch a document by its ID + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, patch: TPatch) = + Configuration.dbConn().use { byId(tableName, docId, patch, it) } + + /** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun byFields( + tableName: String, + fields: Collection>, + patch: TPatch, + howMatched: FieldMatch? = null, + conn: Connection + ) { + val named = Parameters.nameFields(fields) + Custom.nonQuery( + PatchQuery.byFields(tableName, named, howMatched), + Parameters.addFields(named, mutableListOf(Parameters.json(":data", patch))), + conn + ) + } + + /** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields( + tableName: String, + fields: Collection>, + patch: TPatch, + howMatched: FieldMatch? = null + ) = + Configuration.dbConn().use { byFields(tableName, fields, patch, howMatched, it) } + + /** + * Patch documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, patch: TPatch, conn: Connection) = + Custom.nonQuery( + PatchQuery.byContains(tableName), + listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch)), + conn + ) + + /** + * Patch documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, patch: TPatch) = + Configuration.dbConn().use { byContains(tableName, criteria, patch, it) } + + /** + * Patch documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, patch: TPatch, conn: Connection) = + Custom.nonQuery( + PatchQuery.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch)), + conn + ) + + /** + * Patch documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, patch: TPatch) = + Configuration.dbConn().use { byJsonPath(tableName, path, patch, it) } +} diff --git a/src/core/src/main/kotlin/java/RemoveFields.kt b/src/core/src/main/kotlin/java/RemoveFields.kt new file mode 100644 index 0000000..540d504 --- /dev/null +++ b/src/core/src/main/kotlin/java/RemoveFields.kt @@ -0,0 +1,178 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.RemoveFieldsQuery +import java.sql.Connection +import kotlin.jvm.Throws + +/** + * Functions to remove fields from documents + */ +object RemoveFields { + + /** + * Translate field paths to JSON paths for SQLite queries + * + * @param parameters The parameters for the specified fields + * @return The parameters for the specified fields, translated if used for SQLite + */ + private fun translatePath(parameters: MutableCollection>): MutableCollection> { + val dialect = Configuration.dialect("remove fields") + return when (dialect) { + Dialect.POSTGRESQL -> parameters + Dialect.SQLITE -> parameters.map { Parameter(it.name, it.type, "$.${it.value}") }.toMutableList() + } + } + + /** + * Remove fields from a document by its ID + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, toRemove: Collection, conn: Connection) { + val nameParams = Parameters.fieldNames(toRemove) + Custom.nonQuery( + RemoveFieldsQuery.byId(tableName, nameParams, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id")), translatePath(nameParams)), + conn + ) + } + + /** + * Remove fields from a document by its ID + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + * @throws DocumentException If no connection string has been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun byId(tableName: String, docId: TKey, toRemove: Collection) = + Configuration.dbConn().use { byId(tableName, docId, toRemove, it) } + + /** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + fun byFields( + tableName: String, + fields: Collection>, + toRemove: Collection, + howMatched: FieldMatch? = null, + conn: Connection + ) { + val named = Parameters.nameFields(fields) + val nameParams = Parameters.fieldNames(toRemove) + Custom.nonQuery( + RemoveFieldsQuery.byFields(tableName, nameParams, named, howMatched), + Parameters.addFields(named, translatePath(nameParams)), + conn + ) + } + + /** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields( + tableName: String, + fields: Collection>, + toRemove: Collection, + howMatched: FieldMatch? = null + ) = + Configuration.dbConn().use { byFields(tableName, fields, toRemove, howMatched, it) } + + /** + * Remove fields from documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains( + tableName: String, + criteria: TContains, + toRemove: Collection, + conn: Connection + ) { + val nameParams = Parameters.fieldNames(toRemove) + Custom.nonQuery( + RemoveFieldsQuery.byContains(tableName, nameParams), + listOf(Parameters.json(":criteria", criteria), *nameParams.toTypedArray()), + conn + ) + } + + /** + * Remove fields from documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, toRemove: Collection) = + Configuration.dbConn().use { byContains(tableName, criteria, toRemove, it) } + + /** + * Remove fields from documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, toRemove: Collection, conn: Connection) { + val nameParams = Parameters.fieldNames(toRemove) + Custom.nonQuery( + RemoveFieldsQuery.byJsonPath(tableName, nameParams), + listOf(Parameter(":path", ParameterType.STRING, path), *nameParams.toTypedArray()), + conn + ) + } + + /** + * Remove fields from documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, toRemove: Collection) = + Configuration.dbConn().use { byJsonPath(tableName, path, toRemove, it) } +} diff --git a/src/core/src/main/kotlin/java/Results.kt b/src/core/src/main/kotlin/java/Results.kt new file mode 100644 index 0000000..3f49229 --- /dev/null +++ b/src/core/src/main/kotlin/java/Results.kt @@ -0,0 +1,167 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import java.io.PrintWriter +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.SQLException + +/** + * Functions to create results from queries + */ +object Results { + + /** + * Create a domain item from a document, specifying the field in which the document is found + * + * @param field The field name containing the JSON document + * @param rs A `ResultSet` set to the row with the document to be constructed + * @param clazz The class of the document to be returned + * @return The constructed domain item + */ + @JvmStatic + fun fromDocument(field: String, rs: ResultSet, clazz: Class) = + DocumentConfig.serializer.deserialize(rs.getString(field), clazz) + + /** + * Create a domain item from a document + * + * @param rs A `ResultSet` set to the row with the document to be constructed< + * @param clazz The class of the document to be returned + * @return The constructed domain item + */ + @JvmStatic + fun fromData(rs: ResultSet, clazz: Class) = + fromDocument("data", rs, clazz) + + /** + * Create a list of items for the results of the given command, using the specified mapping function + * + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to domain class instance + * @param clazz The class of the document to be returned + * @return A list of items from the query's result + * @throws DocumentException If there is a problem executing the query (unchecked) + */ + @JvmStatic + fun toCustomList( + stmt: PreparedStatement, clazz: Class, mapFunc: (ResultSet, Class) -> TDoc + ) = + try { + stmt.executeQuery().use { + val results = mutableListOf() + while (it.next()) { + results.add(mapFunc(it, clazz)) + } + results.toList() + } + } catch (ex: SQLException) { + throw DocumentException("Error retrieving documents from query: ${ex.message}", ex) + } + + /** + * Extract a count from the first column + * + * @param rs A `ResultSet` set to the row with the count to retrieve + * @param clazz The type parameter (ignored; this always returns `Long`) + * @return The count from the row + * @throws DocumentException If the dialect has not been set (unchecked) + */ + @JvmStatic + fun toCount(rs: ResultSet, clazz: Class<*>) = + when (Configuration.dialect()) { + Dialect.POSTGRESQL -> rs.getInt("it").toLong() + Dialect.SQLITE -> rs.getLong("it") + } + + /** + * Extract a true/false value from the first column + * + * @param rs A `ResultSet` set to the row with the true/false value to retrieve + * @param clazz The type parameter (ignored; this always returns `Boolean`) + * @return The true/false value from the row + * @throws DocumentException If the dialect has not been set (unchecked) + */ + @JvmStatic + fun toExists(rs: ResultSet, clazz: Class<*>) = + when (Configuration.dialect()) { + Dialect.POSTGRESQL -> rs.getBoolean("it") + Dialect.SQLITE -> toCount(rs, Long::class.java) > 0L + } + + /** + * Retrieve the JSON text of a document, specifying the field in which the document is found + * + * @param field The field name containing the JSON document + * @param rs A `ResultSet` set to the row with the document to be constructed + * @return The JSON text of the document + */ + @JvmStatic + fun jsonFromDocument(field: String, rs: ResultSet) = + rs.getString(field) ?: "{}" + + /** + * Retrieve the JSON text of a document, specifying the field in which the document is found + * + * @param rs A `ResultSet` set to the row with the document to be constructed + * @return The JSON text of the document + */ + @JvmStatic + fun jsonFromData(rs: ResultSet) = + jsonFromDocument("data", rs) + + /** + * Create a JSON array of items for the results of the given command, using the specified mapping function + * + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to JSON text + * @return A string with a JSON array of documents from the query's result + * @throws DocumentException If there is a problem executing the query (unchecked) + */ + @JvmStatic + fun toJsonArray(stmt: PreparedStatement, mapFunc: (ResultSet) -> String): String = + try { + val results = StringBuilder("[") + stmt.executeQuery().use { + while (it.next()) { + if (results.length > 2) results.append(",") + results.append(mapFunc(it)) + } + } + results.append("]").toString() + } catch (ex: SQLException) { + throw DocumentException("Error retrieving documents from query: ${ex.message}", ex) + } + + /** + * Write a JSON array of items for the results of the given command to the given `PrintWriter`, using the specified + * mapping function + * + * @param writer The writer for the results of the query + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to JSON text + * @return A string with a JSON array of documents from the query's result + * @throws DocumentException If there is a problem executing the query (unchecked) + */ + @JvmStatic + fun writeJsonArray(writer: PrintWriter, stmt: PreparedStatement, mapFunc: (ResultSet) -> String) = + try { + writer.write("[") + stmt.executeQuery().use { + var isFirst = true + while (it.next()) { + if (isFirst) { + isFirst = false + } else { + writer.write(",") + } + writer.write(mapFunc(it)) + } + } + writer.write("]") + } catch (ex: SQLException) { + throw DocumentException("Error writing documents from query: ${ex.message}", ex) + } +} diff --git a/src/core/src/main/kotlin/java/extensions/Connection.kt b/src/core/src/main/kotlin/java/extensions/Connection.kt new file mode 100644 index 0000000..9b13fee --- /dev/null +++ b/src/core/src/main/kotlin/java/extensions/Connection.kt @@ -0,0 +1,885 @@ +@file:JvmName("ConnExt") + +package solutions.bitbadger.documents.java.extensions + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.java.* +import java.io.PrintWriter +import java.sql.Connection +import java.sql.ResultSet +import kotlin.jvm.Throws + +// ~~~ CUSTOM QUERIES ~~~ + +/** + * Execute a query that returns a list of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param clazz The class of the document to be returned + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If parameters are invalid + */ +@Throws(DocumentException::class) +fun Connection.customList( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + mapFunc: (ResultSet, Class) -> TDoc +) = Custom.list(query, parameters, clazz, this, mapFunc) + +/** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ +@Throws(DocumentException::class) +fun Connection.customJsonArray( + query: String, + parameters: Collection> = listOf(), + mapFunc: (ResultSet) -> String +) = Custom.jsonArray(query, parameters, this, mapFunc) + +/** + * Execute a query, writing its JSON array of results to the given `PrintWriter` (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ +@Throws(DocumentException::class) +fun Connection.writeCustomJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + mapFunc: (ResultSet) -> String +) = Custom.writeJsonArray(query, parameters, writer, this, mapFunc) + +/** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param clazz The class of the document to be returned + * @param mapFunc The mapping function between the document and the domain item + * @return The document if one matches the query, `null` otherwise + * @throws DocumentException If parameters are invalid + */ +@Throws(DocumentException::class) +fun Connection.customSingle( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + mapFunc: (ResultSet, Class) -> TDoc +) = Custom.single(query, parameters, clazz, this, mapFunc) + +/** + * Execute a query that returns JSON for one or no documents + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ +@Throws(DocumentException::class) +fun Connection.customJsonSingle( + query: String, + parameters: Collection> = listOf(), + mapFunc: (ResultSet) -> String +) = Custom.jsonSingle(query, parameters, this, mapFunc) + +/** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @throws DocumentException If parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.customNonQuery(query: String, parameters: Collection> = listOf()) = + Custom.nonQuery(query, parameters, this) + +/** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param clazz The class of the document to be returned + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If parameters are invalid + */ +@Throws(DocumentException::class) +fun Connection.customScalar( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + mapFunc: (ResultSet, Class) -> T +) = Custom.scalar(query, parameters, clazz, this, mapFunc) + +// ~~~ DEFINITION QUERIES ~~~ + +/** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + * @throws DocumentException If the dialect is not configured + */ +@Throws(DocumentException::class) +fun Connection.ensureTable(tableName: String) = + Definition.ensureTable(tableName, this) + +/** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed< + * @throws DocumentException If any dependent process does + */ +@Throws(DocumentException::class) +fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection) = + Definition.ensureFieldIndex(tableName, indexName, fields, this) + +/** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) = + Definition.ensureDocumentIndex(tableName, indexType, this) + +// ~~~ DOCUMENT MANIPULATION QUERIES ~~~ + +/** + * Insert a new document + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + * @throws DocumentException If IDs are misconfigured, or if the database command fails + */ +@Throws(DocumentException::class) +fun Connection.insert(tableName: String, document: TDoc) = + Document.insert(tableName, document, this) + +/** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + * @throws DocumentException If the database command fails + */ +@Throws(DocumentException::class) +fun Connection.save(tableName: String, document: TDoc) = + Document.save(tableName, document, this) + +/** + * Update (replace) a document by its ID + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + * @throws DocumentException If no dialect has been configured, or if the database command fails + */ +@Throws(DocumentException::class) +fun Connection.update(tableName: String, docId: TKey, document: TDoc) = + Document.update(tableName, docId, document, this) + +// ~~~ DOCUMENT COUNT QUERIES ~~~ + +/** + * Count all documents in the table + * + * @param tableName The name of the table in which documents should be counted + * @return A count of the documents in the table + * @throws DocumentException If any dependent process does + */ +@Throws(DocumentException::class) +fun Connection.countAll(tableName: String) = + Count.all(tableName, this) + +/** + * Count documents using a field comparison + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return A count of the matching documents in the table + * @throws DocumentException If the dialect has not been configured + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.countByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Count.byFields(tableName, fields, howMatched, this) + +/** + * Count documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.countByContains(tableName: String, criteria: TContains) = + Count.byContains(tableName, criteria, this) + +/** + * Count documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.countByJsonPath(tableName: String, path: String) = + Count.byJsonPath(tableName, path, this) + +// ~~~ DOCUMENT EXISTENCE QUERIES ~~~ + +/** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @return True if the document exists, false if not + * @throws DocumentException If no dialect has been configured + */ +@Throws(DocumentException::class) +fun Connection.existsById(tableName: String, docId: TKey) = + Exists.byId(tableName, docId, this) + +/** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return True if any matching documents exist, false if not + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.existsByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Exists.byFields(tableName, fields, howMatched, this) + +/** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.existsByContains(tableName: String, criteria: TContains) = + Exists.byContains(tableName, criteria, this) + +/** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.existsByJsonPath(tableName: String, path: String) = + Exists.byJsonPath(tableName, path, this) + +// ~~~ DOCUMENT RETRIEVAL QUERIES (Domain Objects) ~~~ + +/** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents from the given table + * @throws DocumentException If query execution fails + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findAll(tableName: String, clazz: Class, orderBy: Collection>? = null) = + Find.all(tableName, clazz, orderBy, this) + +/** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @param clazz The class of the document to be returned + * @return An `Optional` item with the document if it is found + * @throws DocumentException If no dialect has been configured + */ +@Throws(DocumentException::class) +fun Connection.findById(tableName: String, docId: TKey, clazz: Class) = + Find.byId(tableName, docId, clazz, this) + +/** + * Retrieve documents using a field comparison, ordering results by the optional given fields + * + * @param tableName The table from which the document should be retrieved + * @param fields The fields which should be compared + * @param clazz The class of the document to be returned + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findByFields( + tableName: String, + fields: Collection>, + clazz: Class, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Find.byFields(tableName, fields, clazz, howMatched, orderBy, this) + +/** + * Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findByContains( + tableName: String, + criteria: TContains, + clazz: Class, + orderBy: Collection>? = null +) = Find.byContains(tableName, criteria, clazz, orderBy, this) + +/** + * Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findByJsonPath( + tableName: String, + path: String, + clazz: Class, + orderBy: Collection>? = null +) = Find.byJsonPath(tableName, path, clazz, orderBy, this) + +/** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param clazz The class of the document to be returned + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Optional` item, with the first document matching the field comparison if found + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findFirstByFields( + tableName: String, + fields: Collection>, + clazz: Class, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Find.firstByFields(tableName, fields, clazz, howMatched, orderBy, this) + +/** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Optional` item, with the first document matching the JSON containment query if found + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findFirstByContains( + tableName: String, + criteria: TContains, + clazz: Class, + orderBy: Collection>? = null +) = Find.firstByContains(tableName, criteria, clazz, orderBy, this) + +/** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param clazz The class of the document to be returned + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Optional` item, with the first document matching the JSON Path match query if found + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findFirstByJsonPath( + tableName: String, + path: String, + clazz: Class, + orderBy: Collection>? = null +) = Find.firstByJsonPath(tableName, path, clazz, orderBy, this) + +// ~~~ DOCUMENT RETRIEVAL QUERIES (Raw JSON) ~~~ + +/** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents from the given table + * @throws DocumentException If query execution fails + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.jsonAll(tableName: String, orderBy: Collection>? = null) = + Json.all(tableName, orderBy, this) + +/** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no dialect has been configured + */ +@Throws(DocumentException::class) +fun Connection.jsonById(tableName: String, docId: TKey) = + Json.byId(tableName, docId, this) + +/** + * Retrieve documents using a field comparison, ordering results by the optional given fields + * + * @param tableName The table from which the document should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.jsonByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Json.byFields(tableName, fields, howMatched, orderBy, this) + +/** + * Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.jsonByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null +) = Json.byContains(tableName, criteria, orderBy, this) + +/** + * Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.jsonByJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + Json.byJsonPath(tableName, path, orderBy, this) + +/** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.jsonFirstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Json.firstByFields(tableName, fields, howMatched, orderBy, this) + +/** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.jsonFirstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null +) = Json.firstByContains(tableName, criteria, orderBy, this) + +/** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.jsonFirstByJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + Json.firstByJsonPath(tableName, path, orderBy, this) + +// ~~~ DOCUMENT RETRIEVAL QUERIES (Write raw JSON to output) ~~~ + +/** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If query execution fails + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.writeJsonAll(tableName: String, writer: PrintWriter, orderBy: Collection>? = null) = + Json.writeAll(tableName, writer, orderBy, this) + +/** + * Write a document to the given `PrintWriter` by its ID + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @throws DocumentException If no dialect has been configured + */ +@Throws(DocumentException::class) +fun Connection.writeJsonById(tableName: String, writer: PrintWriter, docId: TKey) = + Json.writeById(tableName, writer, docId, this) + +/** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the optional given fields + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.writeJsonByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Json.writeByFields(tableName, writer, fields, howMatched, orderBy, this) + +/** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the optional given + * fields (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.writeJsonByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null +) = Json.writeByContains(tableName, writer, criteria, orderBy, this) + +/** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the optional given + * fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.writeJsonByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null +) = Json.writeByJsonPath(tableName, writer, path, orderBy, this) + +/** + * Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.writeJsonFirstByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Json.writeFirstByFields(tableName, writer, fields, howMatched, orderBy, this) + +/** + * Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.writeJsonFirstByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null +) = Json.writeFirstByContains(tableName, writer, criteria, orderBy, this) + +/** + * Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.writeJsonFirstByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null +) = Json.writeFirstByJsonPath(tableName, writer, path, orderBy, this) + +// ~~~ DOCUMENT PATCH (PARTIAL UPDATE) QUERIES ~~~ + +/** + * Patch a document by its ID + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If no dialect has been configured + */ +@Throws(DocumentException::class) +fun Connection.patchById(tableName: String, docId: TKey, patch: TPatch) = + Patch.byId(tableName, docId, patch, this) + +/** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.patchByFields( + tableName: String, + fields: Collection>, + patch: TPatch, + howMatched: FieldMatch? = null +) = Patch.byFields(tableName, fields, patch, howMatched, this) + +/** + * Patch documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.patchByContains( + tableName: String, + criteria: TContains, + patch: TPatch +) = Patch.byContains(tableName, criteria, patch, this) + +/** + * Patch documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.patchByJsonPath(tableName: String, path: String, patch: TPatch) = + Patch.byJsonPath(tableName, path, patch, this) + +// ~~~ DOCUMENT FIELD REMOVAL QUERIES ~~~ + +/** + * Remove fields from a document by its ID + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + * @throws DocumentException If no dialect has been configured + */ +@Throws(DocumentException::class) +fun Connection.removeFieldsById(tableName: String, docId: TKey, toRemove: Collection) = + RemoveFields.byId(tableName, docId, toRemove, this) + +/** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.removeFieldsByFields( + tableName: String, + fields: Collection>, + toRemove: Collection, + howMatched: FieldMatch? = null +) = RemoveFields.byFields(tableName, fields, toRemove, howMatched, this) + +/** + * Remove fields from documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.removeFieldsByContains( + tableName: String, + criteria: TContains, + toRemove: Collection +) = RemoveFields.byContains(tableName, criteria, toRemove, this) + +/** + * Remove fields from documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.removeFieldsByJsonPath(tableName: String, path: String, toRemove: Collection) = + RemoveFields.byJsonPath(tableName, path, toRemove, this) + +// ~~~ DOCUMENT DELETION QUERIES ~~~ + +/** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + * @throws DocumentException If no dialect has been configured + */ +@Throws(DocumentException::class) +fun Connection.deleteById(tableName: String, docId: TKey) = + Delete.byId(tableName, docId, this) + +/** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.deleteByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Delete.byFields(tableName, fields, howMatched, this) + +/** + * Delete documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.deleteByContains(tableName: String, criteria: TContains) = + Delete.byContains(tableName, criteria, this) + +/** + * Delete documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @throws DocumentException If called on a SQLite connection + */ +@Throws(DocumentException::class) +fun Connection.deleteByJsonPath(tableName: String, path: String) = + Delete.byJsonPath(tableName, path, this) diff --git a/src/core/src/main/kotlin/query/CountQuery.kt b/src/core/src/main/kotlin/query/CountQuery.kt new file mode 100644 index 0000000..6439f6c --- /dev/null +++ b/src/core/src/main/kotlin/query/CountQuery.kt @@ -0,0 +1,62 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import kotlin.jvm.Throws +import solutions.bitbadger.documents.query.byFields as byFieldsBase + +/** + * Functions to count documents + */ +object CountQuery { + + /** + * Query to count all documents in a table + * + * @param tableName The table in which to count documents (may include schema) + * @return A query to count documents + */ + @JvmStatic + fun all(tableName: String) = + "SELECT COUNT(*) AS it FROM $tableName" + + /** + * Query to count documents matching the given fields + * + * @param tableName The table in which to count documents (may include schema) + * @param fields The field comparisons for the count + * @param howMatched How fields should be compared (optional, defaults to ALL) + * @return A query to count documents matching the given fields + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + byFieldsBase(all(tableName), fields, howMatched) + + /** + * Query to count documents via JSON containment (PostgreSQL only) + * + * @param tableName The table in which to count documents (may include schema) + * @return A query to count documents via JSON containment + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String) = + statementWhere(all(tableName), Where.jsonContains()) + + /** + * Query to count documents via a JSON path match (PostgreSQL only) + * + * @param tableName The table in which to count documents (may include schema) + * @return A query to count documents via a JSON path match + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String) = + statementWhere(all(tableName), Where.jsonPathMatches()) +} diff --git a/src/core/src/main/kotlin/query/DefinitionQuery.kt b/src/core/src/main/kotlin/query/DefinitionQuery.kt new file mode 100644 index 0000000..24876bb --- /dev/null +++ b/src/core/src/main/kotlin/query/DefinitionQuery.kt @@ -0,0 +1,108 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.* +import kotlin.jvm.Throws + +/** + * Functions to create queries to define tables and indexes + */ +object DefinitionQuery { + + /** + * SQL statement to create a document table + * + * @param tableName The name of the table to create (may include schema) + * @param dataType The type of data for the column (`JSON`, `JSONB`, etc.) + * @return A query to create a document table + */ + @JvmStatic + fun ensureTableFor(tableName: String, dataType: String) = + "CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)" + + /** + * SQL statement to create a document table in the current dialect + * + * @param tableName The name of the table to create (may include schema) + * @param dialect The dialect to generate (optional, used in place of current) + * @return A query to create a document table + * @throws DocumentException If the dialect is neither provided nor configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun ensureTable(tableName: String, dialect: Dialect? = null) = + when (dialect ?: Configuration.dialect("create table creation query")) { + Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB") + Dialect.SQLITE -> ensureTableFor(tableName, "TEXT") + } + + /** + * Split a schema and table name + * + * @param tableName The name of the table, possibly with a schema + * @return A pair with the first item as the schema and the second as the table name + */ + private fun splitSchemaAndTable(tableName: String) = + tableName.split('.').let { if (it.size == 1) Pair("", tableName) else Pair(it[0], it[1]) } + + /** + * SQL statement to create an index on one or more fields in a JSON document + * + * @param tableName The table on which an index should be created (may include schema) + * @param indexName The name of the index to be created + * @param fields One or more fields to include in the index + * @param dialect The SQL dialect to use when creating this index (optional, used in place of current) + * @return A query to create the field index + * @throws DocumentException If the dialect is neither provided nor configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun ensureIndexOn( + tableName: String, + indexName: String, + fields: Collection, + dialect: Dialect? = null + ): String { + val (_, tbl) = splitSchemaAndTable(tableName) + val mode = dialect ?: Configuration.dialect("create index $tbl.$indexName") + val jsonFields = fields.joinToString(", ") { + val parts = it.split(' ') + val direction = if (parts.size > 1) " ${parts[1]}" else "" + "(" + Field.nameToPath(parts[0], mode, FieldFormat.SQL) + ")$direction" + } + return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)" + } + + /** + * SQL statement to create a key index for a document table + * + * @param tableName The table on which a key index should be created (may include schema) + * @param dialect The SQL dialect to use when creating this index (optional, used in place of current) + * @return A query to create the key index + * @throws DocumentException If the dialect is neither provided nor configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun ensureKey(tableName: String, dialect: Dialect? = null) = + ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX") + + /** + * Create a document-wide index on a table (PostgreSQL only) + * + * @param tableName The name of the table on which the document index should be created + * @param indexType The type of index to be created + * @return The SQL statement to create an index on JSON documents in the specified table + * @throws DocumentException If the database mode is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun ensureDocumentIndexOn(tableName: String, indexType: DocumentIndex): String { + if (Configuration.dialect("create document index query") != Dialect.POSTGRESQL) { + throw DocumentException("'Document indexes are only supported on PostgreSQL") + } + val (_, tbl) = splitSchemaAndTable(tableName) + return "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tableName USING GIN (data${indexType.sql})" + } +} diff --git a/src/core/src/main/kotlin/query/DeleteQuery.kt b/src/core/src/main/kotlin/query/DeleteQuery.kt new file mode 100644 index 0000000..7dd1b07 --- /dev/null +++ b/src/core/src/main/kotlin/query/DeleteQuery.kt @@ -0,0 +1,76 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import kotlin.jvm.Throws +import solutions.bitbadger.documents.query.byFields as byFieldsBase +import solutions.bitbadger.documents.query.byId as byIdBase + +/** + * Functions to delete documents + */ +object DeleteQuery { + + /** + * Query to delete documents from a table + * + * @param tableName The table in which documents should be deleted (may include schema) + * @return A query to delete documents + */ + private fun delete(tableName: String) = + "DELETE FROM $tableName" + + /** + * Query to delete a document by its ID + * + * @param tableName The table from which documents should be deleted (may include schema) + * @param docId The ID of the document (optional, used for type checking) + * @return A query to delete a document by its ID + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byId(tableName: String, docId: TKey? = null) = + byIdBase(delete(tableName), docId) + + /** + * Query to delete documents matching the given fields + * + * @param tableName The table from which documents should be deleted (may include schema) + * @param fields The field comparisons for documents to be deleted + * @param howMatched How fields should be compared (optional, defaults to ALL) + * @return A query to delete documents matching for the given fields + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + byFieldsBase(delete(tableName), fields, howMatched) + + /** + * Query to delete documents via JSON containment (PostgreSQL only) + * + * @param tableName The table from which documents should be deleted (may include schema) + * @return A query to delete documents via JSON containment + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String) = + statementWhere(delete(tableName), Where.jsonContains()) + + /** + * Query to delete documents via a JSON path match (PostgreSQL only) + * + * @param tableName The table from which documents should be deleted (may include schema) + * @return A query to delete documents via a JSON path match + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String) = + statementWhere(delete(tableName), Where.jsonPathMatches()) +} diff --git a/src/core/src/main/kotlin/query/DocumentQuery.kt b/src/core/src/main/kotlin/query/DocumentQuery.kt new file mode 100644 index 0000000..34834c6 --- /dev/null +++ b/src/core/src/main/kotlin/query/DocumentQuery.kt @@ -0,0 +1,68 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import kotlin.jvm.Throws + +/** + * Functions for document-level operations + */ +object DocumentQuery { + + /** + * Query to insert a document + * + * @param tableName The table into which to insert (may include schema) + * @return A query to insert a document + * @throws DocumentException If the dialect is not configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun insert(tableName: String, autoId: AutoId? = null): String { + val id = Configuration.idField + val values = when (Configuration.dialect("create INSERT statement")) { + Dialect.POSTGRESQL -> when (autoId ?: AutoId.DISABLED) { + AutoId.DISABLED -> ":data" + AutoId.NUMBER -> ":data::jsonb || ('{\"$id\":' || " + + "(SELECT COALESCE(MAX((data->>'$id')::numeric), 0) + 1 " + + "FROM $tableName) || '}')::jsonb" + AutoId.UUID -> ":data::jsonb || '{\"$id\":\"${AutoId.generateUUID()}\"}'" + AutoId.RANDOM_STRING -> ":data::jsonb || '{\"$id\":\"${AutoId.generateRandomString()}\"}'" + } + Dialect.SQLITE -> when (autoId ?: AutoId.DISABLED) { + AutoId.DISABLED -> ":data" + AutoId.NUMBER -> "json_set(:data, '$.$id', " + + "(SELECT coalesce(max(data->>'$id'), 0) + 1 FROM $tableName))" + AutoId.UUID -> "json_set(:data, '$.$id', '${AutoId.generateUUID()}')" + AutoId.RANDOM_STRING -> "json_set(:data, '$.$id', '${AutoId.generateRandomString()}')" + } + } + return "INSERT INTO $tableName VALUES ($values)" + } + + /** + * Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table into which to save (may include schema) + * @return A query to save a document + * @throws DocumentException If the dialect is not configured + */ + @Throws(DocumentException::class) + @JvmStatic + fun save(tableName: String) = + insert(tableName, AutoId.DISABLED) + + " ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data" + + /** + * Query to update (replace) a document (this query has no `WHERE` clause) + * + * @param tableName The table in which documents should be replaced (may include schema) + * @return A query to update documents + */ + @JvmStatic + fun update(tableName: String) = + "UPDATE $tableName SET data = :data" +} diff --git a/src/core/src/main/kotlin/query/ExistsQuery.kt b/src/core/src/main/kotlin/query/ExistsQuery.kt new file mode 100644 index 0000000..e15f5ca --- /dev/null +++ b/src/core/src/main/kotlin/query/ExistsQuery.kt @@ -0,0 +1,75 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import kotlin.jvm.Throws + +/** + * Functions to check for document existence + */ +object ExistsQuery { + + /** + * Query to check for document existence in a table + * + * @param tableName The table in which existence should be checked (may include schema) + * @param where The `WHERE` clause with the existence criteria + * @return A query to check document existence + */ + private fun exists(tableName: String, where: String) = + "SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it" + + /** + * Query to check for document existence by ID + * + * @param tableName The table in which existence should be checked (may include schema) + * @param docId The ID of the document (optional, used for type checking) + * @return A query to determine document existence by ID + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byId(tableName: String, docId: TKey? = null) = + exists(tableName, Where.byId(docId = docId)) + + /** + * Query to check for document existence matching the given fields + * + * @param tableName The table in which existence should be checked (may include schema) + * @param fields The field comparisons for the existence check + * @param howMatched How fields should be compared (optional, defaults to ALL) + * @return A query to determine document existence for the given fields + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + exists(tableName, Where.byFields(fields, howMatched)) + + /** + * Query to check for document existence via JSON containment (PostgreSQL only) + * + * @param tableName The table in which existence should be checked (may include schema) + * @return A query to determine document existence via JSON containment + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String) = + exists(tableName, Where.jsonContains()) + + /** + * Query to check for document existence via a JSON path match (PostgreSQL only) + * + * @param tableName The table in which existence should be checked (may include schema) + * @return A query to determine document existence via a JSON path match + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String) = + exists(tableName, Where.jsonPathMatches()) +} diff --git a/src/core/src/main/kotlin/query/FindQuery.kt b/src/core/src/main/kotlin/query/FindQuery.kt new file mode 100644 index 0000000..13077bf --- /dev/null +++ b/src/core/src/main/kotlin/query/FindQuery.kt @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import kotlin.jvm.Throws +import solutions.bitbadger.documents.query.byId as byIdBase +import solutions.bitbadger.documents.query.byFields as byFieldsBase + +/** + * Functions to retrieve documents + */ +object FindQuery { + + /** + * Query to retrieve all documents from a table + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @return A query to retrieve documents + */ + @JvmStatic + fun all(tableName: String) = + "SELECT data FROM $tableName" + + /** + * Query to retrieve a document by its ID + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @param docId The ID of the document (optional, used for type checking) + * @return A query to retrieve a document by its ID + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byId(tableName: String, docId: TKey? = null) = + byIdBase(all(tableName), docId) + + /** + * Query to retrieve documents matching the given fields + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @param fields The field comparisons for matching documents to retrieve + * @param howMatched How fields should be compared (optional, defaults to ALL) + * @return A query to retrieve documents matching the given fields + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + byFieldsBase(all(tableName), fields, howMatched) + + /** + * Query to retrieve documents via JSON containment (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @return A query to retrieve documents via JSON containment + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String) = + statementWhere(all(tableName), Where.jsonContains()) + + /** + * Query to retrieve documents via a JSON path match (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved (may include schema) + * @return A query to retrieve documents via a JSON path match + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String) = + statementWhere(all(tableName), Where.jsonPathMatches()) +} diff --git a/src/core/src/main/kotlin/query/PatchQuery.kt b/src/core/src/main/kotlin/query/PatchQuery.kt new file mode 100644 index 0000000..dbde8fe --- /dev/null +++ b/src/core/src/main/kotlin/query/PatchQuery.kt @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.* +import kotlin.jvm.Throws +import solutions.bitbadger.documents.query.byFields as byFieldsBase +import solutions.bitbadger.documents.query.byId as byIdBase + +/** + * Functions to create queries to patch (partially update) JSON documents + */ +object PatchQuery { + + /** + * Create an `UPDATE` statement to patch documents + * + * @param tableName The table to be updated + * @return A query to patch documents + */ + private fun patch(tableName: String) = + when (Configuration.dialect("create patch query")) { + Dialect.POSTGRESQL -> "data || :data" + Dialect.SQLITE -> "json_patch(data, json(:data))" + }.let { "UPDATE $tableName SET data = $it" } + + /** + * A query to patch (partially update) a JSON document by its ID + * + * @param tableName The name of the table where the document is stored + * @param docId The ID of the document to be updated (optional, used for type checking) + * @return A query to patch a JSON document by its ID + * @throws DocumentException If the dialect is not configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byId(tableName: String, docId: TKey? = null) = + byIdBase(patch(tableName), docId) + + /** + * A query to patch (partially update) a JSON document using field match criteria + * + * @param tableName The name of the table where the documents are stored + * @param fields The field criteria + * @param howMatched How the fields should be matched (optional, defaults to `ALL`) + * @return A query to patch JSON documents by field match criteria + * @throws DocumentException If the dialect is not configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + byFieldsBase(patch(tableName), fields, howMatched) + + /** + * A query to patch (partially update) a JSON document by JSON containment (PostgreSQL only) + * + * @param tableName The name of the table where the document is stored + * @return A query to patch JSON documents by JSON containment + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String) = + statementWhere(patch(tableName), Where.jsonContains()) + + /** + * A query to patch (partially update) a JSON document by JSON path match (PostgreSQL only) + * + * @param tableName The name of the table where the document is stored + * @return A query to patch JSON documents by JSON path match + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String) = + statementWhere(patch(tableName), Where.jsonPathMatches()) +} diff --git a/src/core/src/main/kotlin/query/Query.kt b/src/core/src/main/kotlin/query/Query.kt new file mode 100644 index 0000000..9381e81 --- /dev/null +++ b/src/core/src/main/kotlin/query/Query.kt @@ -0,0 +1,82 @@ +@file:JvmName("QueryUtils") +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.* +import kotlin.jvm.Throws + +// ~~~ TOP-LEVEL FUNCTIONS FOR THE QUERY PACKAGE ~~~ + +/** + * Combine a query (`SELECT`, `UPDATE`, etc.) and a `WHERE` clause + * + * @param statement The first part of the statement + * @param where The `WHERE` clause for the statement + * @return The two parts of the query combined with `WHERE` + */ +fun statementWhere(statement: String, where: String) = + "$statement WHERE $where" + +/** + * Create a query by a document's ID + * + * @param statement The SQL statement to be run against a document by its ID + * @param docId The ID of the document targeted + * @returns A query addressing a document by its ID + * @throws DocumentException If the dialect has not been set + */ +@Throws(DocumentException::class) +fun byId(statement: String, docId: TKey) = + statementWhere(statement, Where.byId(docId = docId)) + +/** + * Create a query on JSON fields + * + * @param statement The SQL statement to be run against matching fields + * @param fields The field conditions to be matched + * @param howMatched Whether to match any or all of the field conditions (optional; default ALL) + * @return A query addressing documents by field matching conditions + */ +@Throws(DocumentException::class) +@JvmOverloads +fun byFields(statement: String, fields: Collection>, howMatched: FieldMatch? = null) = + statementWhere(statement, Where.byFields(fields, howMatched)) + +/** + * Create an `ORDER BY` clause for the given fields + * + * @param fields One or more fields by which to order + * @param dialect The SQL dialect for the generated clause + * @return An `ORDER BY` clause for the given fields + */ +@Throws(DocumentException::class) +@JvmOverloads +fun orderBy(fields: Collection>, dialect: Dialect? = null): String { + val mode = dialect ?: Configuration.dialect("generate ORDER BY clause") + if (fields.isEmpty()) return "" + val orderFields = fields.joinToString(", ") { + val (field, direction) = + if (it.name.indexOf(' ') > -1) { + val parts = it.name.split(' ') + Pair(Field.named(parts[0]), " " + parts.drop(1).joinToString(" ")) + } else { + Pair, String?>(it, null) + } + val path = when { + field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld -> + when (mode) { + Dialect.POSTGRESQL -> "(${fld.path(mode)})::numeric" + Dialect.SQLITE -> fld.path(mode) + } + } + field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(mode).let { p -> + when (mode) { + Dialect.POSTGRESQL -> "LOWER($p)" + Dialect.SQLITE -> "$p COLLATE NOCASE" + } + } + else -> field.path(mode) + } + "$path${direction ?: ""}" + } + return " ORDER BY $orderFields" +} diff --git a/src/core/src/main/kotlin/query/RemoveFieldsQuery.kt b/src/core/src/main/kotlin/query/RemoveFieldsQuery.kt new file mode 100644 index 0000000..fd036ee --- /dev/null +++ b/src/core/src/main/kotlin/query/RemoveFieldsQuery.kt @@ -0,0 +1,89 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.* +import kotlin.jvm.Throws +import solutions.bitbadger.documents.query.byFields as byFieldsBase +import solutions.bitbadger.documents.query.byId as byIdBase + +/** + * Functions to create queries to remove fields from documents + */ +object RemoveFieldsQuery { + + /** + * Create a query to remove fields based on the given parameters + * + * @param tableName The name of the table in which documents should have fields removed + * @param toRemove The parameters for the fields to be removed + * @return A query to remove fields from documents in the given table + */ + private fun removeFields(tableName: String, toRemove: Collection>) = + when (Configuration.dialect("generate field removal query")) { + Dialect.POSTGRESQL -> "UPDATE $tableName SET data = data - ${toRemove.elementAt(0).name}::text[]" + Dialect.SQLITE -> toRemove.joinToString(", ") { it.name }.let { + "UPDATE $tableName SET data = json_remove(data, $it)" + } + } + + /** + * A query to patch (partially update) a JSON document by its ID + * + * @param tableName The name of the table where the document is stored + * @param toRemove The parameters for the fields to be removed + * @param docId The ID of the document to be updated (optional, used for type checking) + * @return A query to patch a JSON document by its ID + * @throws DocumentException If the dialect is not configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byId(tableName: String, toRemove: Collection>, docId: TKey? = null) = + byIdBase(removeFields(tableName, toRemove), docId) + + /** + * A query to patch (partially update) a JSON document using field match criteria + * + * @param tableName The name of the table where the documents are stored + * @param toRemove The parameters for the fields to be removed + * @param fields The field criteria + * @param howMatched How the fields should be matched (optional, defaults to `ALL`) + * @return A query to patch JSON documents by field match criteria + * @throws DocumentException If the dialect is not configured + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields( + tableName: String, + toRemove: Collection>, + fields: Collection>, + howMatched: FieldMatch? = null + ) = + byFieldsBase(removeFields(tableName, toRemove), fields, howMatched) + + /** + * A query to patch (partially update) a JSON document by JSON containment (PostgreSQL only) + * + * @param tableName The name of the table where the document is stored + * @param toRemove The parameters for the fields to be removed + * @return A query to patch JSON documents by JSON containment + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, toRemove: Collection>) = + statementWhere(removeFields(tableName, toRemove), Where.jsonContains()) + + /** + * A query to patch (partially update) a JSON document by JSON path match (PostgreSQL only) + * + * @param tableName The name of the table where the document is stored + * @param toRemove The parameters for the fields to be removed + * @return A query to patch JSON documents by JSON path match + * @throws DocumentException If the database dialect is not PostgreSQL + */ + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, toRemove: Collection>) = + statementWhere(removeFields(tableName, toRemove), Where.jsonPathMatches()) +} diff --git a/src/core/src/main/kotlin/query/Where.kt b/src/core/src/main/kotlin/query/Where.kt new file mode 100644 index 0000000..da232ed --- /dev/null +++ b/src/core/src/main/kotlin/query/Where.kt @@ -0,0 +1,74 @@ +package solutions.bitbadger.documents.query + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import kotlin.jvm.Throws + +/** + * Functions to create `WHERE` clause fragments + */ +object Where { + + /** + * Create a `WHERE` clause fragment to query by one or more fields + * + * @param fields The fields to be queried + * @param howMatched How the fields should be matched (optional, defaults to `ALL`) + * @return A `WHERE` clause fragment to match the given fields + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byFields(fields: Collection>, howMatched: FieldMatch? = null) = + fields.joinToString(" ${(howMatched ?: FieldMatch.ALL).sql} ") { it.toWhere() } + + /** + * Create a `WHERE` clause fragment to retrieve a document by its ID + * + * @param parameterName The parameter name to use for the ID placeholder (optional, defaults to ":id") + * @param docId The ID value (optional; used for type determinations, string assumed if not provided) + * @return A `WHERE` clause fragment to match the document's ID + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byId(parameterName: String = ":id", docId: TKey? = null) = + byFields(listOf(Field.equal(Configuration.idField, docId ?: "", parameterName))) + + /** + * Create a `WHERE` clause fragment to implement a JSON containment query (PostgreSQL only) + * + * @param parameterName The parameter name to use for the JSON placeholder (optional, defaults to ":criteria") + * @return A `WHERE` clause fragment to implement a JSON containment criterion + * @throws DocumentException If called against a SQLite database + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun jsonContains(parameterName: String = ":criteria") = + when (Configuration.dialect("create containment WHERE clause")) { + Dialect.POSTGRESQL -> "data @> $parameterName" + Dialect.SQLITE -> throw DocumentException("JSON containment is not supported") + } + + /** + * Create a `WHERE` clause fragment to implement a JSON path match query (PostgreSQL only) + * + * @param parameterName The parameter name to use for the placeholder (optional, defaults to ":path") + * @return A `WHERE` clause fragment to implement a JSON path match criterion + * @throws DocumentException If called against a SQLite database + */ + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun jsonPathMatches(parameterName: String = ":path") = + when (Configuration.dialect("create JSON path match WHERE clause")) { + Dialect.POSTGRESQL -> "jsonb_path_exists(data, $parameterName::jsonpath)" + Dialect.SQLITE -> throw DocumentException("JSON path match is not supported") + } +} diff --git a/src/core/src/main/module-info.md b/src/core/src/main/module-info.md new file mode 100644 index 0000000..158ae4b --- /dev/null +++ b/src/core/src/main/module-info.md @@ -0,0 +1,19 @@ +# Module core + +This module contains configuration and support files for the document store API, as well as an implementation suitable for any JVM language. + +# Package solutions.bitbadger.documents + +Configuration and other items to support the document store API + +# Package solutions.bitbadger.documents.query + +Functions to create document manipulation queries + +# Package solutions.bitbadger.documents.java + +A Java-focused implementation of the document store API + +# Package solutions.bitbadger.documents.java.extensions + +Extensions on the Java `Connection` object for document manipulation diff --git a/src/core/src/test/java/module-info.java b/src/core/src/test/java/module-info.java new file mode 100644 index 0000000..5fafbfc --- /dev/null +++ b/src/core/src/test/java/module-info.java @@ -0,0 +1,20 @@ +module solutions.bitbadger.documents.core.tests { + requires solutions.bitbadger.documents.core; + requires com.fasterxml.jackson.databind; + requires java.sql; + requires kotlin.stdlib; + requires kotlin.test.junit5; + requires org.junit.jupiter.api; + requires org.slf4j; + requires annotations; + //requires org.checkerframework.checker.qual; + + exports solutions.bitbadger.documents.core.tests; + exports solutions.bitbadger.documents.core.tests.integration; + exports solutions.bitbadger.documents.core.tests.java; + exports solutions.bitbadger.documents.core.tests.java.integration; + + opens solutions.bitbadger.documents.core.tests; + opens solutions.bitbadger.documents.core.tests.java; + opens solutions.bitbadger.documents.core.tests.java.integration; +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/AutoIdTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/AutoIdTest.java new file mode 100644 index 0000000..8f6c0f8 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/AutoIdTest.java @@ -0,0 +1,216 @@ +package solutions.bitbadger.documents.core.tests.java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.AutoId; +import solutions.bitbadger.documents.DocumentException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `AutoId` enum + */ +@DisplayName("Core | Java | AutoId") +final public class AutoIdTest { + + @Test + @DisplayName("Generates a UUID string") + public void generateUUID() { + assertEquals(32, AutoId.generateUUID().length(), "The UUID should have been a 32-character string"); + } + + @Test + @DisplayName("Generates a random hex character string of an even length") + public void generateRandomStringEven() { + final String result = AutoId.generateRandomString(8); + assertEquals(8, result.length(), "There should have been 8 characters in " + result); + } + + @Test + @DisplayName("Generates a random hex character string of an odd length") + public void generateRandomStringOdd() { + final String result = AutoId.generateRandomString(11); + assertEquals(11, result.length(), "There should have been 11 characters in " + result); + } + + @Test + @DisplayName("Generates different random hex character strings") + public void generateRandomStringIsRandom() { + final String result1 = AutoId.generateRandomString(16); + final String result2 = AutoId.generateRandomString(16); + assertNotEquals(result1, result2, "There should have been 2 different strings generated"); + } + + @Test + @DisplayName("needsAutoId fails for null document") + public void needsAutoIdFailsForNullDocument() { + assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.DISABLED, null, "id")); + } + + @Test + @DisplayName("needsAutoId fails for missing ID property") + public void needsAutoIdFailsForMissingId() { + assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.UUID, new IntIdClass(0), "Id")); + } + + @Test + @DisplayName("needsAutoId returns false if disabled") + public void needsAutoIdFalseIfDisabled() { + try { + assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns true for Number strategy and byte ID of 0") + public void needsAutoIdTrueForByteWithZero() { + try { + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 0), "id"), + "Number Auto ID with 0 should return true"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0") + public void needsAutoIdFalseForByteWithNonZero() { + try { + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 77), "id"), + "Number Auto ID with 77 should return false"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns true for Number strategy and short ID of 0") + public void needsAutoIdTrueForShortWithZero() { + try { + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 0), "id"), + "Number Auto ID with 0 should return true"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns false for Number strategy and short ID of non-0") + public void needsAutoIdFalseForShortWithNonZero() { + try { + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 31), "id"), + "Number Auto ID with 31 should return false"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns true for Number strategy and int ID of 0") + public void needsAutoIdTrueForIntWithZero() { + try { + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(0), "id"), + "Number Auto ID with 0 should return true"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns false for Number strategy and int ID of non-0") + public void needsAutoIdFalseForIntWithNonZero() { + try { + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(6), "id"), + "Number Auto ID with 6 should return false"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns true for Number strategy and long ID of 0") + public void needsAutoIdTrueForLongWithZero() { + try { + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(0L), "id"), + "Number Auto ID with 0 should return true"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns false for Number strategy and long ID of non-0") + public void needsAutoIdFalseForLongWithNonZero() { + try { + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(2L), "id"), + "Number Auto ID with 2 should return false"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId fails for Number strategy and non-number ID") + public void needsAutoIdFailsForNumberWithStringId() { + assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.NUMBER, new StringIdClass(""), "id")); + } + + @Test + @DisplayName("needsAutoId returns true for UUID strategy and blank ID") + public void needsAutoIdTrueForUUIDWithBlank() { + try { + assertTrue(AutoId.needsAutoId(AutoId.UUID, new StringIdClass(""), "id"), + "UUID Auto ID with blank should return true"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns false for UUID strategy and non-blank ID") + public void needsAutoIdFalseForUUIDNotBlank() { + try { + assertFalse(AutoId.needsAutoId(AutoId.UUID, new StringIdClass("howdy"), "id"), + "UUID Auto ID with non-blank should return false"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId fails for UUID strategy and non-string ID") + public void needsAutoIdFailsForUUIDNonString() { + assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.UUID, new IntIdClass(5), "id")); + } + + @Test + @DisplayName("needsAutoId returns true for Random String strategy and blank ID") + public void needsAutoIdTrueForRandomWithBlank() { + try { + assertTrue(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass(""), "id"), + "Random String Auto ID with blank should return true"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId returns false for Random String strategy and non-blank ID") + public void needsAutoIdFalseForRandomNotBlank() { + try { + assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass("full"), "id"), + "Random String Auto ID with non-blank should return false"); + } catch (DocumentException ex) { + fail(ex); + } + } + + @Test + @DisplayName("needsAutoId fails for Random String strategy and non-string ID") + public void needsAutoIdFailsForRandomNonString() { + assertThrows(DocumentException.class, + () -> AutoId.needsAutoId(AutoId.RANDOM_STRING, new ShortIdClass((short) 55), "id")); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ByteIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ByteIdClass.java new file mode 100644 index 0000000..72e872f --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ByteIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.core.tests.java; + +public class ByteIdClass { + + private byte id; + + public byte getId() { + return id; + } + + public void setId(byte id) { + this.id = id; + } + + public ByteIdClass(byte id) { + this.id = id; + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ConfigurationTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ConfigurationTest.java new file mode 100644 index 0000000..d198c6c --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ConfigurationTest.java @@ -0,0 +1,48 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.Dialect; +import solutions.bitbadger.documents.DocumentException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests for the `Configuration` object + */ +@DisplayName("Core | Java | Configuration") +final public class ConfigurationTest { + + @Test + @DisplayName("Default ID field is `id`") + public void defaultIdField() { + assertEquals("id", Configuration.idField, "Default ID field incorrect"); + } + + @Test + @DisplayName("Default Auto ID strategy is `DISABLED`") + public void defaultAutoId() { + assertEquals(AutoId.DISABLED, Configuration.autoIdStrategy, "Default Auto ID strategy should be `disabled`"); + } + + @Test + @DisplayName("Default ID string length should be 16") + public void defaultIdStringLength() { + assertEquals(16, Configuration.idStringLength, "Default ID string length should be 16"); + } + + @Test + @DisplayName("Dialect is derived from connection string") + public void dialectIsDerived() throws DocumentException { + try { + assertThrows(DocumentException.class, Configuration::dialect); + Configuration.setConnectionString("jdbc:postgresql:db"); + assertEquals(Dialect.POSTGRESQL, Configuration.dialect()); + } finally { + Configuration.setConnectionString(null); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/CountQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/CountQueryTest.java new file mode 100644 index 0000000..71f3c74 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/CountQueryTest.java @@ -0,0 +1,87 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Count` object + */ +@DisplayName("Core | Java | Query | CountQuery") +final public class CountQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("all generates correctly") + public void all() { + assertEquals(String.format("SELECT COUNT(*) AS it FROM %s", TEST_TABLE), CountQuery.all(TEST_TABLE), + "Count query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + public void byFieldsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("SELECT COUNT(*) AS it FROM %s WHERE data->>'test' = :field0", TEST_TABLE), + CountQuery.byFields(TEST_TABLE, List.of(Field.equal("test", "", ":field0"))), + "Count query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + public void byFieldsSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("SELECT COUNT(*) AS it FROM %s WHERE data->>'test' = :field0", TEST_TABLE), + CountQuery.byFields(TEST_TABLE, List.of(Field.equal("test", "", ":field0"))), + "Count query not constructed correctly"); + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + public void byContainsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("SELECT COUNT(*) AS it FROM %s WHERE data @> :criteria", TEST_TABLE), + CountQuery.byContains(TEST_TABLE), "Count query not constructed correctly"); + } + + @Test + @DisplayName("byContains fails | SQLite") + public void byContainsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> CountQuery.byContains(TEST_TABLE)); + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + public void byJsonPathPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals( + String.format("SELECT COUNT(*) AS it FROM %s WHERE jsonb_path_exists(data, :path::jsonpath)", + TEST_TABLE), + CountQuery.byJsonPath(TEST_TABLE), "Count query not constructed correctly"); + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + public void byJsonPathSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> CountQuery.byJsonPath(TEST_TABLE)); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DefinitionQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DefinitionQueryTest.java new file mode 100644 index 0000000..219c31f --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DefinitionQueryTest.java @@ -0,0 +1,133 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Definition` object + */ +@DisplayName("Core | Java | Query | DefinitionQuery") +final public class DefinitionQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("ensureTableFor generates correctly") + public void ensureTableFor() { + assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", + DefinitionQuery.ensureTableFor("my.table", "JSONB"), + "CREATE TABLE statement not constructed correctly"); + } + + @Test + @DisplayName("ensureTable generates correctly | PostgreSQL") + public void ensureTablePostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("CREATE TABLE IF NOT EXISTS %s (data JSONB NOT NULL)", TEST_TABLE), + DefinitionQuery.ensureTable(TEST_TABLE)); + } + + @Test + @DisplayName("ensureTable generates correctly | SQLite") + public void ensureTableSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("CREATE TABLE IF NOT EXISTS %s (data TEXT NOT NULL)", TEST_TABLE), + DefinitionQuery.ensureTable(TEST_TABLE)); + } + + @Test + @DisplayName("ensureTable fails when no dialect is set") + public void ensureTableFailsUnknown() { + assertThrows(DocumentException.class, () -> DefinitionQuery.ensureTable(TEST_TABLE)); + } + + @Test + @DisplayName("ensureKey generates correctly with schema") + public void ensureKeyWithSchema() throws DocumentException { + assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", + DefinitionQuery.ensureKey("test.table", Dialect.POSTGRESQL), + "CREATE INDEX for key statement with schema not constructed correctly"); + } + + @Test + @DisplayName("ensureKey generates correctly without schema") + public void ensureKeyWithoutSchema() throws DocumentException { + assertEquals( + String.format("CREATE UNIQUE INDEX IF NOT EXISTS idx_%1$s_key ON %1$s ((data->>'id'))", TEST_TABLE), + DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE), + "CREATE INDEX for key statement without schema not constructed correctly"); + } + + @Test + @DisplayName("ensureIndexOn generates multiple fields and directions") + public void ensureIndexOnMultipleFields() throws DocumentException { + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", + DefinitionQuery.ensureIndexOn("test.table", "gibberish", List.of("taco", "guac DESC", "salsa ASC"), + Dialect.POSTGRESQL), + "CREATE INDEX for multiple field statement not constructed correctly"); + } + + @Test + @DisplayName("ensureIndexOn generates nested field | PostgreSQL") + public void ensureIndexOnNestedPostgres() throws DocumentException { + assertEquals(String.format("CREATE INDEX IF NOT EXISTS idx_%1$s_nest ON %1$s ((data#>>'{a,b,c}'))", TEST_TABLE), + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List.of("a.b.c"), Dialect.POSTGRESQL), + "CREATE INDEX for nested PostgreSQL field incorrect"); + } + + @Test + @DisplayName("ensureIndexOn generates nested field | SQLite") + public void ensureIndexOnNestedSQLite() throws DocumentException { + assertEquals( + String.format("CREATE INDEX IF NOT EXISTS idx_%1$s_nest ON %1$s ((data->'a'->'b'->>'c'))", TEST_TABLE), + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List.of("a.b.c"), Dialect.SQLITE), + "CREATE INDEX for nested SQLite field incorrect"); + } + + @Test + @DisplayName("ensureDocumentIndexOn generates Full | PostgreSQL") + public void ensureDocumentIndexOnFullPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("CREATE INDEX IF NOT EXISTS idx_%1$s_document ON %1$s USING GIN (data)", TEST_TABLE), + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL), + "CREATE INDEX for full document index incorrect"); + } + + @Test + @DisplayName("ensureDocumentIndexOn generates Optimized | PostgreSQL") + public void ensureDocumentIndexOnOptimizedPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals( + String.format("CREATE INDEX IF NOT EXISTS idx_%1$s_document ON %1$s USING GIN (data jsonb_path_ops)", + TEST_TABLE), + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED), + "CREATE INDEX for optimized document index incorrect"); + } + + @Test + @DisplayName("ensureDocumentIndexOn fails | SQLite") + public void ensureDocumentIndexOnFailsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, + () -> DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL)); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DeleteQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DeleteQueryTest.java new file mode 100644 index 0000000..3128652 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DeleteQueryTest.java @@ -0,0 +1,94 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Delete` object + */ +@DisplayName("Core | Java | Query | DeleteQuery") +final public class DeleteQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + public void byIdPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("DELETE FROM %s WHERE data->>'id' = :id", TEST_TABLE), DeleteQuery.byId(TEST_TABLE), + "Delete query not constructed correctly"); + } + + @Test + @DisplayName("byId generates correctly | SQLite") + public void byIdSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("DELETE FROM %s WHERE data->>'id' = :id", TEST_TABLE), DeleteQuery.byId(TEST_TABLE), + "Delete query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + public void byFieldsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("DELETE FROM %s WHERE data->>'a' = :b", TEST_TABLE), + DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal("a", "", ":b"))), + "Delete query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + public void byFieldsSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("DELETE FROM %s WHERE data->>'a' = :b", TEST_TABLE), + DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal("a", "", ":b"))), + "Delete query not constructed correctly"); + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + public void byContainsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("DELETE FROM %s WHERE data @> :criteria", TEST_TABLE), + DeleteQuery.byContains(TEST_TABLE), "Delete query not constructed correctly"); + } + + @Test + @DisplayName("byContains fails | SQLite") + public void byContainsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> DeleteQuery.byContains(TEST_TABLE)); + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + public void byJsonPathPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("DELETE FROM %s WHERE jsonb_path_exists(data, :path::jsonpath)", TEST_TABLE), + DeleteQuery.byJsonPath(TEST_TABLE), "Delete query not constructed correctly"); + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + public void byJsonPathSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> DeleteQuery.byJsonPath(TEST_TABLE)); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DialectTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DialectTest.java new file mode 100644 index 0000000..543986d --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DialectTest.java @@ -0,0 +1,43 @@ +package solutions.bitbadger.documents.core.tests.java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.Dialect; +import solutions.bitbadger.documents.DocumentException; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `Dialect` enum + */ +@DisplayName("Core | Java | Dialect") +final public class DialectTest { + + @Test + @DisplayName("deriveFromConnectionString derives PostgreSQL correctly") + public void derivesPostgres() throws DocumentException { + assertEquals(Dialect.POSTGRESQL, Dialect.deriveFromConnectionString("jdbc:postgresql:db"), + "Dialect should have been PostgreSQL"); + } + + @Test + @DisplayName("deriveFromConnectionString derives SQLite correctly") + public void derivesSQLite() throws DocumentException { + assertEquals( + Dialect.SQLITE, Dialect.deriveFromConnectionString("jdbc:sqlite:memory"), + "Dialect should have been SQLite"); + } + + @Test + @DisplayName("deriveFromConnectionString fails when the connection string is unknown") + public void deriveFailsWhenUnknown() { + try { + Dialect.deriveFromConnectionString("SQL Server"); + fail("Dialect derivation should have failed"); + } catch (DocumentException ex) { + assertNotNull(ex.getMessage(), "The exception message should not have been null"); + assertTrue(ex.getMessage().contains("[SQL Server]"), + "The connection string should have been in the exception message"); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentIndexTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentIndexTest.java new file mode 100644 index 0000000..7354ebb --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentIndexTest.java @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.core.tests.java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentIndex; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the `DocumentIndex` enum + */ +@DisplayName("Core | Java | DocumentIndex") +final public class DocumentIndexTest { + + @Test + @DisplayName("FULL uses proper SQL") + public void fullSQL() { + assertEquals("", DocumentIndex.FULL.getSql(), "The SQL for Full is incorrect"); + } + + @Test + @DisplayName("OPTIMIZED uses proper SQL") + public void optimizedSQL() { + assertEquals(" jsonb_path_ops", DocumentIndex.OPTIMIZED.getSql(), "The SQL for Optimized is incorrect"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentQueryTest.java new file mode 100644 index 0000000..ada52b3 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentQueryTest.java @@ -0,0 +1,134 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.core.tests.ForceDialect; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Document` object + */ +@DisplayName("Core | Java | Query | DocumentQuery") +final public class DocumentQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("insert generates no auto ID | PostgreSQL") + public void insertNoAutoPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("INSERT INTO %s VALUES (:data)", TEST_TABLE), DocumentQuery.insert(TEST_TABLE)); + } + + @Test + @DisplayName("insert generates no auto ID | SQLite") + public void insertNoAutoSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("INSERT INTO %s VALUES (:data)", TEST_TABLE), DocumentQuery.insert(TEST_TABLE)); + } + + @Test + @DisplayName("insert generates auto number | PostgreSQL") + public void insertAutoNumberPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("INSERT INTO %1$s VALUES (:data::jsonb || ('{\"id\":' " + + "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM %1$s) || '}')::jsonb)", + TEST_TABLE), + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER)); + } + + @Test + @DisplayName("insert generates auto number | SQLite") + public void insertAutoNumberSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("INSERT INTO %1$s VALUES (json_set(:data, '$.id', " + + "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM %1$s)))", TEST_TABLE), + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER)); + } + + @Test + @DisplayName("insert generates auto UUID | PostgreSQL") + public void insertAutoUUIDPostgres() throws DocumentException { + ForceDialect.postgres(); + final String query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID); + assertTrue(query.startsWith(String.format("INSERT INTO %s VALUES (:data::jsonb || '{\"id\":\"", TEST_TABLE)), + String.format("Query start not correct (actual: %s)", query)); + assertTrue(query.endsWith("\"}')"), "Query end not correct"); + } + + @Test + @DisplayName("insert generates auto UUID | SQLite") + public void insertAutoUUIDSQLite() throws DocumentException { + ForceDialect.sqlite(); + final String query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID); + assertTrue(query.startsWith(String.format("INSERT INTO %s VALUES (json_set(:data, '$.id', '", TEST_TABLE)), + String.format("Query start not correct (actual: %s)", query)); + assertTrue(query.endsWith("'))"), "Query end not correct"); + } + + @Test + @DisplayName("insert generates auto random string | PostgreSQL") + public void insertAutoRandomPostgres() throws DocumentException { + try { + ForceDialect.postgres(); + Configuration.idStringLength = 8; + final String query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING); + final String start = String.format("INSERT INTO %s VALUES (:data::jsonb || '{\"id\":\"", TEST_TABLE); + final String end = "\"}')"; + assertTrue(query.startsWith(start), String.format("Query start not correct (actual: %s)", query)); + assertTrue(query.endsWith(end), "Query end not correct"); + assertEquals(8, query.replace(start, "").replace(end, "").length(), "Random string length incorrect"); + } finally { + Configuration.idStringLength = 16; + } + } + + @Test + @DisplayName("insert generates auto random string | SQLite") + public void insertAutoRandomSQLite() throws DocumentException { + ForceDialect.sqlite(); + final String query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING); + final String start = String.format("INSERT INTO %s VALUES (json_set(:data, '$.id', '", TEST_TABLE); + final String end = "'))"; + assertTrue(query.startsWith(start), String.format("Query start not correct (actual: %s)", query)); + assertTrue(query.endsWith(end), "Query end not correct"); + assertEquals(Configuration.idStringLength, query.replace(start, "").replace(end, "").length(), + "Random string length incorrect"); + } + + @Test + @DisplayName("insert fails when no dialect is set") + public void insertFailsUnknown() { + assertThrows(DocumentException.class, () -> DocumentQuery.insert(TEST_TABLE)); + } + + @Test + @DisplayName("save generates correctly") + public void save() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format( + "INSERT INTO %s VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", + TEST_TABLE), + DocumentQuery.save(TEST_TABLE), "INSERT ON CONFLICT UPDATE statement not constructed correctly"); + } + + @Test + @DisplayName("update generates successfully") + public void update() { + assertEquals(String.format("UPDATE %s SET data = :data", TEST_TABLE), DocumentQuery.update(TEST_TABLE), + "Update query not constructed correctly"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ExistsQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ExistsQueryTest.java new file mode 100644 index 0000000..d4b2e57 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ExistsQueryTest.java @@ -0,0 +1,97 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.ExistsQuery; +import solutions.bitbadger.documents.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Exists` object + */ +@DisplayName("Core | Java | Query | ExistsQuery") +final public class ExistsQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + public void byIdPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("SELECT EXISTS (SELECT 1 FROM %s WHERE data->>'id' = :id) AS it", TEST_TABLE), + ExistsQuery.byId(TEST_TABLE), "Exists query not constructed correctly"); + } + + @Test + @DisplayName("byId generates correctly | SQLite") + public void byIdSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("SELECT EXISTS (SELECT 1 FROM %s WHERE data->>'id' = :id) AS it", TEST_TABLE), + ExistsQuery.byId(TEST_TABLE), "Exists query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + public void byFieldsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format( + "SELECT EXISTS (SELECT 1 FROM %s WHERE (data->>'it')::numeric = :test) AS it", TEST_TABLE), + ExistsQuery.byFields(TEST_TABLE, List.of(Field.equal("it", 7, ":test"))), + "Exists query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + public void byFieldsSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("SELECT EXISTS (SELECT 1 FROM %s WHERE data->>'it' = :test) AS it", TEST_TABLE), + ExistsQuery.byFields(TEST_TABLE, List.of(Field.equal("it", 7, ":test"))), + "Exists query not constructed correctly"); + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + public void byContainsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("SELECT EXISTS (SELECT 1 FROM %s WHERE data @> :criteria) AS it", TEST_TABLE), + ExistsQuery.byContains(TEST_TABLE), "Exists query not constructed correctly"); + } + + @Test + @DisplayName("byContains fails | SQLite") + public void byContainsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> ExistsQuery.byContains(TEST_TABLE)); + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + public void byJsonPathPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format( + "SELECT EXISTS (SELECT 1 FROM %s WHERE jsonb_path_exists(data, :path::jsonpath)) AS it", + TEST_TABLE), + ExistsQuery.byJsonPath(TEST_TABLE), "Exists query not constructed correctly"); + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + public void byJsonPathSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> ExistsQuery.byJsonPath(TEST_TABLE)); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldMatchTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldMatchTest.java new file mode 100644 index 0000000..54a9096 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldMatchTest.java @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.core.tests.java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.FieldMatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the `FieldMatch` enum + */ +@DisplayName("Core | Java | FieldMatch") +final public class FieldMatchTest { + + @Test + @DisplayName("ANY uses proper SQL") + public void any() { + assertEquals("OR", FieldMatch.ANY.getSql(), "ANY should use OR"); + } + + @Test + @DisplayName("ALL uses proper SQL") + public void all() { + assertEquals("AND", FieldMatch.ALL.getSql(), "ALL should use AND"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldTest.java new file mode 100644 index 0000000..193869a --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldTest.java @@ -0,0 +1,636 @@ +package solutions.bitbadger.documents.core.tests.java; + +import kotlin.Pair; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.*; +import solutions.bitbadger.documents.core.tests.ForceDialect; + +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `Field` class + */ +@DisplayName("Core | Java | Field") +final public class FieldTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + // ~~~ INSTANCE METHODS ~~~ + + @Test + @DisplayName("withParameterName fails for invalid name") + public void withParamNameFails() { + assertThrows(DocumentException.class, () -> Field.equal("it", "").withParameterName("2424")); + } + + @Test + @DisplayName("withParameterName works with colon prefix") + public void withParamNameColon() { + Field field = Field.equal("abc", "22").withQualifier("me"); + Field withParam = field.withParameterName(":test"); + assertNotSame(field, withParam, "A new Field instance should have been created"); + assertEquals(field.getName(), withParam.getName(), "Name should have been preserved"); + assertEquals(field.getComparison(), withParam.getComparison(), "Comparison should have been preserved"); + assertEquals(":test", withParam.getParameterName(), "Parameter name not set correctly"); + assertEquals(field.getQualifier(), withParam.getQualifier(), "Qualifier should have been preserved"); + } + + @Test + @DisplayName("withParameterName works with at-sign prefix") + public void withParamNameAtSign() { + Field field = Field.equal("def", "44"); + Field withParam = field.withParameterName("@unit"); + assertNotSame(field, withParam, "A new Field instance should have been created"); + assertEquals(field.getName(), withParam.getName(), "Name should have been preserved"); + assertEquals(field.getComparison(), withParam.getComparison(), "Comparison should have been preserved"); + assertEquals("@unit", withParam.getParameterName(), "Parameter name not set correctly"); + assertEquals(field.getQualifier(), withParam.getQualifier(), "Qualifier should have been preserved"); + } + + @Test + @DisplayName("withQualifier sets qualifier correctly") + public void withQualifier() { + Field field = Field.equal("j", "k"); + Field withQual = field.withQualifier("test"); + assertNotSame(field, withQual, "A new Field instance should have been created"); + assertEquals(field.getName(), withQual.getName(), "Name should have been preserved"); + assertEquals(field.getComparison(), withQual.getComparison(), "Comparison should have been preserved"); + assertEquals(field.getParameterName(), withQual.getParameterName(), + "Parameter Name should have been preserved"); + assertEquals("test", withQual.getQualifier(), "Qualifier not set correctly"); + } + + @Test + @DisplayName("path generates for simple unqualified field | PostgreSQL") + public void pathPostgresSimpleUnqualified() { + assertEquals("data->>'SomethingCool'", + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not correct"); + } + + @Test + @DisplayName("path generates for simple qualified field | PostgreSQL") + public void pathPostgresSimpleQualified() { + assertEquals("this.data->>'SomethingElse'", + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not correct"); + } + + @Test + @DisplayName("path generates for nested unqualified field | PostgreSQL") + public void pathPostgresNestedUnqualified() { + assertEquals("data#>>'{My,Nested,Field}'", + Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct"); + } + + @Test + @DisplayName("path generates for nested qualified field | PostgreSQL") + public void pathPostgresNestedQualified() { + assertEquals("bird.data#>>'{Nest,Away}'", + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not correct"); + } + + @Test + @DisplayName("path generates for simple unqualified field | SQLite") + public void pathSQLiteSimpleUnqualified() { + assertEquals("data->>'SomethingCool'", + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct"); + } + + @Test + @DisplayName("path generates for simple qualified field | SQLite") + public void pathSQLiteSimpleQualified() { + assertEquals("this.data->>'SomethingElse'", + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL), + "Path not correct"); + } + + @Test + @DisplayName("path generates for nested unqualified field | SQLite") + public void pathSQLiteNestedUnqualified() { + assertEquals("data->'My'->'Nested'->>'Field'", + Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct"); + } + + @Test + @DisplayName("path generates for nested qualified field | SQLite") + public void pathSQLiteNestedQualified() { + assertEquals("bird.data->'Nest'->>'Away'", + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL), + "Path not correct"); + } + + @Test + @DisplayName("toWhere generates for exists w/o qualifier | PostgreSQL") + public void toWhereExistsNoQualPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for exists w/o qualifier | SQLite") + public void toWhereExistsNoQualSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for not-exists w/o qualifier | PostgreSQL") + public void toWhereNotExistsNoQualPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for not-exists w/o qualifier | SQLite") + public void toWhereNotExistsNoQualSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range | PostgreSQL") + public void toWhereBetweenNoQualNumericPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").toWhere(), "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range | PostgreSQL") + public void toWhereBetweenNoQualAlphaPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'city' BETWEEN :citymin AND :citymax", + Field.between("city", "Atlanta", "Chicago", ":city").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier | SQLite") + public void toWhereBetweenNoQualSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range | PostgreSQL") + public void toWhereBetweenQualNumericPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("(test.data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").withQualifier("test").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range | PostgreSQL") + public void toWhereBetweenQualAlphaPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("unit.data->>'city' BETWEEN :citymin AND :citymax", + Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier | SQLite") + public void toWhereBetweenQualSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("my.data->>'age' BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").withQualifier("my").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for IN/any, numeric values | PostgreSQL") + public void toWhereAnyNumericPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", + Field.any("even", List.of(2, 4, 6), ":nbr").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for IN/any, alphanumeric values | PostgreSQL") + public void toWhereAnyAlphaPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'test' IN (:city_0, :city_1)", + Field.any("test", List.of("Atlanta", "Chicago"), ":city").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for IN/any | SQLite") + public void toWhereAnySQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'test' IN (:city_0, :city_1)", + Field.any("test", List.of("Atlanta", "Chicago"), ":city").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for inArray | PostgreSQL") + public void toWhereInArrayPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]", + Field.inArray("even", "tbl", List.of(2, 4, 6, 8), ":it").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for inArray | SQLite") + public void toWhereInArraySQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("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(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for others w/o qualifier | PostgreSQL") + public void toWhereOtherNoQualPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates for others w/o qualifier | SQLite") + public void toWhereOtherNoQualSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates no-parameter w/ qualifier | PostgreSQL") + public void toWhereNoParamWithQualPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates no-parameter w/ qualifier | SQLite") + public void toWhereNoParamWithQualSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates parameter w/ qualifier | PostgreSQL") + public void toWhereParamWithQualPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("(q.data->>'le_field')::numeric <= :it", + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere generates parameter w/ qualifier | SQLite") + public void toWhereParamWithQualSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("q.data->>'le_field' <= :it", + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(), + "Field WHERE clause not generated correctly"); + } + + @Test + @DisplayName("toWhere fails when dialect is not set") + public void toWhereFailsNoDialect() { + assertThrows(DocumentException.class, () -> Field.equal("a", "oops").toWhere()); + } + + // ~~~ STATIC TESTS ~~~ + + @Test + @DisplayName("equal constructs a field w/o parameter name") + public void equalCtor() { + final Field field = Field.equal("Test", 14); + assertEquals("Test", field.getName(), "Field name not filled correctly"); + assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals(14, field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("equal constructs a field w/ parameter name") + public void equalParameterCtor() { + final Field field = Field.equal("Test", 14, ":w"); + assertEquals("Test", field.getName(), "Field name not filled correctly"); + assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals(14, field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertEquals(":w", field.getParameterName(), "Field parameter name not filled correctly"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("greater constructs a field w/o parameter name") + public void greaterCtor() { + final Field field = Field.greater("Great", "night"); + assertEquals("Great", field.getName(), "Field name not filled correctly"); + assertEquals(Op.GREATER, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("night", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("greater constructs a field w/ parameter name") + public void greaterParameterCtor() { + final Field field = Field.greater("Great", "night", ":yeah"); + assertEquals("Great", field.getName(), "Field name not filled correctly"); + assertEquals(Op.GREATER, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("night", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertEquals(":yeah", field.getParameterName(), "Field parameter name not filled correctly"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("greaterOrEqual constructs a field w/o parameter name") + public void greaterOrEqualCtor() { + final Field field = Field.greaterOrEqual("Nice", 88L); + assertEquals("Nice", field.getName(), "Field name not filled correctly"); + assertEquals(Op.GREATER_OR_EQUAL, field.getComparison().getOp(), + "Field comparison operation not filled correctly"); + assertEquals(88L, field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("greaterOrEqual constructs a field w/ parameter name") + public void greaterOrEqualParameterCtor() { + final Field field = Field.greaterOrEqual("Nice", 88L, ":nice"); + assertEquals("Nice", field.getName(), "Field name not filled correctly"); + assertEquals(Op.GREATER_OR_EQUAL, field.getComparison().getOp(), + "Field comparison operation not filled correctly"); + assertEquals(88L, field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertEquals(":nice", field.getParameterName(), "Field parameter name not filled correctly"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("less constructs a field w/o parameter name") + public void lessCtor() { + final Field field = Field.less("Lesser", "seven"); + assertEquals("Lesser", field.getName(), "Field name not filled correctly"); + assertEquals(Op.LESS, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("seven", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("less constructs a field w/ parameter name") + public void lessParameterCtor() { + final Field field = Field.less("Lesser", "seven", ":max"); + assertEquals("Lesser", field.getName(), "Field name not filled correctly"); + assertEquals(Op.LESS, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("seven", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertEquals(":max", field.getParameterName(), "Field parameter name not filled correctly"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("lessOrEqual constructs a field w/o parameter name") + public void lessOrEqualCtor() { + final Field field = Field.lessOrEqual("Nobody", "KNOWS"); + assertEquals("Nobody", field.getName(), "Field name not filled correctly"); + assertEquals(Op.LESS_OR_EQUAL, field.getComparison().getOp(), + "Field comparison operation not filled correctly"); + assertEquals("KNOWS", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("lessOrEqual constructs a field w/ parameter name") + public void lessOrEqualParameterCtor() { + final Field field = Field.lessOrEqual("Nobody", "KNOWS", ":nope"); + assertEquals("Nobody", field.getName(), "Field name not filled correctly"); + assertEquals(Op.LESS_OR_EQUAL, field.getComparison().getOp(), + "Field comparison operation not filled correctly"); + assertEquals("KNOWS", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertEquals(":nope", field.getParameterName(), "Field parameter name not filled correctly"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("notEqual constructs a field w/o parameter name") + public void notEqualCtor() { + final Field field = Field.notEqual("Park", "here"); + assertEquals("Park", field.getName(), "Field name not filled correctly"); + assertEquals(Op.NOT_EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("here", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("notEqual constructs a field w/ parameter name") + public void notEqualParameterCtor() { + final Field field = Field.notEqual("Park", "here", ":now"); + assertEquals("Park", field.getName(), "Field name not filled correctly"); + assertEquals(Op.NOT_EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("here", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertEquals(":now", field.getParameterName(), "Field parameter name not filled correctly"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("between constructs a field w/o parameter name") + public void betweenCtor() { + final Field> field = Field.between("Age", 18, 49); + assertEquals("Age", field.getName(), "Field name not filled correctly"); + assertEquals(Op.BETWEEN, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals(18, field.getComparison().getValue().getFirst(), + "Field comparison min value not filled correctly"); + assertEquals(49, field.getComparison().getValue().getSecond(), + "Field comparison max value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("between constructs a field w/ parameter name") + public void betweenParameterCtor() { + final Field> field = Field.between("Age", 18, 49, ":limit"); + assertEquals("Age", field.getName(), "Field name not filled correctly"); + assertEquals(Op.BETWEEN, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals(18, field.getComparison().getValue().getFirst(), + "Field comparison min value not filled correctly"); + assertEquals(49, field.getComparison().getValue().getSecond(), + "Field comparison max value not filled correctly"); + assertEquals(":limit", field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("any constructs a field w/o parameter name") + public void anyCtor() { + final Field> field = Field.any("Here", List.of(8, 16, 32)); + assertEquals("Here", field.getName(), "Field name not filled correctly"); + assertEquals(Op.IN, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals(List.of(8, 16, 32), field.getComparison().getValue(), + "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("any constructs a field w/ parameter name") + public void anyParameterCtor() { + final Field> field = Field.any("Here", List.of(8, 16, 32), ":list"); + assertEquals("Here", field.getName(), "Field name not filled correctly"); + assertEquals(Op.IN, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals(List.of(8, 16, 32), field.getComparison().getValue(), + "Field comparison value not filled correctly"); + assertEquals(":list", field.getParameterName(), "Field parameter name not filled correctly"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("inArray constructs a field w/o parameter name") + public void inArrayCtor() { + final Field>> field = Field.inArray("ArrayField", "table", List.of("z")); + assertEquals("ArrayField", field.getName(), "Field name not filled correctly"); + assertEquals(Op.IN_ARRAY, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("table", field.getComparison().getValue().getFirst(), + "Field comparison table not filled correctly"); + assertEquals(List.of("z"), field.getComparison().getValue().getSecond(), + "Field comparison values not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("inArray constructs a field w/ parameter name") + public void inArrayParameterCtor() { + final Field>> field = Field.inArray("ArrayField", "table", List.of("z"), ":a"); + assertEquals("ArrayField", field.getName(), "Field name not filled correctly"); + assertEquals(Op.IN_ARRAY, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("table", field.getComparison().getValue().getFirst(), + "Field comparison table not filled correctly"); + assertEquals(List.of("z"), field.getComparison().getValue().getSecond(), + "Field comparison values not filled correctly"); + assertEquals(":a", field.getParameterName(), "Field parameter name not filled correctly"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("exists constructs a field") + public void existsCtor() { + final Field field = Field.exists("Groovy"); + assertEquals("Groovy", field.getName(), "Field name not filled correctly"); + assertEquals(Op.EXISTS, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("notExists constructs a field") + public void notExistsCtor() { + final Field field = Field.notExists("Groovy"); + assertEquals("Groovy", field.getName(), "Field name not filled correctly"); + assertEquals(Op.NOT_EXISTS, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("named constructs a field") + public void namedCtor() { + final Field field = Field.named("Tacos"); + assertEquals("Tacos", field.getName(), "Field name not filled correctly"); + assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly"); + assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly"); + assertNull(field.getParameterName(), "The parameter name should have been null"); + assertNull(field.getQualifier(), "The qualifier should have been null"); + } + + @Test + @DisplayName("static constructors fail for invalid parameter name") + public void staticCtorsFailOnParamName() { + assertThrows(DocumentException.class, () -> Field.equal("a", "b", "that ain't it, Jack...")); + } + + @Test + @DisplayName("nameToPath creates a simple PostgreSQL SQL name") + public void nameToPathPostgresSimpleSQL() { + assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not constructed correctly"); + } + + @Test + @DisplayName("nameToPath creates a simple SQLite SQL name") + public void nameToPathSQLiteSimpleSQL() { + assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL), + "Path not constructed correctly"); + } + + @Test + @DisplayName("nameToPath creates a nested PostgreSQL SQL name") + public void nameToPathPostgresNestedSQL() { + assertEquals("data#>>'{A,Long,Path,to,the,Property}'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not constructed correctly"); + } + + @Test + @DisplayName("nameToPath creates a nested SQLite SQL name") + public void nameToPathSQLiteNestedSQL() { + assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL), + "Path not constructed correctly"); + } + + @Test + @DisplayName("nameToPath creates a simple PostgreSQL JSON name") + public void nameToPathPostgresSimpleJSON() { + assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON), + "Path not constructed correctly"); + } + + @Test + @DisplayName("nameToPath creates a simple SQLite JSON name") + public void nameToPathSQLiteSimpleJSON() { + assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON), + "Path not constructed correctly"); + } + + @Test + @DisplayName("nameToPath creates a nested PostgreSQL JSON name") + public void nameToPathPostgresNestedJSON() { + assertEquals("data#>'{A,Long,Path,to,the,Property}'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON), + "Path not constructed correctly"); + } + + @Test + @DisplayName("nameToPath creates a nested SQLite JSON name") + public void nameToPathSQLiteNestedJSON() { + assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON), + "Path not constructed correctly"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FindQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FindQueryTest.java new file mode 100644 index 0000000..900c96b --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FindQueryTest.java @@ -0,0 +1,102 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Find` object + */ +@DisplayName("Core | Java | Query | FindQuery") +final public class FindQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("all generates correctly") + public void all() { + assertEquals(String.format("SELECT data FROM %s", TEST_TABLE), FindQuery.all(TEST_TABLE), + "Find query not constructed correctly"); + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + public void byIdPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("SELECT data FROM %s WHERE data->>'id' = :id", TEST_TABLE), + FindQuery.byId(TEST_TABLE), "Find query not constructed correctly"); + } + + @Test + @DisplayName("byId generates correctly | SQLite") + public void byIdSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("SELECT data FROM %s WHERE data->>'id' = :id", TEST_TABLE), + FindQuery.byId(TEST_TABLE), "Find query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + public void byFieldsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals( + String.format("SELECT data FROM %s WHERE data->>'a' = :b AND (data->>'c')::numeric < :d", TEST_TABLE), + FindQuery.byFields(TEST_TABLE, List.of(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))), + "Find query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + public void byFieldsSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("SELECT data FROM %s WHERE data->>'a' = :b AND data->>'c' < :d", TEST_TABLE), + FindQuery.byFields(TEST_TABLE, List.of(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))), + "Find query not constructed correctly"); + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + public void byContainsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("SELECT data FROM %s WHERE data @> :criteria", TEST_TABLE), + FindQuery.byContains(TEST_TABLE), "Find query not constructed correctly"); + } + + @Test + @DisplayName("byContains fails | SQLite") + public void byContainsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> FindQuery.byContains(TEST_TABLE)); + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + public void byJsonPathPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("SELECT data FROM %s WHERE jsonb_path_exists(data, :path::jsonpath)", TEST_TABLE), + FindQuery.byJsonPath(TEST_TABLE), "Find query not constructed correctly"); + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + public void byJsonPathSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> FindQuery.byJsonPath(TEST_TABLE)); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/IntIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/IntIdClass.java new file mode 100644 index 0000000..2acdef6 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/IntIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.core.tests.java; + +public class IntIdClass { + + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public IntIdClass(int id) { + this.id = id; + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/LongIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/LongIdClass.java new file mode 100644 index 0000000..0ea90a9 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/LongIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.core.tests.java; + +public class LongIdClass { + + private long id; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public LongIdClass(long id) { + this.id = id; + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/OpTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/OpTest.java new file mode 100644 index 0000000..5ecfd48 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/OpTest.java @@ -0,0 +1,80 @@ +package solutions.bitbadger.documents.core.tests.java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.Op; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the `Op` enum + */ +@DisplayName("Core | Java | Op") +final public class OpTest { + + @Test + @DisplayName("EQUAL uses proper SQL") + public void equalSQL() { + assertEquals("=", Op.EQUAL.getSql(), "The SQL for equal is incorrect"); + } + + @Test + @DisplayName("GREATER uses proper SQL") + public void greaterSQL() { + assertEquals(">", Op.GREATER.getSql(), "The SQL for greater is incorrect"); + } + + @Test + @DisplayName("GREATER_OR_EQUAL uses proper SQL") + public void greaterOrEqualSQL() { + assertEquals(">=", Op.GREATER_OR_EQUAL.getSql(), "The SQL for greater-or-equal is incorrect"); + } + + @Test + @DisplayName("LESS uses proper SQL") + public void lessSQL() { + assertEquals("<", Op.LESS.getSql(), "The SQL for less is incorrect"); + } + + @Test + @DisplayName("LESS_OR_EQUAL uses proper SQL") + public void lessOrEqualSQL() { + assertEquals("<=", Op.LESS_OR_EQUAL.getSql(), "The SQL for less-or-equal is incorrect"); + } + + @Test + @DisplayName("NOT_EQUAL uses proper SQL") + public void notEqualSQL() { + assertEquals("<>", Op.NOT_EQUAL.getSql(), "The SQL for not-equal is incorrect"); + } + + @Test + @DisplayName("BETWEEN uses proper SQL") + public void betweenSQL() { + assertEquals("BETWEEN", Op.BETWEEN.getSql(), "The SQL for between is incorrect"); + } + + @Test + @DisplayName("IN uses proper SQL") + public void inSQL() { + assertEquals("IN", Op.IN.getSql(), "The SQL for in is incorrect"); + } + + @Test + @DisplayName("IN_ARRAY uses proper SQL") + public void inArraySQL() { + assertEquals("??|", Op.IN_ARRAY.getSql(), "The SQL for in-array is incorrect"); + } + + @Test + @DisplayName("EXISTS uses proper SQL") + public void existsSQL() { + assertEquals("IS NOT NULL", Op.EXISTS.getSql(), "The SQL for exists is incorrect"); + } + + @Test + @DisplayName("NOT_EXISTS uses proper SQL") + public void notExistsSQL() { + assertEquals("IS NULL", Op.NOT_EXISTS.getSql(), "The SQL for not-exists is incorrect"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterNameTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterNameTest.java new file mode 100644 index 0000000..32d88e7 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterNameTest.java @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.core.tests.java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.ParameterName; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the `ParameterName` class + */ +@DisplayName("Core | Java | ParameterName") +final public class ParameterNameTest { + + @Test + @DisplayName("derive works when given existing names") + public void withExisting() { + ParameterName names = new ParameterName(); + assertEquals(":taco", names.derive(":taco"), "Name should have been :taco"); + assertEquals(":field0", names.derive(null), "Counter should not have advanced for named field"); + } + + @Test + @DisplayName("derive works when given all anonymous fields") + public void allAnonymous() { + ParameterName names = new ParameterName(); + assertEquals(":field0", names.derive(null), "Anonymous field name should have been returned"); + assertEquals(":field1", names.derive(null), "Counter should have advanced from previous call"); + assertEquals(":field2", names.derive(null), "Counter should have advanced from previous call"); + assertEquals(":field3", names.derive(null), "Counter should have advanced from previous call"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterTest.java new file mode 100644 index 0000000..abe2ae3 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterTest.java @@ -0,0 +1,40 @@ +package solutions.bitbadger.documents.core.tests.java; + +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 org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `Parameter` class + */ +@DisplayName("Core | Java | Parameter") +final public class ParameterTest { + + @Test + @DisplayName("Construction with colon-prefixed name") + public void ctorWithColon() { + Parameter p = new Parameter<>(":test", ParameterType.STRING, "ABC"); + assertEquals(":test", p.getName(), "Parameter name was incorrect"); + assertEquals(ParameterType.STRING, p.getType(), "Parameter type was incorrect"); + assertEquals("ABC", p.getValue(), "Parameter value was incorrect"); + } + + @Test + @DisplayName("Construction with at-sign-prefixed name") + public void ctorWithAtSign() { + Parameter p = new Parameter<>("@yo", ParameterType.NUMBER, null); + assertEquals("@yo", p.getName(), "Parameter name was incorrect"); + assertEquals(ParameterType.NUMBER, p.getType(), "Parameter type was incorrect"); + assertNull(p.getValue(), "Parameter value was incorrect"); + } + + @Test + @DisplayName("Construction fails with incorrect prefix") + public void ctorFailsForPrefix() { + assertThrows(DocumentException.class, () -> new Parameter<>("it", ParameterType.JSON, "")); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParametersTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParametersTest.java new file mode 100644 index 0000000..c74fac6 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParametersTest.java @@ -0,0 +1,121 @@ +package solutions.bitbadger.documents.core.tests.java; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.*; +import solutions.bitbadger.documents.java.Parameters; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `Parameters` object + */ +@DisplayName("Core | Java | Parameters") +final public class ParametersTest { + + /** + * Reset the dialect + */ + @AfterEach + public void cleanUp() { + Configuration.setConnectionString(null); + } + + @Test + @DisplayName("nameFields works with no changes") + public void nameFieldsNoChange() { + List> fields = List.of(Field.equal("a", "", ":test"), Field.exists("q"), Field.equal("b", "", ":me")); + Field[] named = Parameters.nameFields(fields).toArray(new Field[] { }); + assertEquals(fields.size(), named.length, "There should have been 3 fields in the list"); + assertSame(fields.get(0), named[0], "The first field should be the same"); + assertSame(fields.get(1), named[1], "The second field should be the same"); + assertSame(fields.get(2), named[2], "The third field should be the same"); + } + + @Test + @DisplayName("nameFields works when changing fields") + public void nameFieldsChange() { + List> fields = List.of( + Field.equal("a", ""), Field.equal("e", "", ":hi"), Field.equal("b", ""), Field.notExists("z")); + Field[] named = Parameters.nameFields(fields).toArray(new Field[] { }); + assertEquals(fields.size(), named.length, "There should have been 4 fields in the list"); + assertNotSame(fields.get(0), named[0], "The first field should not be the same"); + assertEquals(":field0", named[0].getParameterName(), "First parameter name incorrect"); + assertSame(fields.get(1), named[1], "The second field should be the same"); + assertNotSame(fields.get(2), named[2], "The third field should not be the same"); + assertEquals(":field1", named[2].getParameterName(), "Third parameter name incorrect"); + assertSame(fields.get(3), named[3], "The fourth field should be the same"); + } + + @Test + @DisplayName("replaceNamesInQuery replaces successfully") + public void replaceNamesInQuery() { + List> parameters = List.of(new Parameter<>(":data", ParameterType.JSON, "{}"), + new Parameter<>(":data_ext", ParameterType.STRING, "")); + String query = + "SELECT data, data_ext FROM tbl WHERE data = :data AND data_ext = :data_ext AND more_data = :data"; + assertEquals("SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?", + Parameters.replaceNamesInQuery(query, parameters), "Parameters not replaced correctly"); + } + + @Test + @DisplayName("fieldNames generates a single parameter (PostgreSQL)") + public void fieldNamesSinglePostgres() throws DocumentException { + Configuration.setConnectionString(":postgresql:"); + Parameter[] nameParams = Parameters.fieldNames(List.of("test")).toArray(new Parameter[]{}); + assertEquals(1, nameParams.length, "There should be one name parameter"); + assertEquals(":name", nameParams[0].getName(), "The parameter name is incorrect"); + assertEquals(ParameterType.STRING, nameParams[0].getType(), "The parameter type is incorrect"); + assertEquals("{test}", nameParams[0].getValue(), "The parameter value is incorrect"); + } + + @Test + @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") + public void fieldNamesMultiplePostgres() throws DocumentException { + Configuration.setConnectionString(":postgresql:"); + Parameter[] nameParams = Parameters.fieldNames(List.of("test", "this", "today")) + .toArray(new Parameter[]{}); + assertEquals(1, nameParams.length, "There should be one name parameter"); + assertEquals(":name", nameParams[0].getName(), "The parameter name is incorrect"); + assertEquals(ParameterType.STRING, nameParams[0].getType(), "The parameter type is incorrect"); + assertEquals("{test,this,today}", nameParams[0].getValue(), "The parameter value is incorrect"); + } + + @Test + @DisplayName("fieldNames generates a single parameter (SQLite)") + public void fieldNamesSingleSQLite() throws DocumentException { + Configuration.setConnectionString(":sqlite:"); + Parameter[] nameParams = Parameters.fieldNames(List.of("test")).toArray(new Parameter[]{}); + assertEquals(1, nameParams.length, "There should be one name parameter"); + assertEquals(":name0", nameParams[0].getName(), "The parameter name is incorrect"); + assertEquals(ParameterType.STRING, nameParams[0].getType(), "The parameter type is incorrect"); + assertEquals("test", nameParams[0].getValue(), "The parameter value is incorrect"); + } + + @Test + @DisplayName("fieldNames generates multiple parameters (SQLite)") + public void fieldNamesMultipleSQLite() throws DocumentException { + Configuration.setConnectionString(":sqlite:"); + Parameter[] nameParams = Parameters.fieldNames(List.of("test", "this", "today")) + .toArray(new Parameter[]{}); + assertEquals(3, nameParams.length, "There should be one name parameter"); + assertEquals(":name0", nameParams[0].getName(), "The first parameter name is incorrect"); + assertEquals(ParameterType.STRING, nameParams[0].getType(), "The first parameter type is incorrect"); + assertEquals("test", nameParams[0].getValue(), "The first parameter value is incorrect"); + assertEquals(":name1", nameParams[1].getName(), "The second parameter name is incorrect"); + assertEquals(ParameterType.STRING, nameParams[1].getType(), "The second parameter type is incorrect"); + assertEquals("this", nameParams[1].getValue(), "The second parameter value is incorrect"); + assertEquals(":name2", nameParams[2].getName(), "The third parameter name is incorrect"); + assertEquals(ParameterType.STRING, nameParams[2].getType(), "The third parameter type is incorrect"); + assertEquals("today", nameParams[2].getValue(), "The third parameter value is incorrect"); + } + + @Test + @DisplayName("fieldNames fails if dialect not set") + public void fieldNamesFails() { + assertThrows(DocumentException.class, () -> Parameters.fieldNames(List.of())); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/PatchQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/PatchQueryTest.java new file mode 100644 index 0000000..f0f6b7a --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/PatchQueryTest.java @@ -0,0 +1,97 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Patch` object + */ +@DisplayName("Core | Java | Query | PatchQuery") +final public class PatchQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + public void byIdPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("UPDATE %s SET data = data || :data WHERE data->>'id' = :id", TEST_TABLE), + PatchQuery.byId(TEST_TABLE), "Patch query not constructed correctly"); + } + + @Test + @DisplayName("byId generates correctly | SQLite") + public void byIdSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format( + "UPDATE %s SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id", TEST_TABLE), + PatchQuery.byId(TEST_TABLE), "Patch query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + public void byFieldsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("UPDATE %s SET data = data || :data WHERE data->>'z' = :y", TEST_TABLE), + PatchQuery.byFields(TEST_TABLE, List.of(Field.equal("z", "", ":y"))), + "Patch query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + public void byFieldsSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format( + "UPDATE %s SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y", TEST_TABLE), + PatchQuery.byFields(TEST_TABLE, List.of(Field.equal("z", "", ":y"))), + "Patch query not constructed correctly"); + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + public void byContainsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("UPDATE %s SET data = data || :data WHERE data @> :criteria", TEST_TABLE), + PatchQuery.byContains(TEST_TABLE), "Patch query not constructed correctly"); + } + + @Test + @DisplayName("byContains fails | SQLite") + public void byContainsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> PatchQuery.byContains(TEST_TABLE)); + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + public void byJsonPathPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("UPDATE %s SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)", + TEST_TABLE), + PatchQuery.byJsonPath(TEST_TABLE), "Patch query not constructed correctly"); + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + public void byJsonPathSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> PatchQuery.byJsonPath(TEST_TABLE)); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/QueryUtilsTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/QueryUtilsTest.java new file mode 100644 index 0000000..48d8ffb --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/QueryUtilsTest.java @@ -0,0 +1,166 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.FieldMatch; +import solutions.bitbadger.documents.query.QueryUtils; +import solutions.bitbadger.documents.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the package-level query functions, presented as `QueryUtils` for Java-compatible use + */ +@DisplayName("Core | Java | Query | QueryUtils") +final public class QueryUtilsTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("statementWhere generates correctly") + public void statementWhere() { + assertEquals("x WHERE y", QueryUtils.statementWhere("x", "y"), "Statements not combined correctly"); + } + + @Test + @DisplayName("byId generates a numeric ID query | PostgreSQL") + public void byIdNumericPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("test WHERE (data->>'id')::numeric = :id", QueryUtils.byId("test", 9)); + } + + @Test + @DisplayName("byId generates an alphanumeric ID query | PostgreSQL") + public void byIdAlphaPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("unit WHERE data->>'id' = :id", QueryUtils.byId("unit", "18")); + } + + @Test + @DisplayName("byId generates ID query | SQLite") + public void byIdSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("yo WHERE data->>'id' = :id", QueryUtils.byId("yo", 27)); + } + + @Test + @DisplayName("byFields generates default field query | PostgreSQL") + public void byFieldsMultipleDefaultPostgres() throws DocumentException { + 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") + public void byFieldsMultipleDefaultSQLite() throws DocumentException { + 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") + public void byFieldsMultipleAnyPostgres() throws DocumentException { + 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") + public void byFieldsMultipleAnySQLite() throws DocumentException { + 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") + public void orderByNone() throws DocumentException { + assertEquals("", QueryUtils.orderBy(List.of(), Dialect.POSTGRESQL), + "ORDER BY should have been blank (PostgreSQL)"); + assertEquals("", QueryUtils.orderBy(List.of(), Dialect.SQLITE), "ORDER BY should have been blank (SQLite)"); + } + + @Test + @DisplayName("orderBy generates single, no direction | PostgreSQL") + public void orderBySinglePostgres() throws DocumentException { + assertEquals(" ORDER BY data->>'TestField'", + QueryUtils.orderBy(List.of(Field.named("TestField")), Dialect.POSTGRESQL), + "ORDER BY not constructed correctly"); + } + + @Test + @DisplayName("orderBy generates single, no direction | SQLite") + public void orderBySingleSQLite() throws DocumentException { + assertEquals(" ORDER BY data->>'TestField'", + QueryUtils.orderBy(List.of(Field.named("TestField")), Dialect.SQLITE), + "ORDER BY not constructed correctly"); + } + + @Test + @DisplayName("orderBy generates multiple with direction | PostgreSQL") + public void orderByMultiplePostgres() throws DocumentException { + assertEquals(" 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), + "ORDER BY not constructed correctly"); + } + + @Test + @DisplayName("orderBy generates multiple with direction | SQLite") + public void orderByMultipleSQLite() throws DocumentException { + assertEquals(" 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), + "ORDER BY not constructed correctly"); + } + + @Test + @DisplayName("orderBy generates numeric ordering | PostgreSQL") + public void orderByNumericPostgres() throws DocumentException { + assertEquals(" ORDER BY (data->>'Test')::numeric", + QueryUtils.orderBy(List.of(Field.named("n:Test")), Dialect.POSTGRESQL), + "ORDER BY not constructed correctly"); + } + + @Test + @DisplayName("orderBy generates numeric ordering | SQLite") + public void orderByNumericSQLite() throws DocumentException { + assertEquals(" ORDER BY data->>'Test'", QueryUtils.orderBy(List.of(Field.named("n:Test")), Dialect.SQLITE), + "ORDER BY not constructed correctly"); + } + + @Test + @DisplayName("orderBy generates case-insensitive ordering | PostgreSQL") + public void orderByCIPostgres() throws DocumentException { + assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", + QueryUtils.orderBy(List.of(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL), + "ORDER BY not constructed correctly"); + } + + @Test + @DisplayName("orderBy generates case-insensitive ordering | SQLite") + public void orderByCISQLite() throws DocumentException { + assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", + QueryUtils.orderBy(List.of(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE), + "ORDER BY not constructed correctly"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/RemoveFieldsQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/RemoveFieldsQueryTest.java new file mode 100644 index 0000000..10d9de0 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/RemoveFieldsQueryTest.java @@ -0,0 +1,110 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.Parameter; +import solutions.bitbadger.documents.ParameterType; +import solutions.bitbadger.documents.query.RemoveFieldsQuery; +import solutions.bitbadger.documents.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `RemoveFields` object + */ +@DisplayName("Core | Java | Query | RemoveFieldsQuery") +final public class RemoveFieldsQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + public void byIdPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("UPDATE %s SET data = data - :name::text[] WHERE data->>'id' = :id", TEST_TABLE), + RemoveFieldsQuery.byId(TEST_TABLE, List.of(new Parameter<>(":name", ParameterType.STRING, "{a,z}"))), + "Remove Fields query not constructed correctly"); + } + + @Test + @DisplayName("byId generates correctly | SQLite") + public void byIdSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("UPDATE %s SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id", + TEST_TABLE), + RemoveFieldsQuery.byId(TEST_TABLE, List.of(new Parameter<>(":name0", ParameterType.STRING, "a"), + new Parameter<>(":name1", ParameterType.STRING, "z"))), + "Remove Field query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + public void byFieldsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("UPDATE %s SET data = data - :name::text[] WHERE data->>'f' > :g", TEST_TABLE), + RemoveFieldsQuery.byFields(TEST_TABLE, List.of(new Parameter<>(":name", ParameterType.STRING, "{b,c}")), + List.of(Field.greater("f", "", ":g"))), + "Remove Field query not constructed correctly"); + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + public void byFieldsSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals(String.format("UPDATE %s SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g", + TEST_TABLE), + RemoveFieldsQuery.byFields(TEST_TABLE, List.of(new Parameter<>(":name0", ParameterType.STRING, "b"), + new Parameter<>(":name1", ParameterType.STRING, "c")), + List.of(Field.greater("f", "", ":g"))), + "Remove Field query not constructed correctly"); + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + public void byContainsPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format("UPDATE %s SET data = data - :name::text[] WHERE data @> :criteria", TEST_TABLE), + RemoveFieldsQuery.byContains(TEST_TABLE, + List.of(new Parameter<>(":name", ParameterType.STRING, "{m,n}"))), + "Remove Field query not constructed correctly"); + } + + @Test + @DisplayName("byContains fails | SQLite") + public void byContainsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> RemoveFieldsQuery.byContains(TEST_TABLE, List.of())); + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + public void byJsonPathPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals(String.format( + "UPDATE %s SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)", + TEST_TABLE), + RemoveFieldsQuery.byJsonPath(TEST_TABLE, + List.of(new Parameter<>(":name", ParameterType.STRING, "{o,p}"))), + "Remove Field query not constructed correctly"); + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + public void byJsonPathSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, () -> RemoveFieldsQuery.byJsonPath(TEST_TABLE, List.of())); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ShortIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ShortIdClass.java new file mode 100644 index 0000000..f064677 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ShortIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.core.tests.java; + +public class ShortIdClass { + + private short id; + + public short getId() { + return id; + } + + public void setId(short id) { + this.id = id; + } + + public ShortIdClass(short id) { + this.id = id; + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/StringIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/StringIdClass.java new file mode 100644 index 0000000..b47d34d --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/StringIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.core.tests.java; + +public class StringIdClass { + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public StringIdClass(String id) { + this.id = id; + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/WhereTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/WhereTest.java new file mode 100644 index 0000000..575effa --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/WhereTest.java @@ -0,0 +1,172 @@ +package solutions.bitbadger.documents.core.tests.java; + +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.FieldMatch; +import solutions.bitbadger.documents.query.Where; +import solutions.bitbadger.documents.core.tests.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests for the `Where` object + */ +@DisplayName("Core | Java | Query | Where") +final public class WhereTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + ForceDialect.none(); + } + + @Test + @DisplayName("byFields is blank when given no fields") + public void byFieldsBlankIfEmpty() throws DocumentException { + assertEquals("", Where.byFields(List.of())); + } + + @Test + @DisplayName("byFields generates one numeric field | PostgreSQL") + public void byFieldsOneFieldPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("(data->>'it')::numeric = :that", Where.byFields(List.of(Field.equal("it", 9, ":that")))); + } + + @Test + @DisplayName("byFields generates one alphanumeric field | PostgreSQL") + public void byFieldsOneAlphaFieldPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'it' = :that", Where.byFields(List.of(Field.equal("it", "", ":that")))); + } + + @Test + @DisplayName("byFields generates one field | SQLite") + public void byFieldsOneFieldSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'it' = :that", Where.byFields(List.of(Field.equal("it", "", ":that")))); + } + + @Test + @DisplayName("byFields generates multiple fields w/ default match | PostgreSQL") + public void byFieldsMultipleDefaultPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three", + Where.byFields(List.of( + Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")))); + } + + @Test + @DisplayName("byFields generates multiple fields w/ default match | SQLite") + public void byFieldsMultipleDefaultSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three", + Where.byFields(List.of( + Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")))); + } + + @Test + @DisplayName("byFields generates multiple fields w/ ANY match | PostgreSQL") + public void byFieldsMultipleAnyPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three", + Where.byFields(List.of( + Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")), + FieldMatch.ANY)); + } + + @Test + @DisplayName("byFields generates multiple fields w/ ANY match | SQLite") + public void byFieldsMultipleAnySQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three", + Where.byFields(List.of( + Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")), + FieldMatch.ANY)); + } + + @Test + @DisplayName("byId generates defaults for alphanumeric key | PostgreSQL") + public void byIdDefaultAlphaPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'id' = :id", Where.byId(":id", "")); + } + + @Test + @DisplayName("byId generates defaults for numeric key | PostgreSQL") + public void byIdDefaultNumericPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("(data->>'id')::numeric = :id", Where.byId(":id", 5)); + } + + @Test + @DisplayName("byId generates defaults | SQLite") + public void byIdDefaultSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'id' = :id", Where.byId(":id", "")); + } + + @Test + @DisplayName("byId generates named ID | PostgreSQL") + public void byIdDefaultNamedPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data->>'id' = :key", Where.byId(":key")); + } + + @Test + @DisplayName("byId generates named ID | SQLite") + public void byIdDefaultNamedSQLite() throws DocumentException { + ForceDialect.sqlite(); + assertEquals("data->>'id' = :key", Where.byId(":key")); + } + + @Test + @DisplayName("jsonContains generates defaults | PostgreSQL") + public void jsonContainsDefaultPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data @> :criteria", Where.jsonContains()); + } + + @Test + @DisplayName("jsonContains generates named parameter | PostgreSQL") + public void jsonContainsNamedPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("data @> :it", Where.jsonContains(":it")); + } + + @Test + @DisplayName("jsonContains fails | SQLite") + public void jsonContainsFailsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, Where::jsonContains); + } + + @Test + @DisplayName("jsonPathMatches generates defaults | PostgreSQL") + public void jsonPathMatchDefaultPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("jsonb_path_exists(data, :path::jsonpath)", Where.jsonPathMatches()); + } + + @Test + @DisplayName("jsonPathMatches generates named parameter | PostgreSQL") + public void jsonPathMatchNamedPostgres() throws DocumentException { + ForceDialect.postgres(); + assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Where.jsonPathMatches(":jp")); + } + + @Test + @DisplayName("jsonPathMatches fails | SQLite") + public void jsonPathFailsSQLite() { + ForceDialect.sqlite(); + assertThrows(DocumentException.class, Where::jsonPathMatches); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ArrayDocument.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ArrayDocument.java new file mode 100644 index 0000000..482160b --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ArrayDocument.java @@ -0,0 +1,41 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import java.util.List; + +public class ArrayDocument { + + private String id; + private List values; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } + + public ArrayDocument(String id, List values) { + this.id = id; + this.values = values; + } + + public ArrayDocument() { + this("", List.of()); + } + + public static List testDocuments = + List.of( + new ArrayDocument("first", List.of("a", "b", "c")), + new ArrayDocument("second", List.of("c", "d", "e")), + new ArrayDocument("third", List.of("x", "y", "z"))); + +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CountFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CountFunctions.java new file mode 100644 index 0000000..b851ce0 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CountFunctions.java @@ -0,0 +1,62 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +/** + * Integration tests for the `Count` object + */ +final public class CountFunctions { + + public static void all(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should have been 5 documents in the table"); + } + + public static void byFieldsNumeric(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(3L, countByFields(db.getConn(), TEST_TABLE, List.of(Field.between("numValue", 10, 20))), + "There should have been 3 matching documents"); + } + + public static void byFieldsAlpha(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(1L, countByFields(db.getConn(), TEST_TABLE, List.of(Field.between("value", "aardvark", "apple"))), + "There should have been 1 matching document"); + } + + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(2L, countByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")), + "There should have been 2 matching documents"); + } + + public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(0L, countByContains(db.getConn(), TEST_TABLE, Map.of("value", "magenta")), + "There should have been no matching documents"); + } + + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(2L, countByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ < 5)"), + "There should have been 2 matching documents"); + } + + public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(0L, countByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no matching documents"); + } + + private CountFunctions() { + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java new file mode 100644 index 0000000..f0a2dc9 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java @@ -0,0 +1,146 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.*; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; +import solutions.bitbadger.documents.java.Results; +import solutions.bitbadger.documents.query.CountQuery; +import solutions.bitbadger.documents.query.DeleteQuery; +import solutions.bitbadger.documents.query.FindQuery; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; +import static solutions.bitbadger.documents.query.QueryUtils.orderBy; + +final public class CustomFunctions { + + public static void listEmpty(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + deleteByFields(db.getConn(), TEST_TABLE, List.of(Field.exists(Configuration.idField))); + Collection result = + customList(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData); + assertEquals(0, result.size(), "There should have been no results"); + } + + public static void listAll(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + Collection result = + customList(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData); + assertEquals(5, result.size(), "There should have been 5 results"); + } + + public static void jsonArrayEmpty(ThrowawayDatabase db) throws DocumentException { + assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "The test table should be empty"); + assertEquals("[]", customJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData), + "An empty list was not represented correctly"); + } + + public static void jsonArraySingle(ThrowawayDatabase db) throws DocumentException { + insert(db.getConn(), TEST_TABLE, new ArrayDocument("one", List.of("2", "3"))); + assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), + customJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData), + "A single document list was not represented correctly"); + } + + public static void jsonArrayMany(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"first\",\"values\":[\"a\",\"b\",\"c\"]}," + + "{\"id\":\"second\",\"values\":[\"c\",\"d\",\"e\"]}," + + "{\"id\":\"third\",\"values\":[\"x\",\"y\",\"z\"]}]"), + customJsonArray(db.getConn(), FindQuery.all(TEST_TABLE) + orderBy(List.of(Field.named("id"))), + List.of(), Results::jsonFromData), + "A multiple document list was not represented correctly"); + } + + public static void writeJsonArrayEmpty(ThrowawayDatabase db) throws DocumentException { + assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "The test table should be empty"); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), writer, Results::jsonFromData); + assertEquals("[]", output.toString(), "An empty list was not represented correctly"); + } + + public static void writeJsonArraySingle(ThrowawayDatabase db) throws DocumentException { + insert(db.getConn(), TEST_TABLE, new ArrayDocument("one", List.of("2", "3"))); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), writer, Results::jsonFromData); + assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), output.toString(), + "A single document list was not represented correctly"); + } + + public static void writeJsonArrayMany(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE) + orderBy(List.of(Field.named("id"))), List.of(), + writer, Results::jsonFromData); + assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"first\",\"values\":[\"a\",\"b\",\"c\"]}," + + "{\"id\":\"second\",\"values\":[\"c\",\"d\",\"e\"]}," + + "{\"id\":\"third\",\"values\":[\"x\",\"y\",\"z\"]}]"), + output.toString(), "A multiple document list was not represented correctly"); + } + + public static void singleNone(ThrowawayDatabase db) throws DocumentException { + assertFalse( + customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData) + .isPresent(), + "There should not have been a document returned"); + } + + public static void singleOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertTrue( + customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData) + .isPresent(), + "There should have been a document returned"); + } + + public static void jsonSingleNone(ThrowawayDatabase db) throws DocumentException { + assertEquals("{}", customJsonSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData), + "An empty document was not represented correctly"); + } + + public static void jsonSingleOne(ThrowawayDatabase db) throws DocumentException { + insert(db.getConn(), TEST_TABLE, new ArrayDocument("me", List.of("myself", "i"))); + assertEquals(JsonFunctions.maybeJsonB("{\"id\":\"me\",\"values\":[\"myself\",\"i\"]}"), + customJsonSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData), + "A single document was not represented correctly"); + } + + public static void nonQueryChanges(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, + customScalar(db.getConn(), CountQuery.all(TEST_TABLE), List.of(), Long.class, Results::toCount), + "There should have been 5 documents in the table"); + customNonQuery(db.getConn(), String.format("DELETE FROM %s", TEST_TABLE)); + assertEquals(0L, + customScalar(db.getConn(), CountQuery.all(TEST_TABLE), List.of(), Long.class, Results::toCount), + "There should have been no documents in the table"); + } + + public static void nonQueryNoChanges(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, + customScalar(db.getConn(), CountQuery.all(TEST_TABLE), List.of(), Long.class, Results::toCount), + "There should have been 5 documents in the table"); + customNonQuery(db.getConn(), DeleteQuery.byId(TEST_TABLE, "eighty-two"), + List.of(new Parameter<>(":id", ParameterType.STRING, "eighty-two"))); + assertEquals(5L, + customScalar(db.getConn(), CountQuery.all(TEST_TABLE), List.of(), Long.class, Results::toCount), + "There should still have been 5 documents in the table"); + } + + public static void scalar(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(3L, + customScalar(db.getConn(), String.format("SELECT 3 AS it FROM %s LIMIT 1", TEST_TABLE), List.of(), + Long.class, Results::toCount), + "The number 3 should have been returned"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DefinitionFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DefinitionFunctions.java new file mode 100644 index 0000000..13d2336 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DefinitionFunctions.java @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.DocumentIndex; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +final public class DefinitionFunctions { + + public static void ensuresATable(ThrowawayDatabase db) throws DocumentException { + assertFalse(db.dbObjectExists("ensured"), "The 'ensured' table should not exist"); + assertFalse(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should not exist"); + ensureTable(db.getConn(), "ensured"); + assertTrue(db.dbObjectExists("ensured"), "The 'ensured' table should exist"); + assertTrue(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should now exist"); + } + + public static void ensuresAFieldIndex(ThrowawayDatabase db) throws DocumentException { + assertFalse(db.dbObjectExists(String.format("idx_%s_test", TEST_TABLE)), "The test index should not exist"); + ensureFieldIndex(db.getConn(), TEST_TABLE, "test", List.of("id", "category")); + assertTrue(db.dbObjectExists(String.format("idx_%s_test", TEST_TABLE)), "The test index should now exist"); + } + + public static void ensureDocumentIndexFull(ThrowawayDatabase db) throws DocumentException { + assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist"); + ensureTable(db.getConn(), "doc_table"); + assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist"); + assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist"); + ensureDocumentIndex(db.getConn(), "doc_table", DocumentIndex.FULL); + assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist"); + } + + public static void ensureDocumentIndexOptimized(ThrowawayDatabase db) throws DocumentException { + assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist"); + ensureTable(db.getConn(), "doc_table"); + assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist"); + assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist"); + ensureDocumentIndex(db.getConn(), "doc_table", DocumentIndex.OPTIMIZED); + assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DeleteFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DeleteFunctions.java new file mode 100644 index 0000000..89c02ee --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DeleteFunctions.java @@ -0,0 +1,71 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +final public class DeleteFunctions { + + public static void byIdMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table"); + deleteById(db.getConn(), TEST_TABLE, "four"); + assertEquals(4L, countAll(db.getConn(), TEST_TABLE), "There should now be 4 documents in the table"); + } + + public static void byIdNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table"); + deleteById(db.getConn(), TEST_TABLE, "negative four"); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should still be 5 documents in the table"); + } + + public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table"); + deleteByFields( db.getConn(), TEST_TABLE, List.of(Field.notEqual("value", "purple"))); + assertEquals(2L, countAll(db.getConn(), TEST_TABLE), "There should now be 2 documents in the table"); + } + + public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table"); + deleteByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "crimson"))); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should still be 5 documents in the table"); + } + + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table"); + deleteByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")); + assertEquals(3L, countAll(db.getConn(), TEST_TABLE), "There should now be 3 documents in the table"); + } + + public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table"); + deleteByContains(db.getConn(), TEST_TABLE, Map.of("target", "acquired")); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should still be 5 documents in the table"); + } + + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table"); + deleteByJsonPath(db.getConn(), TEST_TABLE, "$.value ? (@ == \"purple\")"); + assertEquals(3L, countAll(db.getConn(), TEST_TABLE), "There should now be 3 documents in the table"); + } + + public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table"); + deleteByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should still be 5 documents in the table"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DocumentFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DocumentFunctions.java new file mode 100644 index 0000000..6bec3a8 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/DocumentFunctions.java @@ -0,0 +1,138 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.AutoId; +import solutions.bitbadger.documents.Configuration; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +final public class DocumentFunctions { + + public static void insertDefault(ThrowawayDatabase db) throws DocumentException { + assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "There should be no documents in the table"); + insert(db.getConn(), TEST_TABLE, new JsonDocument("turkey", "", 0, new SubDocument("gobble", "gobble!"))); + final List after = findAll(db.getConn(), TEST_TABLE, JsonDocument.class); + assertEquals(1, after.size(), "There should be one document in the table"); + final JsonDocument doc = after.get(0); + assertEquals("turkey", doc.getId(), "The inserted document's ID is incorrect"); + assertEquals("", doc.getValue(), "The inserted document's value is incorrect"); + assertEquals(0, doc.getNumValue(), "The document's numeric value is incorrect"); + assertNotNull(doc.getSub(), "The inserted document's subdocument was not created"); + assertEquals("gobble", doc.getSub().getFoo(), "The subdocument's \"foo\" property is incorrect"); + assertEquals("gobble!", doc.getSub().getBar(), "The subdocument's \"bar\" property is incorrect"); + } + + public static void insertDupe(ThrowawayDatabase db) throws DocumentException { + insert(db.getConn(), TEST_TABLE, new JsonDocument("a", "", 0)); + assertThrows(DocumentException.class, () -> insert(db.getConn(), TEST_TABLE, new JsonDocument("a", "b", 22)), + "Inserting a document with a duplicate key should have thrown an exception"); + } + + public static void insertNumAutoId(ThrowawayDatabase db) throws DocumentException { + try { + Configuration.autoIdStrategy = AutoId.NUMBER; + Configuration.idField = "key"; + assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "There should be no documents in the table"); + + insert(db.getConn(), TEST_TABLE, new NumIdDocument(0, "one")); + insert(db.getConn(), TEST_TABLE, new NumIdDocument(0, "two")); + insert(db.getConn(), TEST_TABLE, new NumIdDocument(77, "three")); + insert(db.getConn(), TEST_TABLE, new NumIdDocument(0, "four")); + + final List after = findAll(db.getConn(), TEST_TABLE, NumIdDocument.class, + List.of(Field.named("key"))); + assertEquals(4, after.size(), "There should have been 4 documents returned"); + assertEquals("1|2|77|78", + after.stream().map(doc -> String.valueOf(doc.getKey())) + .reduce((acc, item) -> String.format("%s|%s", acc, item)).get(), + "The IDs were not generated correctly"); + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED; + Configuration.idField = "id"; + } + } + + public static void insertUUIDAutoId(ThrowawayDatabase db) throws DocumentException { + try { + Configuration.autoIdStrategy = AutoId.UUID; + assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "There should be no documents in the table"); + + insert(db.getConn(), TEST_TABLE, new JsonDocument("")); + + final List after = findAll(db.getConn(), TEST_TABLE, JsonDocument.class); + assertEquals(1, after.size(), "There should have been 1 document returned"); + assertEquals(32, after.get(0).getId().length(), "The ID was not generated correctly"); + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED; + } + } + + public static void insertStringAutoId(ThrowawayDatabase db) throws DocumentException { + try { + Configuration.autoIdStrategy = AutoId.RANDOM_STRING; + assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "There should be no documents in the table"); + + insert(db.getConn(), TEST_TABLE, new JsonDocument("")); + + Configuration.idStringLength = 21; + insert(db.getConn(), TEST_TABLE, new JsonDocument("")); + + final List after = findAll(db.getConn(), TEST_TABLE, JsonDocument.class); + assertEquals(2, after.size(), "There should have been 2 documents returned"); + assertEquals(16, after.get(0).getId().length(), "The first document's ID was not generated correctly"); + assertEquals(21, after.get(1).getId().length(), "The second document's ID was not generated correctly"); + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED; + Configuration.idStringLength = 16; + } + } + + public static void saveMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + save(db.getConn(), TEST_TABLE, new JsonDocument("two", "", 44)); + final Optional tryDoc = findById(db.getConn(), TEST_TABLE, "two", JsonDocument.class); + assertTrue(tryDoc.isPresent(), "There should have been a document returned"); + final JsonDocument doc = tryDoc.get(); + assertEquals("two", doc.getId(), "An incorrect document was returned"); + assertEquals("", doc.getValue(), "The \"value\" field was not updated"); + assertEquals(44, doc.getNumValue(), "The \"numValue\" field was not updated"); + assertNull(doc.getSub(), "The \"sub\" field was not updated"); + } + + public static void saveNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final JsonDocument toSave = new JsonDocument("test"); + toSave.setSub(new SubDocument("a", "b")); + save(db.getConn(), TEST_TABLE, toSave); + assertTrue(findById(db.getConn(), TEST_TABLE, "test", JsonDocument.class).isPresent(), + "The test document should have been saved"); + } + + public static void updateMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + update(db.getConn(), TEST_TABLE, "one", new JsonDocument("one", "howdy", 8, new SubDocument("y", "z"))); + final Optional tryDoc = findById(db.getConn(), TEST_TABLE, "one", JsonDocument.class); + assertTrue(tryDoc.isPresent(), "There should have been a document returned"); + final JsonDocument doc = tryDoc.get(); + assertEquals("one", doc.getId(), "An incorrect document was returned"); + assertEquals("howdy", doc.getValue(), "The \"value\" field was not updated"); + assertEquals(8, doc.getNumValue(), "The \"numValue\" field was not updated"); + assertNotNull(doc.getSub(), "The sub-document should not be null"); + assertEquals("y", doc.getSub().getFoo(), "The sub-document \"foo\" field was not updated"); + assertEquals("z", doc.getSub().getBar(), "The sub-document \"bar\" field was not updated"); + } + + public static void updateNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsById(db.getConn(), TEST_TABLE, "two-hundred")); + update(db.getConn(), TEST_TABLE, "two-hundred", new JsonDocument("two-hundred", "", 200)); + assertFalse(existsById(db.getConn(), TEST_TABLE, "two-hundred")); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ExistsFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ExistsFunctions.java new file mode 100644 index 0000000..48464e7 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ExistsFunctions.java @@ -0,0 +1,62 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +final public class ExistsFunctions { + + public static void byIdMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertTrue(existsById(db.getConn(), TEST_TABLE, "three"), "The document with ID \"three\" should exist"); + } + + public static void byIdNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsById(db.getConn(), TEST_TABLE, "seven"), "The document with ID \"seven\" should not exist"); + } + + public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertTrue(existsByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("numValue", 10))), + "Matching documents should have been found"); + } + + public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("nothing", "none"))), + "No matching documents should have been found"); + } + + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertTrue(existsByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")), + "Matching documents should have been found"); + } + + public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsByContains(db.getConn(), TEST_TABLE, Map.of("value", "violet")), + "Matching documents should not have been found"); + } + + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertTrue(existsByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10)"), + "Matching documents should have been found"); + } + + public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10.1)"), + "Matching documents should not have been found"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/FindFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/FindFunctions.java new file mode 100644 index 0000000..5dbe009 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/FindFunctions.java @@ -0,0 +1,279 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.Configuration; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.FieldMatch; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +final public class FindFunctions { + + private static String docIds(List docs) { + return docs.stream().map(JsonDocument::getId).reduce((acc, docId) -> String.format("%s|%s", acc, docId)).get(); + } + + public static void allDefault(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(5, findAll(db.getConn(), TEST_TABLE, JsonDocument.class).size(), + "There should have been 5 documents returned"); + } + + public static void allAscending(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findAll(db.getConn(), TEST_TABLE, JsonDocument.class, + List.of(Field.named("id"))); + assertEquals(5, docs.size(), "There should have been 5 documents returned"); + assertEquals("five|four|one|three|two", docIds(docs), "The documents were not ordered correctly"); + } + + public static void allDescending(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findAll(db.getConn(), TEST_TABLE, JsonDocument.class, + List.of(Field.named("id DESC"))); + assertEquals(5, docs.size(), "There should have been 5 documents returned"); + assertEquals("two|three|one|four|five", docIds(docs), "The documents were not ordered correctly"); + } + + public static void allNumOrder(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findAll(db.getConn(), TEST_TABLE, JsonDocument.class, + List.of(Field.named("sub.foo NULLS LAST"), Field.named("n:numValue"))); + assertEquals(5, docs.size(), "There should have been 5 documents returned"); + assertEquals("two|four|one|three|five", docIds(docs), "The documents were not ordered correctly"); + } + + public static void allEmpty(ThrowawayDatabase db) throws DocumentException { + assertEquals(0, findAll(db.getConn(), TEST_TABLE, JsonDocument.class).size(), + "There should have been no documents returned"); + } + + public static void byIdString(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findById(db.getConn(), TEST_TABLE, "two", JsonDocument.class); + assertTrue(doc.isPresent(), "The document should have been returned"); + assertEquals("two", doc.get().getId(), "An incorrect document was returned"); + } + + public static void byIdNumber(ThrowawayDatabase db) throws DocumentException { + Configuration.idField = "key"; + try { + insert(db.getConn(), TEST_TABLE, new NumIdDocument(18, "howdy")); + final Optional doc = findById(db.getConn(), TEST_TABLE, 18, NumIdDocument.class); + assertTrue(doc.isPresent(), "The document should have been returned"); + } finally { + Configuration.idField = "id"; + } + } + + public static void byIdNotFound(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(findById(db.getConn(), TEST_TABLE, "x", JsonDocument.class).isPresent(), + "There should have been no document returned"); + } + + public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findByFields(db.getConn(), TEST_TABLE, + List.of(Field.any("value", List.of("blue", "purple")), Field.exists("sub")), JsonDocument.class, + FieldMatch.ALL); + assertEquals(1, docs.size(), "There should have been a document returned"); + assertEquals("four", docs.get(0).getId(), "The incorrect document was returned"); + } + + public static void byFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "purple")), + JsonDocument.class, null, List.of(Field.named("id"))); + assertEquals(2, docs.size(), "There should have been 2 documents returned"); + assertEquals("five|four", docIds(docs), "The documents were not ordered correctly"); + } + + public static void byFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findByFields(db.getConn(), TEST_TABLE, + List.of(Field.any("numValue", List.of(2, 4, 6, 8))), JsonDocument.class); + assertEquals(1, docs.size(), "There should have been a document returned"); + assertEquals("three", docs.get(0).getId(), "The incorrect document was returned"); + } + + public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(0, + findByFields(db.getConn(), TEST_TABLE, List.of(Field.greater("numValue", 100)), JsonDocument.class) + .size(), + "There should have been no documents returned"); + } + + public static void byFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException { + for (final ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + final List docs = findByFields(db.getConn(), TEST_TABLE, + List.of(Field.inArray("values", TEST_TABLE, List.of("c"))), ArrayDocument.class); + assertEquals(2, docs.size(), "There should have been two documents returned"); + assertTrue(List.of("first", "second").contains(docs.get(0).getId()), + String.format("An incorrect document was returned (%s)", docs.get(0).getId())); + assertTrue(List.of("first", "second").contains(docs.get(1).getId()), + String.format("An incorrect document was returned (%s)", docs.get(1).getId())); + } + + public static void byFieldsNoMatchInArray(ThrowawayDatabase db) throws DocumentException { + for (final ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + assertEquals(0, + findByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE, List.of("j"))), + ArrayDocument.class).size(), + "There should have been no documents returned"); + } + + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"), + JsonDocument.class); + assertEquals(2, docs.size(), "There should have been 2 documents returned"); + assertTrue(List.of("four", "five").contains(docs.get(0).getId()), + String.format("An incorrect document was returned (%s)", docs.get(0).getId())); + assertTrue(List.of("four", "five").contains(docs.get(1).getId()), + String.format("An incorrect document was returned (%s)", docs.get(1).getId())); + } + + public static void byContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findByContains(db.getConn(), TEST_TABLE, Map.of("sub", Map.of("foo", "green")), + JsonDocument.class, List.of(Field.named("value"))); + assertEquals(2, docs.size(), "There should have been 2 documents returned"); + assertEquals("two|four", docIds(docs), "The documents were not ordered correctly"); + } + + public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(0, findByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo"), JsonDocument.class).size(), + "There should have been no documents returned"); + } + + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + JsonDocument.class); + assertEquals(2, docs.size(), "There should have been 2 documents returned"); + assertTrue(List.of("four", "five").contains(docs.get(0).getId()), + String.format("An incorrect document was returned (%s)", docs.get(0).getId())); + assertTrue(List.of("four", "five").contains(docs.get(1).getId()), + String.format("An incorrect document was returned (%s)", docs.get(1).getId())); } + + public static void byJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List docs = findByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + JsonDocument.class, List.of(Field.named("id"))); + assertEquals(2, docs.size(), "There should have been 2 documents returned"); + assertEquals("five|four", docIds(docs), "The documents were not ordered correctly"); + } + + public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals(0, findByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument.class).size(), + "There should have been no documents returned"); + } + + public static void firstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByFields(db.getConn(), TEST_TABLE, + List.of(Field.equal("value", "another")), JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("two", doc.get().getId(), "The incorrect document was returned"); + } + + public static void firstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByFields(db.getConn(), TEST_TABLE, + List.of(Field.equal("sub.foo", "green")), JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertTrue(List.of("two", "four").contains(doc.get().getId()), + String.format("An incorrect document was returned (%s)", doc.get().getId())); + } + + public static void firstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByFields(db.getConn(), TEST_TABLE, + List.of(Field.equal("sub.foo", "green")), JsonDocument.class, null, + List.of(Field.named("n:numValue DESC"))); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("four", doc.get().getId(), "An incorrect document was returned"); + } + + public static void firstByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(findFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "absent")), + JsonDocument.class).isPresent(), + "There should have been no document returned"); + } + + public static void firstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "FIRST!"), + JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("one", doc.get().getId(), "An incorrect document was returned"); + } + + public static void firstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"), + JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertTrue(List.of("four", "five").contains(doc.get().getId()), + String.format("An incorrect document was returned (%s)", doc.get().getId())); + } + + public static void firstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"), + JsonDocument.class, List.of(Field.named("sub.bar NULLS FIRST"))); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("five", doc.get().getId(), "An incorrect document was returned"); + } + + public static void firstByContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(findFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo"), JsonDocument.class) + .isPresent(), + "There should have been no document returned"); + } + + public static void firstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10)", + JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("two", doc.get().getId(), "An incorrect document was returned"); + } + + public static void firstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertTrue(List.of("four", "five").contains(doc.get().getId()), + String.format("An incorrect document was returned (%s)", doc.get().getId())); + } + + public static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Optional doc = findFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + JsonDocument.class, List.of(Field.named("id DESC"))); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("four", doc.get().getId(), "An incorrect document was returned"); + } + + public static void firstByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(findFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument.class) + .isPresent(), + "There should have been no document returned"); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonDocument.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonDocument.java new file mode 100644 index 0000000..0745d92 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonDocument.java @@ -0,0 +1,107 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.java.Document; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.fail; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; + +public class JsonDocument { + + private String id; + private String value; + private int numValue; + private SubDocument sub; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getNumValue() { + return numValue; + } + + public void setNumValue(int numValue) { + this.numValue = numValue; + } + + public SubDocument getSub() { + return sub; + } + + public void setSub(SubDocument sub) { + this.sub = sub; + } + + public JsonDocument(String id, String value, int numValue, SubDocument sub) { + this.id = id; + this.value = value; + this.numValue = numValue; + this.sub = sub; + } + + public JsonDocument(String id, String value, int numValue) { + this(id, value, numValue, null); + } + + public JsonDocument(String id) { + this(id, "", 0, null); + } + + public JsonDocument() { + this(null); + } + + private static final List testDocuments = List.of( + new JsonDocument("one", "FIRST!", 0), + new JsonDocument("two", "another", 10, new SubDocument("green", "blue")), + new JsonDocument("three", "", 4), + new JsonDocument("four", "purple", 17, new SubDocument("green", "red")), + new JsonDocument("five", "purple", 18)); + + public static void load(ThrowawayDatabase db, String tableName) { + try { + for (JsonDocument doc : testDocuments) { + Document.insert(tableName, doc, db.getConn()); + } + } catch (DocumentException ex) { + fail("Could not load test documents", ex); + } + } + + public static void load(ThrowawayDatabase db) { + load(db, TEST_TABLE); + } + + /** Document ID one as a JSON string */ + public static String one = "{\"id\":\"one\",\"value\":\"FIRST!\",\"numValue\":0,\"sub\":null}"; + + /** Document ID two as a JSON string */ + public static String two = "{\"id\":\"two\",\"value\":\"another\",\"numValue\":10," + + "\"sub\":{\"foo\":\"green\",\"bar\":\"blue\"}}"; + + /** Document ID three as a JSON string */ + public static String three = "{\"id\":\"three\",\"value\":\"\",\"numValue\":4,\"sub\":null}"; + + /** Document ID four as a JSON string */ + public static String four = "{\"id\":\"four\",\"value\":\"purple\",\"numValue\":17," + + "\"sub\":{\"foo\":\"green\",\"bar\":\"red\"}}"; + + /** Document ID five as a JSON string */ + public static String five = "{\"id\":\"five\",\"value\":\"purple\",\"numValue\":18,\"sub\":null}"; +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonFunctions.java new file mode 100644 index 0000000..e24d46b --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonFunctions.java @@ -0,0 +1,761 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.*; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +/** + * Tests for the JSON-returning functions + *

+ * NOTE: PostgreSQL JSONB columns do not preserve the original JSON with which a document was stored. These tests are + * the most complex within the library, as they have split testing based on the backing data store. The PostgreSQL tests + * check IDs (and, in the case of ordered queries, which ones occur before which others) vs. the entire JSON string. + * Meanwhile, SQLite stores JSON as text, and will return exactly the JSON it was given when it was originally written. + * These tests can ensure the expected round-trip of the entire JSON string. + */ +final public class JsonFunctions { + + /** + * PostgreSQL, when returning JSONB as a string, has spaces after commas and colons delineating fields and values. + * This function will do a crude string replacement to match the target string based on the dialect being tested. + * + * @param json The JSON which should be returned + * @return The actual expected JSON based on the database being tested + */ + public static String maybeJsonB(String json) throws DocumentException { + return switch (Configuration.dialect()) { + case SQLITE -> json; + case POSTGRESQL -> json.replace("\":", "\": ").replace(",\"", ", \""); + }; + } + + /** + * Create a snippet of JSON to find a document ID + * + * @param id The ID of the document + * @return A connection-aware ID to check for presence and positioning + */ + private static String docId(String id) throws DocumentException { + return maybeJsonB(String.format("{\"id\":\"%s\"", id)); + } + + private static void checkAllDefault(String json) throws DocumentException { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)"); + switch (Configuration.dialect()) { + case SQLITE: + assertTrue(json.contains(JsonDocument.one), + String.format("Document 'one' not found in JSON (%s)", json)); + assertTrue(json.contains(JsonDocument.two), + String.format("Document 'two' not found in JSON (%s)", json)); + assertTrue(json.contains(JsonDocument.three), + String.format("Document 'three' not found in JSON (%s)", json)); + assertTrue(json.contains(JsonDocument.four), + String.format("Document 'four' not found in JSON (%s)", json)); + assertTrue(json.contains(JsonDocument.five), + String.format("Document 'five' not found in JSON (%s)", json)); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("one")), String.format("Document 'one' not found in JSON (%s)", json)); + assertTrue(json.contains(docId("two")), String.format("Document 'two' not found in JSON (%s)", json)); + assertTrue(json.contains(docId("three")), + String.format("Document 'three' not found in JSON (%s)", json)); + assertTrue(json.contains(docId("four")), String.format("Document 'four' not found in JSON (%s)", json)); + assertTrue(json.contains(docId("five")), String.format("Document 'five' not found in JSON (%s)", json)); + break; + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)"); + } + + public static void allDefault(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkAllDefault(jsonAll(db.getConn(), TEST_TABLE)); + } + + public static void writeAllDefault(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonAll(db.getConn(), TEST_TABLE, writer); + checkAllDefault(output.toString()); + } + + private static void checkAllEmpty(String json) { + assertEquals("[]", json, "There should have been no documents returned"); + } + + public static void allEmpty(ThrowawayDatabase db) throws DocumentException { + checkAllEmpty(jsonAll(db.getConn(), TEST_TABLE)); + } + + public static void writeAllEmpty(ThrowawayDatabase db) throws DocumentException { + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonAll(db.getConn(), TEST_TABLE, writer); + checkAllEmpty(output.toString()); + } + + private static void checkByIdString(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(JsonDocument.two, json, "An incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("two")), String.format("An incorrect document was returned (%s)", json)); + break; + } + } + + public static void byIdString(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByIdString(jsonById(db.getConn(), TEST_TABLE, "two")); + } + + public static void writeByIdString(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonById(db.getConn(), TEST_TABLE, writer, "two"); + checkByIdString(output.toString()); + } + + private static void checkByIdNumber(String json) throws DocumentException { + assertEquals(maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), json, + "The document should have been found by numeric ID"); + } + + public static void byIdNumber(ThrowawayDatabase db) throws DocumentException { + Configuration.idField = "key"; + try { + insert(db.getConn(), TEST_TABLE, new NumIdDocument(18, "howdy")); + checkByIdNumber(jsonById(db.getConn(), TEST_TABLE, 18)); + } finally { + Configuration.idField = "id"; + } + } + + public static void writeByIdNumber(ThrowawayDatabase db) throws DocumentException { + Configuration.idField = "key"; + try { + insert(db.getConn(), TEST_TABLE, new NumIdDocument(18, "howdy")); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonById(db.getConn(), TEST_TABLE, writer, 18); + checkByIdNumber(output.toString()); + } finally { + Configuration.idField = "id"; + } + } + + private static void checkByIdNotFound(String json) { + assertEquals("{}", json, "There should have been no document returned"); + } + + public static void byIdNotFound(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByIdNotFound(jsonById(db.getConn(), TEST_TABLE, "x")); + } + + public static void writeByIdNotFound(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonById(db.getConn(), TEST_TABLE, writer, "x"); + checkByIdNotFound(output.toString()); + } + + private static void checkByFieldsMatch(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(String.format("[%s]", JsonDocument.four), json, "The incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json)); + assertTrue(json.contains(docId("four")), + String.format("The incorrect document was returned (%s)", json)); + assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); + break; + } + } + + public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByFieldsMatch(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.any("value", List.of("blue", "purple")), + Field.exists("sub")), FieldMatch.ALL)); + } + + public static void writeByFieldsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.any("value", List.of("blue", "purple")), + Field.exists("sub")), FieldMatch.ALL); + checkByFieldsMatch(output.toString()); + } + + private static void checkByFieldsMatchOrdered(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(String.format("[%s,%s]", JsonDocument.five, JsonDocument.four), json, + "The documents were not ordered correctly"); + break; + case POSTGRESQL: + final int fiveIdx = json.indexOf(docId("five")); + final int fourIdx = json.indexOf(docId("four")); + assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json)); + assertTrue(fiveIdx >= 0, String.format("Document 'five' not found (%s)", json)); + assertTrue(fourIdx >= 0, String.format("Document 'four' not found (%s)", json)); + assertTrue(fiveIdx < fourIdx, + String.format("Document 'five' should have been before 'four' (%s)", json)); + assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); + break; + } + } + + public static void byFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByFieldsMatchOrdered(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "purple")), null, + List.of(Field.named("id")))); + } + + public static void writeByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("value", "purple")), null, + List.of(Field.named("id"))); + checkByFieldsMatchOrdered(output.toString()); + } + + private static void checkByFieldsMatchNumIn(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(String.format("[%s]", JsonDocument.three), json, "The incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json)); + assertTrue(json.contains(docId("three")), + String.format("The incorrect document was returned (%s)", json)); + assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); + break; + } + } + + public static void byFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByFieldsMatchNumIn(jsonByFields(db.getConn(), TEST_TABLE, + List.of(Field.any("numValue", List.of(2, 4, 6, 8))))); + } + + public static void writeByFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.any("numValue", List.of(2, 4, 6, 8)))); + checkByFieldsMatchNumIn(output.toString()); + } + + private static void checkByFieldsNoMatch(String json) { + assertEquals("[]", json, "There should have been no documents returned"); + } + + public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByFieldsNoMatch(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.greater("numValue", 100)))); + } + + public static void writeByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.greater("numValue", 100))); + checkByFieldsNoMatch(output.toString()); + } + + private static void checkByFieldsMatchInArray(String json) throws DocumentException { + assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json)); + assertTrue(json.contains(docId("first")), String.format("The 'first' document was not found (%s)", json)); + assertTrue(json.contains(docId("second")), String.format("The 'second' document was not found (%s)", json)); + assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); + } + + public static void byFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + checkByFieldsMatchInArray(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE, + List.of("c"))))); + } + + public static void writeByFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.inArray("values", TEST_TABLE, List.of("c")))); + checkByFieldsMatchInArray(output.toString()); + } + + private static void checkByFieldsNoMatchInArray(String json) { + assertEquals("[]", json, "There should have been no documents returned"); + } + + public static void byFieldsNoMatchInArray(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + checkByFieldsNoMatchInArray(jsonByFields(db.getConn(), TEST_TABLE, + List.of(Field.inArray("values", TEST_TABLE, List.of("j"))))); + } + + public static void writeByFieldsNoMatchInArray(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.inArray("values", TEST_TABLE, List.of("j")))); + checkByFieldsNoMatchInArray(output.toString()); + } + + private static void checkByContainsMatch(String json) throws DocumentException { + assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json)); + switch (Configuration.dialect()) { + case SQLITE: + assertTrue(json.contains(JsonDocument.four), String.format("Document 'four' not found (%s)", json)); + assertTrue(json.contains(JsonDocument.five), String.format("Document 'five' not found (%s)", json)); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("four")), String.format("Document 'four' not found (%s)", json)); + assertTrue(json.contains(docId("five")), String.format("Document 'five' not found (%s)", json)); + break; + } + assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); + } + + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByContainsMatch(jsonByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"))); + } + + public static void writeByContainsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "purple")); + checkByContainsMatch(output.toString()); + } + + private static void checkByContainsMatchOrdered(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(String.format("[%s,%s]", JsonDocument.two, JsonDocument.four), json, + "The documents were not ordered correctly"); + break; + case POSTGRESQL: + final int twoIdx = json.indexOf(docId("two")); + final int fourIdx = json.indexOf(docId("four")); + assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json)); + assertTrue(twoIdx >= 0, String.format("Document 'two' not found (%s)", json)); + assertTrue(fourIdx >= 0, String.format("Document 'four' not found (%s)", json)); + assertTrue(twoIdx < fourIdx, String.format("Document 'two' should have been before 'four' (%s)", json)); + assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); + break; + } + } + + public static void byContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByContainsMatchOrdered(jsonByContains(db.getConn(), TEST_TABLE, Map.of("sub", Map.of("foo", "green")), + List.of(Field.named("value")))); + } + + public static void writeByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByContains(db.getConn(), TEST_TABLE, writer, Map.of("sub", Map.of("foo", "green")), + List.of(Field.named("value"))); + checkByContainsMatchOrdered(output.toString()); + } + + private static void checkByContainsNoMatch(String json) { + assertEquals("[]", json, "There should have been no documents returned"); + } + + public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByContainsNoMatch(jsonByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo"))); + } + + public static void writeByContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "indigo")); + checkByContainsNoMatch(output.toString()); + } + + private static void checkByJsonPathMatch(String json) throws DocumentException { + assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json)); + switch (Configuration.dialect()) { + case SQLITE: + assertTrue(json.contains(JsonDocument.four), String.format("Document 'four' not found (%s)", json)); + assertTrue(json.contains(JsonDocument.five), String.format("Document 'five' not found (%s)", json)); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("four")), String.format("Document 'four' not found (%s)", json)); + assertTrue(json.contains(docId("five")), String.format("Document 'five' not found (%s)", json)); + break; + } + assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); + } + + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByJsonPathMatch(jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)")); + } + + public static void writeByJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 10)"); + checkByJsonPathMatch(output.toString()); + } + + private static void checkByJsonPathMatchOrdered(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(String.format("[%s,%s]", JsonDocument.five, JsonDocument.four), json, + "The documents were not ordered correctly"); + break; + case POSTGRESQL: + final int fiveIdx = json.indexOf(docId("five")); + final int fourIdx = json.indexOf(docId("four")); + assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json)); + assertTrue(fiveIdx >= 0, String.format("Document 'five' not found (%s)", json)); + assertTrue(fourIdx >= 0, String.format("Document 'four' not found (%s)", json)); + assertTrue(fiveIdx < fourIdx, + String.format("Document 'five' should have been before 'four' (%s)", json)); + assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); + break; + } + } + + public static void byJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByJsonPathMatchOrdered(jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + List.of(Field.named("id")))); + } + + public static void writeByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 10)", List.of(Field.named("id"))); + checkByJsonPathMatchOrdered(output.toString()); + } + + private static void checkByJsonPathNoMatch(String json) { + assertEquals("[]", json, "There should have been no documents returned"); + } + + public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkByJsonPathNoMatch(jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)")); + } + + public static void writeByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 100)"); + checkByJsonPathNoMatch(output.toString()); + } + + private static void checkFirstByFieldsMatchOne(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(JsonDocument.two, json, "The incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("two")), + String.format("The incorrect document was returned (%s)", json)); + break; + } + } + + public static void firstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByFieldsMatchOne(jsonFirstByFields(db.getConn(), TEST_TABLE, + List.of(Field.equal("value", "another")))); + } + + public static void writeFirstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("value", "another"))); + checkFirstByFieldsMatchOne(output.toString()); + } + + private static void checkFirstByFieldsMatchMany(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertTrue(json.contains(JsonDocument.two) || json.contains(JsonDocument.four), + String.format("Expected document 'two' or 'four' (%s)", json)); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("two")) || json.contains(docId("four")), + String.format("Expected document 'two' or 'four' (%s)", json)); + break; + } + } + + public static void firstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByFieldsMatchMany(jsonFirstByFields(db.getConn(), TEST_TABLE, + List.of(Field.equal("sub.foo", "green")))); + } + + public static void writeFirstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("sub.foo", "green"))); + checkFirstByFieldsMatchMany(output.toString()); + } + + private static void checkFirstByFieldsMatchOrdered(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(JsonDocument.four, json, "An incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("four")), + String.format("An incorrect document was returned (%s)", json)); + break; + } + } + + public static void firstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByFieldsMatchOrdered(jsonFirstByFields(db.getConn(), TEST_TABLE, + List.of(Field.equal("sub.foo", "green")), null, List.of(Field.named("n:numValue DESC")))); + } + + public static void writeFirstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("sub.foo", "green")), null, + List.of(Field.named("n:numValue DESC"))); + checkFirstByFieldsMatchOrdered(output.toString()); + } + + private static void checkFirstByFieldsNoMatch(String json) { + assertEquals("{}", json, "There should have been no document returned"); + } + + public static void firstByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByFieldsNoMatch(jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "absent")))); + } + + public static void writeFirstByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("value", "absent"))); + checkFirstByFieldsNoMatch(output.toString()); + } + + private static void checkFirstByContainsMatchOne(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(JsonDocument.one, json, "An incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("one")), String.format("An incorrect document was returned (%s)", json)); + break; + } + } + + public static void firstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByContainsMatchOne(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "FIRST!"))); + } + + public static void writeFirstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "FIRST!")); + checkFirstByContainsMatchOne(output.toString()); + } + + private static void checkFirstByContainsMatchMany(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertTrue(json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + String.format("Expected document 'four' or 'five' (%s)", json)); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("four")) || json.contains(docId("five")), + String.format("Expected document 'four' or 'five' (%s)", json)); + break; + } + } + + public static void firstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByContainsMatchMany(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"))); + } + + public static void writeFirstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "purple")); + checkFirstByContainsMatchMany(output.toString()); + } + + private static void checkFirstByContainsMatchOrdered(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(JsonDocument.five, json, "An incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("five")), + String.format("An incorrect document was returned (%s)", json)); + break; + } + } + + public static void firstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByContainsMatchOrdered(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"), + List.of(Field.named("sub.bar NULLS FIRST")))); + } + + public static void writeFirstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "purple"), + List.of(Field.named("sub.bar NULLS FIRST"))); + checkFirstByContainsMatchOrdered(output.toString()); + } + + private static void checkFirstByContainsNoMatch(String json) { + assertEquals("{}", json, "There should have been no document returned"); + } + + public static void firstByContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByContainsNoMatch(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo"))); + } + + public static void writeFirstByContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "indigo")); + checkFirstByContainsNoMatch(output.toString()); + } + + private static void checkFirstByJsonPathMatchOne(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(JsonDocument.two, json, "An incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("two")), String.format("An incorrect document was returned (%s)", json)); + break; + } + } + + public static void firstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByJsonPathMatchOne(jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10)")); + } + + public static void writeFirstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ == 10)"); + checkFirstByJsonPathMatchOne(output.toString()); + } + + private static void checkFirstByJsonPathMatchMany(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertTrue(json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + String.format("Expected document 'four' or 'five' (%s)", json)); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("four")) || json.contains(docId("five")), + String.format("Expected document 'four' or 'five' (%s)", json)); + break; + } + } + + public static void firstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByJsonPathMatchMany(jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)")); + } + + public static void writeFirstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 10)"); + checkFirstByJsonPathMatchMany(output.toString()); + } + + private static void checkFirstByJsonPathMatchOrdered(String json) throws DocumentException { + switch (Configuration.dialect()) { + case SQLITE: + assertEquals(JsonDocument.four, json, "An incorrect document was returned"); + break; + case POSTGRESQL: + assertTrue(json.contains(docId("four")), + String.format("An incorrect document was returned (%s)", json)); + break; + } + } + + public static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByJsonPathMatchOrdered(jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + List.of(Field.named("id DESC")))); + } + + public static void writeFirstByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 10)", + List.of(Field.named("id DESC"))); + checkFirstByJsonPathMatchOrdered(output.toString()); + } + + private static void checkFirstByJsonPathNoMatch(String json) { + assertEquals("{}", json, "There should have been no document returned"); + } + + public static void firstByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkFirstByJsonPathNoMatch(jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)")); + } + + public static void writeFirstByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final StringWriter output = new StringWriter(); + final PrintWriter writer = new PrintWriter(output); + writeJsonFirstByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 100)"); + checkFirstByJsonPathNoMatch(output.toString()); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/NumIdDocument.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/NumIdDocument.java new file mode 100644 index 0000000..3d4ea66 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/NumIdDocument.java @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +public class NumIdDocument { + + private int key; + private String text; + + public int getKey() { + return key; + } + + public void setKey(int key) { + this.key = key; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public NumIdDocument(int key, String text) { + this.key = key; + this.text = text; + } + + public NumIdDocument() { + this(0, ""); + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PatchFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PatchFunctions.java new file mode 100644 index 0000000..324d2c6 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PatchFunctions.java @@ -0,0 +1,85 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +final public class PatchFunctions { + + public static void byIdMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + patchById(db.getConn(), TEST_TABLE, "one", Map.of("numValue", 44)); + final Optional doc = findById(db.getConn(), TEST_TABLE, "one", JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("one", doc.get().getId(), "An incorrect document was returned"); + assertEquals(44, doc.get().getNumValue(), "The document was not patched"); + } + + public static void byIdNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsById(db.getConn(), TEST_TABLE, "forty-seven"), + "Document with ID \"forty-seven\" should not exist"); + patchById(db.getConn(), TEST_TABLE, "forty-seven", Map.of("foo", "green")); // no exception = pass + } + + public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + patchByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "purple")), Map.of("numValue", 77)); + assertEquals(2L, countByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("numValue", 77))), + "There should have been 2 documents with numeric value 77"); + } + + public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List> fields = List.of(Field.equal("value", "burgundy")); + assertFalse(existsByFields(db.getConn(), TEST_TABLE, fields), + "There should be no documents with value of \"burgundy\""); + patchByFields(db.getConn(), TEST_TABLE, fields, Map.of("foo", "green")); // no exception = pass + } + + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Map contains = Map.of("value", "another"); + patchByContains(db.getConn(), TEST_TABLE, contains, Map.of("numValue", 12)); + final Optional doc = findFirstByContains(db.getConn(), TEST_TABLE, contains, JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("two", doc.get().getId(), "The incorrect document was returned"); + assertEquals(12, doc.get().getNumValue(), "The document was not updated"); + } + + public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Map contains = Map.of("value", "updated"); + assertFalse(existsByContains(db.getConn(), TEST_TABLE, contains), "There should be no matching documents"); + patchByContains(db.getConn(), TEST_TABLE, contains, Map.of("sub.foo", "green")); // no exception = pass + } + + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String path = "$.numValue ? (@ > 10)"; + patchByJsonPath(db.getConn(), TEST_TABLE, path, Map.of("value", "blue")); + final List docs = findByJsonPath(db.getConn(), TEST_TABLE, path, JsonDocument.class); + assertEquals(2, docs.size(), "There should have been two documents returned"); + for (final JsonDocument doc : docs) { + assertTrue(List.of("four", "five").contains(doc.getId()), + String.format("An incorrect document was returned (%s)", doc.getId())); + assertEquals("blue", doc.getValue(), String.format("The value for ID %s was incorrect", doc.getId())); + } + } + + public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String path = "$.numValue ? (@ > 100)"; + assertFalse(existsByJsonPath(db.getConn(), TEST_TABLE, path), + "There should be no documents with numeric values over 100"); + patchByJsonPath(db.getConn(), TEST_TABLE, path, Map.of("value", "blue")); // no exception = pass + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCountIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCountIT.java new file mode 100644 index 0000000..2d4c829 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCountIT.java @@ -0,0 +1,69 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Count") +public class PostgreSQLCountIT { + + @Test + @DisplayName("all counts all documents") + public void all() throws DocumentException { + try (PgDB db = new PgDB()) { + CountFunctions.all(db); + } + } + + @Test + @DisplayName("byFields counts documents by a numeric value") + public void byFieldsNumeric() throws DocumentException { + try (PgDB db = new PgDB()) { + CountFunctions.byFieldsNumeric(db); + } + } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + public void byFieldsAlpha() throws DocumentException { + try (PgDB db = new PgDB()) { + CountFunctions.byFieldsAlpha(db); + } + } + + @Test + @DisplayName("byContains counts documents when matches are found") + public void byContainsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + CountFunctions.byContainsMatch(db); + } + } + + @Test + @DisplayName("byContains counts documents when no matches are found") + public void byContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + CountFunctions.byContainsNoMatch(db); + } + } + + @Test + @DisplayName("byJsonPath counts documents when matches are found") + public void byJsonPathMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + CountFunctions.byJsonPathMatch(db); + } + } + + @Test + @DisplayName("byJsonPath counts documents when no matches are found") + public void byJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + CountFunctions.byJsonPathNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java new file mode 100644 index 0000000..3f12577 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java @@ -0,0 +1,133 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Custom") +final public class PostgreSQLCustomIT { + + @Test + @DisplayName("list succeeds with empty list") + public void listEmpty() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.listEmpty(db); + } + } + + @Test + @DisplayName("list succeeds with a non-empty list") + public void listAll() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.listAll(db); + } + } + + @Test + @DisplayName("jsonArray succeeds with empty array") + public void jsonArrayEmpty() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.jsonArrayEmpty(db); + } + } + + @Test + @DisplayName("jsonArray succeeds with a single-item array") + public void jsonArraySingle() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.jsonArraySingle(db); + } + } + + @Test + @DisplayName("jsonArray succeeds with a multi-item array") + public void jsonArrayMany() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.jsonArrayMany(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with empty array") + public void writeJsonArrayEmpty() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.writeJsonArrayEmpty(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + public void writeJsonArraySingle() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.writeJsonArraySingle(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + public void writeJsonArrayMany() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.writeJsonArrayMany(db); + } + } + + @Test + @DisplayName("single succeeds when document not found") + public void singleNone() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.singleNone(db); + } + } + + @Test + @DisplayName("single succeeds when a document is found") + public void singleOne() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.singleOne(db); + } + } + + @Test + @DisplayName("jsonSingle succeeds when document not found") + public void jsonSingleNone() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.jsonSingleNone(db); + } + } + + @Test + @DisplayName("jsonSingle succeeds when a document is found") + public void jsonSingleOne() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.jsonSingleOne(db); + } + } + + @Test + @DisplayName("nonQuery makes changes") + public void nonQueryChanges() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.nonQueryChanges(db); + } + } + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + public void nonQueryNoChanges() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.nonQueryNoChanges(db); + } + } + + @Test + @DisplayName("scalar succeeds") + public void scalar() throws DocumentException { + try (PgDB db = new PgDB()) { + CustomFunctions.scalar(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDefinitionIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDefinitionIT.java new file mode 100644 index 0000000..6ba239d --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDefinitionIT.java @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Definition") +final public class PostgreSQLDefinitionIT { + + @Test + @DisplayName("ensureTable creates table and index") + public void ensureTable() throws DocumentException { + try (PgDB db = new PgDB()) { + DefinitionFunctions.ensuresATable(db); + } + } + + @Test + @DisplayName("ensureFieldIndex creates an index") + public void ensureFieldIndex() throws DocumentException { + try (PgDB db = new PgDB()) { + DefinitionFunctions.ensuresAFieldIndex(db); + } + } + + @Test + @DisplayName("ensureDocumentIndex creates a full index") + public void ensureDocumentIndexFull() throws DocumentException { + try (PgDB db = new PgDB()) { + DefinitionFunctions.ensureDocumentIndexFull(db); + } + } + + @Test + @DisplayName("ensureDocumentIndex creates an optimized index") + public void ensureDocumentIndexOptimized() throws DocumentException { + try (PgDB db = new PgDB()) { + DefinitionFunctions.ensureDocumentIndexOptimized(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDeleteIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDeleteIT.java new file mode 100644 index 0000000..1ed918d --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDeleteIT.java @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Delete") +final public class PostgreSQLDeleteIT { + + @Test + @DisplayName("byId deletes a matching ID") + public void byIdMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DeleteFunctions.byIdMatch(db); + } + } + + @Test + @DisplayName("byId succeeds when no ID matches") + public void byIdNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DeleteFunctions.byIdNoMatch(db); + } + } + + @Test + @DisplayName("byFields deletes matching documents") + public void byFieldsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DeleteFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DeleteFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byContains deletes matching documents") + public void byContainsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DeleteFunctions.byContainsMatch(db); + } + } + + @Test + @DisplayName("byContains succeeds when no documents match") + public void byContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DeleteFunctions.byContainsNoMatch(db); + } + } + + @Test + @DisplayName("byJsonPath deletes matching documents") + public void byJsonPathMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DeleteFunctions.byJsonPathMatch(db); + } + } + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + public void byJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DeleteFunctions.byJsonPathNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDocumentIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDocumentIT.java new file mode 100644 index 0000000..a3fd24c --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLDocumentIT.java @@ -0,0 +1,85 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Document") +final public class PostgreSQLDocumentIT { + + @Test + @DisplayName("insert works with default values") + public void insertDefault() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.insertDefault(db); + } + } + + @Test + @DisplayName("insert fails with duplicate key") + public void insertDupe() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.insertDupe(db); + } + } + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + public void insertNumAutoId() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.insertNumAutoId(db); + } + } + + @Test + @DisplayName("insert succeeds with UUID auto ID") + public void insertUUIDAutoId() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.insertUUIDAutoId(db); + } + } + + @Test + @DisplayName("insert succeeds with random string auto ID") + public void insertStringAutoId() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.insertStringAutoId(db); + } + } + + @Test + @DisplayName("save updates an existing document") + public void saveMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.saveMatch(db); + } + } + + @Test + @DisplayName("save inserts a new document") + public void saveNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.saveNoMatch(db); + } + } + + @Test + @DisplayName("update replaces an existing document") + public void updateMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.updateMatch(db); + } + } + + @Test + @DisplayName("update succeeds when no document exists") + public void updateNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + DocumentFunctions.updateNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLExistsIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLExistsIT.java new file mode 100644 index 0000000..e8b6437 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLExistsIT.java @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Exists") +final public class PostgreSQLExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + public void byIdMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + ExistsFunctions.byIdMatch(db); + } + } + + @Test + @DisplayName("byId returns false when no document matches the ID") + public void byIdNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + ExistsFunctions.byIdNoMatch(db); + } + } + + @Test + @DisplayName("byFields returns true when documents match") + public void byFieldsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + ExistsFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields returns false when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + ExistsFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byContains returns true when documents match") + public void byContainsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + ExistsFunctions.byContainsMatch(db); + } + } + + @Test + @DisplayName("byContains returns false when no documents match") + public void byContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + ExistsFunctions.byContainsNoMatch(db); + } + } + + @Test + @DisplayName("byJsonPath returns true when documents match") + public void byJsonPathMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + ExistsFunctions.byJsonPathMatch(db); + } + } + + @Test + @DisplayName("byJsonPath returns false when no documents match") + public void byJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + ExistsFunctions.byJsonPathNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLFindIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLFindIT.java new file mode 100644 index 0000000..920d34a --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLFindIT.java @@ -0,0 +1,269 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Find") +final public class PostgreSQLFindIT { + + @Test + @DisplayName("all retrieves all documents") + public void allDefault() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.allDefault(db); + } + } + + @Test + @DisplayName("all sorts data ascending") + public void allAscending() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.allAscending(db); + } + } + + @Test + @DisplayName("all sorts data descending") + public void allDescending() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.allDescending(db); + } + } + + @Test + @DisplayName("all sorts data numerically") + public void allNumOrder() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.allNumOrder(db); + } + } + + @Test + @DisplayName("all succeeds with an empty table") + public void allEmpty() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.allEmpty(db); + } + } + + @Test + @DisplayName("byId retrieves a document via a string ID") + public void byIdString() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byIdString(db); + } + } + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + public void byIdNumber() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byIdNumber(db); + } + } + + @Test + @DisplayName("byId returns null when a matching ID is not found") + public void byIdNotFound() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byIdNotFound(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents") + public void byFieldsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields retrieves ordered matching documents") + public void byFieldsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + public void byFieldsMatchNumIn() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byFieldsMatchNumIn(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + public void byFieldsMatchInArray() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byFieldsMatchInArray(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + public void byFieldsNoMatchInArray() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byFieldsNoMatchInArray(db); + } + } + + @Test + @DisplayName("byContains retrieves matching documents") + public void byContainsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byContainsMatch(db); + } + } + + @Test + @DisplayName("byContains retrieves ordered matching documents") + public void byContainsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byContainsMatchOrdered(db); + } + } + + @Test + @DisplayName("byContains succeeds when no documents match") + public void byContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byContainsNoMatch(db); + } + } + + @Test + @DisplayName("byJsonPath retrieves matching documents") + public void byJsonPathMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byJsonPathMatch(db); + } + } + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + public void byJsonPathMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byJsonPathMatchOrdered(db); + } + } + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + public void byJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.byJsonPathNoMatch(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + public void firstByFieldsMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByFieldsMatchOne(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + public void firstByFieldsMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByFieldsMatchMany(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + public void firstByFieldsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("firstByFields returns null when no document matches") + public void firstByFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByFieldsNoMatch(db); + } + } + + @Test + @DisplayName("firstByContains retrieves a matching document") + public void firstByContainsMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByContainsMatchOne(db); + } + } + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + public void firstByContainsMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByContainsMatchMany(db); + } + } + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + public void firstByContainsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByContainsMatchOrdered(db); + } + } + + @Test + @DisplayName("firstByContains returns null when no document matches") + public void firstByContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByContainsNoMatch(db); + } + } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + public void firstByJsonPathMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByJsonPathMatchOne(db); + } + } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + public void firstByJsonPathMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByJsonPathMatchMany(db); + } + } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + public void firstByJsonPathMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByJsonPathMatchOrdered(db); + } + } + + @Test + @DisplayName("firstByJsonPath returns null when no document matches") + public void firstByJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + FindFunctions.firstByJsonPathNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLJsonIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLJsonIT.java new file mode 100644 index 0000000..144df98 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLJsonIT.java @@ -0,0 +1,477 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Json") +final public class PostgreSQLJsonIT { + + @Test + @DisplayName("all retrieves all documents") + public void allDefault() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.allDefault(db); + } + } + + @Test + @DisplayName("all succeeds with an empty table") + public void allEmpty() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.allEmpty(db); + } + } + + @Test + @DisplayName("byId retrieves a document via a string ID") + public void byIdString() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byIdString(db); + } + } + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + public void byIdNumber() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byIdNumber(db); + } + } + + @Test + @DisplayName("byId returns null when a matching ID is not found") + public void byIdNotFound() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byIdNotFound(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents") + public void byFieldsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields retrieves ordered matching documents") + public void byFieldsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + public void byFieldsMatchNumIn() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byFieldsMatchNumIn(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + public void byFieldsMatchInArray() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byFieldsMatchInArray(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + public void byFieldsNoMatchInArray() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byFieldsNoMatchInArray(db); + } + } + + @Test + @DisplayName("byContains retrieves matching documents") + public void byContainsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byContainsMatch(db); + } + } + + @Test + @DisplayName("byContains retrieves ordered matching documents") + public void byContainsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byContainsMatchOrdered(db); + } + } + + @Test + @DisplayName("byContains succeeds when no documents match") + public void byContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byContainsNoMatch(db); + } + } + + @Test + @DisplayName("byJsonPath retrieves matching documents") + public void byJsonPathMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byJsonPathMatch(db); + } + } + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + public void byJsonPathMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byJsonPathMatchOrdered(db); + } + } + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + public void byJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.byJsonPathNoMatch(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + public void firstByFieldsMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByFieldsMatchOne(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + public void firstByFieldsMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByFieldsMatchMany(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + public void firstByFieldsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("firstByFields returns null when no document matches") + public void firstByFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByFieldsNoMatch(db); + } + } + + @Test + @DisplayName("firstByContains retrieves a matching document") + public void firstByContainsMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByContainsMatchOne(db); + } + } + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + public void firstByContainsMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByContainsMatchMany(db); + } + } + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + public void firstByContainsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByContainsMatchOrdered(db); + } + } + + @Test + @DisplayName("firstByContains returns null when no document matches") + public void firstByContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByContainsNoMatch(db); + } + } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + public void firstByJsonPathMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByJsonPathMatchOne(db); + } + } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + public void firstByJsonPathMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByJsonPathMatchMany(db); + } + } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + public void firstByJsonPathMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByJsonPathMatchOrdered(db); + } + } + + @Test + @DisplayName("firstByJsonPath returns null when no document matches") + public void firstByJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.firstByJsonPathNoMatch(db); + } + } + + @Test + @DisplayName("writeAll retrieves all documents") + public void writeAllDefault() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeAllDefault(db); + } + } + + @Test + @DisplayName("writeAll succeeds with an empty table") + public void writeAllEmpty() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeAllEmpty(db); + } + } + + @Test + @DisplayName("writeById retrieves a document via a string ID") + public void writeByIdString() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByIdString(db); + } + } + + @Test + @DisplayName("writeById retrieves a document via a numeric ID") + public void writeByIdNumber() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByIdNumber(db); + } + } + + @Test + @DisplayName("writeById returns null when a matching ID is not found") + public void writeByIdNotFound() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByIdNotFound(db); + } + } + + @Test + @DisplayName("writeByFields retrieves matching documents") + public void writeByFieldsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByFieldsMatch(db); + } + } + + @Test + @DisplayName("writeByFields retrieves ordered matching documents") + public void writeByFieldsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("writeByFields retrieves matching documents with a numeric IN clause") + public void writeByFieldsMatchNumIn() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByFieldsMatchNumIn(db); + } + } + + @Test + @DisplayName("writeByFields succeeds when no documents match") + public void writeByFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByFieldsNoMatch(db); + } + } + + @Test + @DisplayName("writeByFields retrieves matching documents with an IN_ARRAY comparison") + public void writeByFieldsMatchInArray() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByFieldsMatchInArray(db); + } + } + + @Test + @DisplayName("writeByFields succeeds when no documents match an IN_ARRAY comparison") + public void writeByFieldsNoMatchInArray() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByFieldsNoMatchInArray(db); + } + } + + @Test + @DisplayName("writeByContains retrieves matching documents") + public void writeByContainsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByContainsMatch(db); + } + } + + @Test + @DisplayName("writeByContains retrieves ordered matching documents") + public void writeByContainsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByContainsMatchOrdered(db); + } + } + + @Test + @DisplayName("writeByContains succeeds when no documents match") + public void writeByContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByContainsNoMatch(db); + } + } + + @Test + @DisplayName("writeByJsonPath retrieves matching documents") + public void writeByJsonPathMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByJsonPathMatch(db); + } + } + + @Test + @DisplayName("writeByJsonPath retrieves ordered matching documents") + public void writeByJsonPathMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByJsonPathMatchOrdered(db); + } + } + + @Test + @DisplayName("writeByJsonPath succeeds when no documents match") + public void writeByJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeByJsonPathNoMatch(db); + } + } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document") + public void writeFirstByFieldsMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByFieldsMatchOne(db); + } + } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many") + public void writeFirstByFieldsMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByFieldsMatchMany(db); + } + } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many (ordered)") + public void writeFirstByFieldsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("writeFirstByFields returns null when no document matches") + public void writeFirstByFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByFieldsNoMatch(db); + } + } + + @Test + @DisplayName("writeFirstByContains retrieves a matching document") + public void writeFirstByContainsMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByContainsMatchOne(db); + } + } + + @Test + @DisplayName("writeFirstByContains retrieves a matching document among many") + public void writeFirstByContainsMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByContainsMatchMany(db); + } + } + + @Test + @DisplayName("writeFirstByContains retrieves a matching document among many (ordered)") + public void writeFirstByContainsMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByContainsMatchOrdered(db); + } + } + + @Test + @DisplayName("writeFirstByContains returns null when no document matches") + public void writeFirstByContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByContainsNoMatch(db); + } + } + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document") + public void writeFirstByJsonPathMatchOne() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByJsonPathMatchOne(db); + } + } + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document among many") + public void writeFirstByJsonPathMatchMany() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByJsonPathMatchMany(db); + } + } + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document among many (ordered)") + public void writeFirstByJsonPathMatchOrdered() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByJsonPathMatchOrdered(db); + } + } + + @Test + @DisplayName("writeFirstByJsonPath returns null when no document matches") + public void writeFirstByJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + JsonFunctions.writeFirstByJsonPathNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLPatchIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLPatchIT.java new file mode 100644 index 0000000..2acd8fb --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLPatchIT.java @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: Patch") +final public class PostgreSQLPatchIT { + + @Test + @DisplayName("byId patches an existing document") + public void byIdMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + PatchFunctions.byIdMatch(db); + } + } + + @Test + @DisplayName("byId succeeds for a non-existent document") + public void byIdNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + PatchFunctions.byIdNoMatch(db); + } + } + + @Test + @DisplayName("byFields patches matching document") + public void byFieldsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + PatchFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + PatchFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byContains patches matching document") + public void byContainsMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + PatchFunctions.byContainsMatch(db); + } + } + + @Test + @DisplayName("byContains succeeds when no documents match") + public void byContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + PatchFunctions.byContainsNoMatch(db); + } + } + + @Test + @DisplayName("byJsonPath patches matching document") + public void byJsonPathMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + PatchFunctions.byJsonPathMatch(db); + } + } + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + public void byJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + PatchFunctions.byJsonPathNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLRemoveFieldsIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLRemoveFieldsIT.java new file mode 100644 index 0000000..d01914e --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLRemoveFieldsIT.java @@ -0,0 +1,109 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.PgDB; + +/** + * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("Core | Java | PostgreSQL: RemoveFields") +final public class PostgreSQLRemoveFieldsIT { + + @Test + @DisplayName("byId removes fields from an existing document") + public void byIdMatchFields() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byIdMatchFields(db); + } + } + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + public void byIdMatchNoFields() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byIdMatchNoFields(db); + } + } + + @Test + @DisplayName("byId succeeds when no document exists") + public void byIdNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byIdNoMatch(db); + } + } + + @Test + @DisplayName("byFields removes fields from matching documents") + public void byFieldsMatchFields() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byFieldsMatchFields(db); + } + } + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + public void byFieldsMatchNoFields() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byFieldsMatchNoFields(db); + } + } + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + public void byFieldsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byContains removes fields from matching documents") + public void byContainsMatchFields() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byContainsMatchFields(db); + } + } + + @Test + @DisplayName("byContains succeeds when fields do not exist on matching documents") + public void byContainsMatchNoFields() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byContainsMatchNoFields(db); + } + } + + @Test + @DisplayName("byContains succeeds when no matching documents exist") + public void byContainsNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byContainsNoMatch(db); + } + } + + @Test + @DisplayName("byJsonPath removes fields from matching documents") + public void byJsonPathMatchFields() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byJsonPathMatchFields(db); + } + } + + @Test + @DisplayName("byJsonPath succeeds when fields do not exist on matching documents") + public void byJsonPathMatchNoFields() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byJsonPathMatchNoFields(db); + } + } + + @Test + @DisplayName("byJsonPath succeeds when no matching documents exist") + public void byJsonPathNoMatch() throws DocumentException { + try (PgDB db = new PgDB()) { + RemoveFieldsFunctions.byJsonPathNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/RemoveFieldsFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/RemoveFieldsFunctions.java new file mode 100644 index 0000000..8bdbd5c --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/RemoveFieldsFunctions.java @@ -0,0 +1,115 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; + +final public class RemoveFieldsFunctions { + + public static void byIdMatchFields(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + removeFieldsById(db.getConn(), TEST_TABLE, "two", List.of("sub", "value")); + final Optional doc = findById(db.getConn(), TEST_TABLE, "two", JsonDocument.class); + assertTrue(doc.isPresent(), "There should have been a document returned"); + assertEquals("", doc.get().getValue(), "The value should have been empty"); + assertNull(doc.get().getSub(), "The sub-document should have been removed"); + } + + public static void byIdMatchNoFields(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsByFields(db.getConn(), TEST_TABLE, List.of(Field.exists("a_field_that_does_not_exist")))); + removeFieldsById(db.getConn(), TEST_TABLE, "one", List.of("a_field_that_does_not_exist")); // no exn = pass + } + + public static void byIdNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsById(db.getConn(), TEST_TABLE, "fifty")); + removeFieldsById(db.getConn(), TEST_TABLE, "fifty", List.of("sub")); // no exception = pass + } + + public static void byFieldsMatchFields(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List> fields = List.of(Field.equal("numValue", 17)); + removeFieldsByFields(db.getConn(), TEST_TABLE, fields, List.of("sub")); + final Optional doc = findFirstByFields(db.getConn(), TEST_TABLE, fields, JsonDocument.class); + assertTrue(doc.isPresent(), "The document should have been returned"); + assertEquals("four", doc.get().getId(), "An incorrect document was returned"); + assertNull(doc.get().getSub(), "The sub-document should have been removed"); + } + + public static void byFieldsMatchNoFields(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsByFields(db.getConn(), TEST_TABLE, List.of(Field.exists("nada")))); + removeFieldsByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("numValue", 17)), List.of("nada")); + // no exception = pass + } + + public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final List> fields = List.of(Field.notEqual("missing", "nope")); + assertFalse(existsByFields(db.getConn(), TEST_TABLE, fields)); + removeFieldsByFields(db.getConn(), TEST_TABLE, fields, List.of("value")); // no exception = pass + } + + public static void byContainsMatchFields(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Map> criteria = Map.of("sub", Map.of("foo", "green")); + removeFieldsByContains(db.getConn(), TEST_TABLE, criteria, List.of("value")); + final List docs = findByContains(db.getConn(), TEST_TABLE, criteria, JsonDocument.class); + assertEquals(2, docs.size(), "There should have been 2 documents returned"); + for (final JsonDocument doc : docs) { + assertTrue(List.of("two", "four").contains(doc.getId()), + String.format("An incorrect document was returned (%s)", doc.getId())); + assertEquals("", doc.getValue(), "The value should have been empty"); + } + } + + public static void byContainsMatchNoFields(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsByFields(db.getConn(), TEST_TABLE, List.of(Field.exists("invalid_field")))); + removeFieldsByContains(db.getConn(), TEST_TABLE, Map.of("sub", Map.of("foo", "green")), + List.of("invalid_field")); // no exception = pass + } + + public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final Map contains = Map.of("value", "substantial"); + assertFalse(existsByContains(db.getConn(), TEST_TABLE, contains)); + removeFieldsByContains(db.getConn(), TEST_TABLE, contains, List.of("numValue")); // no exception = pass + } + + public static void byJsonPathMatchFields(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String path = "$.value ? (@ == \"purple\")"; + removeFieldsByJsonPath(db.getConn(), TEST_TABLE, path, List.of("sub")); + final List docs = findByJsonPath(db.getConn(), TEST_TABLE, path, JsonDocument.class); + assertEquals(2, docs.size(), "There should have been 2 documents returned"); + for (final JsonDocument doc : docs) { + assertTrue(List.of("four", "five").contains(doc.getId()), + String.format("An incorrect document was returned (%s)", doc.getId())); + assertNull(doc.getSub(), "The sub-document should have been removed"); + } + } + + public static void byJsonPathMatchNoFields(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertFalse(existsByFields(db.getConn(), TEST_TABLE, List.of(Field.exists("submarine")))); + removeFieldsByJsonPath(db.getConn(), TEST_TABLE, "$.value ? (@ == \"purple\")", List.of("submarine")); + // no exception = pass + } + + public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String path = "$.value ? (@ == \"mauve\")"; + assertFalse(existsByJsonPath(db.getConn(), TEST_TABLE, path)); + removeFieldsByJsonPath(db.getConn(), TEST_TABLE, path, List.of("value")); // no exception = pass + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCountIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCountIT.java new file mode 100644 index 0000000..bc8947e --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCountIT.java @@ -0,0 +1,55 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Count") +public class SQLiteCountIT { + + @Test + @DisplayName("all counts all documents") + public void all() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CountFunctions.all(db); + } + } + + @Test + @DisplayName("byFields counts documents by a numeric value") + public void byFieldsNumeric() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CountFunctions.byFieldsNumeric(db); + } + } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + public void byFieldsAlpha() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CountFunctions.byFieldsAlpha(db); + } + } + + @Test + @DisplayName("byContains fails") + public void byContainsMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> CountFunctions.byContainsMatch(db)); + } + } + + @Test + @DisplayName("byJsonPath fails") + public void byJsonPathMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> CountFunctions.byJsonPathMatch(db)); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java new file mode 100644 index 0000000..b2e25bf --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java @@ -0,0 +1,133 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +/** + * SQLite integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Custom") +final public class SQLiteCustomIT { + + @Test + @DisplayName("list succeeds with empty list") + public void listEmpty() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.listEmpty(db); + } + } + + @Test + @DisplayName("list succeeds with a non-empty list") + public void listAll() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.listAll(db); + } + } + + @Test + @DisplayName("jsonArray succeeds with empty array") + public void jsonArrayEmpty() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.jsonArrayEmpty(db); + } + } + + @Test + @DisplayName("jsonArray succeeds with a single-item array") + public void jsonArraySingle() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.jsonArraySingle(db); + } + } + + @Test + @DisplayName("jsonArray succeeds with a multi-item array") + public void jsonArrayMany() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.jsonArrayMany(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with empty array") + public void writeJsonArrayEmpty() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.writeJsonArrayEmpty(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + public void writeJsonArraySingle() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.writeJsonArraySingle(db); + } + } + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + public void writeJsonArrayMany() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.writeJsonArrayMany(db); + } + } + + @Test + @DisplayName("single succeeds when document not found") + public void singleNone() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.singleNone(db); + } + } + + @Test + @DisplayName("single succeeds when a document is found") + public void singleOne() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.singleOne(db); + } + } + + @Test + @DisplayName("jsonSingle succeeds when document not found") + public void jsonSingleNone() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.jsonSingleNone(db); + } + } + + @Test + @DisplayName("jsonSingle succeeds when a document is found") + public void jsonSingleOne() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.jsonSingleOne(db); + } + } + + @Test + @DisplayName("nonQuery makes changes") + public void nonQueryChanges() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.nonQueryChanges(db); + } + } + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + public void nonQueryNoChanges() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.nonQueryNoChanges(db); + } + } + + @Test + @DisplayName("scalar succeeds") + public void scalar() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + CustomFunctions.scalar(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDefinitionIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDefinitionIT.java new file mode 100644 index 0000000..eabb698 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDefinitionIT.java @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Definition") +final public class SQLiteDefinitionIT { + + @Test + @DisplayName("ensureTable creates table and index") + public void ensureTable() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DefinitionFunctions.ensuresATable(db); + } + } + + @Test + @DisplayName("ensureFieldIndex creates an index") + public void ensureFieldIndex() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DefinitionFunctions.ensuresAFieldIndex(db); + } + } + + @Test + @DisplayName("ensureDocumentIndex fails for full index") + public void ensureDocumentIndexFull() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> DefinitionFunctions.ensureDocumentIndexFull(db)); + } + } + + @Test + @DisplayName("ensureDocumentIndex fails for optimized index") + public void ensureDocumentIndexOptimized() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> DefinitionFunctions.ensureDocumentIndexOptimized(db)); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDeleteIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDeleteIT.java new file mode 100644 index 0000000..bfcf9d7 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDeleteIT.java @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Delete") +final public class SQLiteDeleteIT { + + @Test + @DisplayName("byId deletes a matching ID") + public void byIdMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DeleteFunctions.byIdMatch(db); + } + } + + @Test + @DisplayName("byId succeeds when no ID matches") + public void byIdNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DeleteFunctions.byIdNoMatch(db); + } + } + + @Test + @DisplayName("byFields deletes matching documents") + public void byFieldsMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DeleteFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DeleteFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byContains fails") + public void byContainsMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> DeleteFunctions.byContainsMatch(db)); + } + } + + @Test + @DisplayName("byJsonPath fails") + public void byJsonPathMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> DeleteFunctions.byJsonPathMatch(db)); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDocumentIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDocumentIT.java new file mode 100644 index 0000000..d068af9 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteDocumentIT.java @@ -0,0 +1,84 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +/** + * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Document") +final public class SQLiteDocumentIT { + @Test + @DisplayName("insert works with default values") + public void insertDefault() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.insertDefault(db); + } + } + + @Test + @DisplayName("insert fails with duplicate key") + public void insertDupe() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.insertDupe(db); + } + } + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + public void insertNumAutoId() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.insertNumAutoId(db); + } + } + + @Test + @DisplayName("insert succeeds with UUID auto ID") + public void insertUUIDAutoId() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.insertUUIDAutoId(db); + } + } + + @Test + @DisplayName("insert succeeds with random string auto ID") + public void insertStringAutoId() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.insertStringAutoId(db); + } + } + + @Test + @DisplayName("save updates an existing document") + public void saveMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.saveMatch(db); + } + } + + @Test + @DisplayName("save inserts a new document") + public void saveNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.saveNoMatch(db); + } + } + + @Test + @DisplayName("update replaces an existing document") + public void updateMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.updateMatch(db); + } + } + + @Test + @DisplayName("update succeeds when no document exists") + public void updateNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + DocumentFunctions.updateNoMatch(db); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteExistsIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteExistsIT.java new file mode 100644 index 0000000..c6a0aa6 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteExistsIT.java @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Exists") +final public class SQLiteExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + public void byIdMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + ExistsFunctions.byIdMatch(db); + } + } + + @Test + @DisplayName("byId returns false when no document matches the ID") + public void byIdNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + ExistsFunctions.byIdNoMatch(db); + } + } + + @Test + @DisplayName("byFields returns true when documents match") + public void byFieldsMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + ExistsFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields returns false when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + ExistsFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byContains fails") + public void byContainsMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> ExistsFunctions.byContainsMatch(db)); + } + } + + @Test + @DisplayName("byJsonPath fails") + public void byJsonPathMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> ExistsFunctions.byJsonPathMatch(db)); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteFindIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteFindIT.java new file mode 100644 index 0000000..66dc051 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteFindIT.java @@ -0,0 +1,191 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Find") +final public class SQLiteFindIT { + + @Test + @DisplayName("all retrieves all documents") + public void allDefault() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.allDefault(db); + } + } + + @Test + @DisplayName("all sorts data ascending") + public void allAscending() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.allAscending(db); + } + } + + @Test + @DisplayName("all sorts data descending") + public void allDescending() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.allDescending(db); + } + } + + @Test + @DisplayName("all sorts data numerically") + public void allNumOrder() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.allNumOrder(db); + } + } + + @Test + @DisplayName("all succeeds with an empty table") + public void allEmpty() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.allEmpty(db); + } + } + + @Test + @DisplayName("byId retrieves a document via a string ID") + public void byIdString() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byIdString(db); + } + } + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + public void byIdNumber() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byIdNumber(db); + } + } + + @Test + @DisplayName("byId returns null when a matching ID is not found") + public void byIdNotFound() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byIdNotFound(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents") + public void byFieldsMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields retrieves ordered matching documents") + public void byFieldsMatchOrdered() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + public void byFieldsMatchNumIn() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byFieldsMatchNumIn(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + public void byFieldsMatchInArray() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byFieldsMatchInArray(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + public void byFieldsNoMatchInArray() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.byFieldsNoMatchInArray(db); + } + } + + @Test + @DisplayName("byContains fails") + public void byContainsFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> FindFunctions.byContainsMatch(db)); + } + } + + @Test + @DisplayName("byJsonPath fails") + public void byJsonPathFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> FindFunctions.byJsonPathMatch(db)); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + public void firstByFieldsMatchOne() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.firstByFieldsMatchOne(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + public void firstByFieldsMatchMany() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.firstByFieldsMatchMany(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + public void firstByFieldsMatchOrdered() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.firstByFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("firstByFields returns null when no document matches") + public void firstByFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + FindFunctions.firstByFieldsNoMatch(db); + } + } + + @Test + @DisplayName("firstByContains fails") + public void firstByContainsFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> FindFunctions.firstByContainsMatchOne(db)); + } + } + + @Test + @DisplayName("firstByJsonPath fails") + public void firstByJsonPathFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> FindFunctions.firstByJsonPathMatchOne(db)); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteJsonIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteJsonIT.java new file mode 100644 index 0000000..37b56aa --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteJsonIT.java @@ -0,0 +1,319 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Json") +final public class SQLiteJsonIT { + + @Test + @DisplayName("all retrieves all documents") + public void allDefault() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.allDefault(db); + } + } + + @Test + @DisplayName("all succeeds with an empty table") + public void allEmpty() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.allEmpty(db); + } + } + + @Test + @DisplayName("byId retrieves a document via a string ID") + public void byIdString() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byIdString(db); + } + } + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + public void byIdNumber() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byIdNumber(db); + } + } + + @Test + @DisplayName("byId returns null when a matching ID is not found") + public void byIdNotFound() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byIdNotFound(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents") + public void byFieldsMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields retrieves ordered matching documents") + public void byFieldsMatchOrdered() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + public void byFieldsMatchNumIn() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byFieldsMatchNumIn(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + public void byFieldsMatchInArray() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byFieldsMatchInArray(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + public void byFieldsNoMatchInArray() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.byFieldsNoMatchInArray(db); + } + } + + @Test + @DisplayName("byContains fails") + public void byContainsFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> JsonFunctions.byContainsMatch(db)); + } + } + + @Test + @DisplayName("byJsonPath fails") + public void byJsonPathFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> JsonFunctions.byJsonPathMatch(db)); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + public void firstByFieldsMatchOne() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.firstByFieldsMatchOne(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + public void firstByFieldsMatchMany() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.firstByFieldsMatchMany(db); + } + } + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + public void firstByFieldsMatchOrdered() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.firstByFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("firstByFields returns null when no document matches") + public void firstByFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.firstByFieldsNoMatch(db); + } + } + + @Test + @DisplayName("firstByContains fails") + public void firstByContainsFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> JsonFunctions.firstByContainsMatchOne(db)); + } + } + + @Test + @DisplayName("firstByJsonPath fails") + public void firstByJsonPathFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> JsonFunctions.firstByJsonPathMatchOne(db)); + } + } + + @Test + @DisplayName("writeAll retrieves all documents") + public void writeAllDefault() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeAllDefault(db); + } + } + + @Test + @DisplayName("writeAll succeeds with an empty table") + public void writeAllEmpty() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeAllEmpty(db); + } + } + + @Test + @DisplayName("writeById retrieves a document via a string ID") + public void writeByIdString() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByIdString(db); + } + } + + @Test + @DisplayName("writeById retrieves a document via a numeric ID") + public void writeByIdNumber() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByIdNumber(db); + } + } + + @Test + @DisplayName("writeById returns null when a matching ID is not found") + public void writeByIdNotFound() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByIdNotFound(db); + } + } + + @Test + @DisplayName("writeByFields retrieves matching documents") + public void writeByFieldsMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByFieldsMatch(db); + } + } + + @Test + @DisplayName("writeByFields retrieves ordered matching documents") + public void writeByFieldsMatchOrdered() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("writeByFields retrieves matching documents with a numeric IN clause") + public void writeByFieldsMatchNumIn() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByFieldsMatchNumIn(db); + } + } + + @Test + @DisplayName("writeByFields succeeds when no documents match") + public void writeByFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByFieldsNoMatch(db); + } + } + + @Test + @DisplayName("writeByFields retrieves matching documents with an IN_ARRAY comparison") + public void writeByFieldsMatchInArray() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByFieldsMatchInArray(db); + } + } + + @Test + @DisplayName("writeByFields succeeds when no documents match an IN_ARRAY comparison") + public void writeByFieldsNoMatchInArray() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeByFieldsNoMatchInArray(db); + } + } + + @Test + @DisplayName("writeByContains fails") + public void writeByContainsFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> JsonFunctions.writeByContainsMatch(db)); + } + } + + @Test + @DisplayName("writeByJsonPath fails") + public void writeByJsonPathFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> JsonFunctions.writeByJsonPathMatch(db)); + } + } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document") + public void writeFirstByFieldsMatchOne() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeFirstByFieldsMatchOne(db); + } + } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many") + public void writeFirstByFieldsMatchMany() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeFirstByFieldsMatchMany(db); + } + } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many (ordered)") + public void writeFirstByFieldsMatchOrdered() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeFirstByFieldsMatchOrdered(db); + } + } + + @Test + @DisplayName("writeFirstByFields returns null when no document matches") + public void writeFirstByFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + JsonFunctions.writeFirstByFieldsNoMatch(db); + } + } + + @Test + @DisplayName("writeFirstByContains fails") + public void writeFirstByContainsFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> JsonFunctions.writeFirstByContainsMatchOne(db)); + } + } + + @Test + @DisplayName("writeFirstByJsonPath fails") + public void writeFirstByJsonPathFails() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> JsonFunctions.writeFirstByJsonPathMatchOne(db)); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLitePatchIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLitePatchIT.java new file mode 100644 index 0000000..293d6d4 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLitePatchIT.java @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: Patch") +final public class SQLitePatchIT { + + @Test + @DisplayName("byId patches an existing document") + public void byIdMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + PatchFunctions.byIdMatch(db); + } + } + + @Test + @DisplayName("byId succeeds for a non-existent document") + public void byIdNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + PatchFunctions.byIdNoMatch(db); + } + } + + @Test + @DisplayName("byFields patches matching document") + public void byFieldsMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + PatchFunctions.byFieldsMatch(db); + } + } + + @Test + @DisplayName("byFields succeeds when no documents match") + public void byFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + PatchFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byContains fails") + public void byContainsMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> ExistsFunctions.byContainsMatch(db)); + } + } + + @Test + @DisplayName("byJsonPath fails") + public void byJsonPathMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> ExistsFunctions.byJsonPathMatch(db)); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteRemoveFieldsIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteRemoveFieldsIT.java new file mode 100644 index 0000000..3c825cc --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteRemoveFieldsIT.java @@ -0,0 +1,79 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("Core | Java | SQLite: RemoveFields") +final public class SQLiteRemoveFieldsIT { + + @Test + @DisplayName("byId removes fields from an existing document") + public void byIdMatchFields() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + RemoveFieldsFunctions.byIdMatchFields(db); + } + } + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + public void byIdMatchNoFields() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + RemoveFieldsFunctions.byIdMatchNoFields(db); + } + } + + @Test + @DisplayName("byId succeeds when no document exists") + public void byIdNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + RemoveFieldsFunctions.byIdNoMatch(db); + } + } + + @Test + @DisplayName("byFields removes fields from matching documents") + public void byFieldsMatchFields() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + RemoveFieldsFunctions.byFieldsMatchFields(db); + } + } + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + public void byFieldsMatchNoFields() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + RemoveFieldsFunctions.byFieldsMatchNoFields(db); + } + } + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + public void byFieldsNoMatch() throws DocumentException { + try (SQLiteDB db = new SQLiteDB()) { + RemoveFieldsFunctions.byFieldsNoMatch(db); + } + } + + @Test + @DisplayName("byContains fails") + public void byContainsMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> RemoveFieldsFunctions.byContainsMatchFields(db)); + } + } + + @Test + @DisplayName("byJsonPath fails") + public void byJsonPathMatch() { + try (SQLiteDB db = new SQLiteDB()) { + assertThrows(DocumentException.class, () -> RemoveFieldsFunctions.byJsonPathMatchFields(db)); + } + } +} diff --git a/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SubDocument.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SubDocument.java new file mode 100644 index 0000000..843b51d --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SubDocument.java @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +public class SubDocument { + + private String foo; + private String bar; + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + public SubDocument(String foo, String bar) { + this.foo = foo; + this.bar = bar; + } + + public SubDocument() { + this("", ""); + } +} diff --git a/src/core/src/test/kotlin/AutoIdTest.kt b/src/core/src/test/kotlin/AutoIdTest.kt new file mode 100644 index 0000000..84f6f3e --- /dev/null +++ b/src/core/src/test/kotlin/AutoIdTest.kt @@ -0,0 +1,167 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.DocumentException +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +/** + * Unit tests for the `AutoId` enum + */ +@DisplayName("Core | Kotlin | AutoId") +class AutoIdTest { + + @Test + @DisplayName("Generates a UUID string") + fun generateUUID() = + assertEquals(32, AutoId.generateUUID().length, "The UUID should have been a 32-character string") + + @Test + @DisplayName("Generates a random hex character string of an even length") + fun generateRandomStringEven() { + val result = AutoId.generateRandomString(8) + assertEquals(8, result.length, "There should have been 8 characters in $result") + } + + @Test + @DisplayName("Generates a random hex character string of an odd length") + fun generateRandomStringOdd() { + val result = AutoId.generateRandomString(11) + assertEquals(11, result.length, "There should have been 11 characters in $result") + } + + @Test + @DisplayName("Generates different random hex character strings") + fun generateRandomStringIsRandom() { + val result1 = AutoId.generateRandomString(16) + val result2 = AutoId.generateRandomString(16) + assertNotEquals(result1, result2, "There should have been 2 different strings generated") + } + + @Test + @DisplayName("needsAutoId fails for null document") + fun needsAutoIdFailsForNullDocument() { + assertThrows { AutoId.needsAutoId(AutoId.DISABLED, null, "id") } + } + + @Test + @DisplayName("needsAutoId fails for missing ID property") + fun needsAutoIdFailsForMissingId() { + assertThrows { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } + } + + @Test + @DisplayName("needsAutoId returns false if disabled") + fun needsAutoIdFalseIfDisabled() = + assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false") + + @Test + @DisplayName("needsAutoId returns true for Number strategy and byte ID of 0") + fun needsAutoIdTrueForByteWithZero() = + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id"), "Number Auto ID with 0 should return true") + + @Test + @DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0") + fun needsAutoIdFalseForByteWithNonZero() = + assertFalse( + AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id"), + "Number Auto ID with 77 should return false" + ) + + @Test + @DisplayName("needsAutoId returns true for Number strategy and short ID of 0") + fun needsAutoIdTrueForShortWithZero() = + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id"), "Number Auto ID with 0 should return true") + + @Test + @DisplayName("needsAutoId returns false for Number strategy and short ID of non-0") + fun needsAutoIdFalseForShortWithNonZero() = + assertFalse( + AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id"), + "Number Auto ID with 31 should return false" + ) + + @Test + @DisplayName("needsAutoId returns true for Number strategy and int ID of 0") + fun needsAutoIdTrueForIntWithZero() = + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id"), "Number Auto ID with 0 should return true") + + @Test + @DisplayName("needsAutoId returns false for Number strategy and int ID of non-0") + fun needsAutoIdFalseForIntWithNonZero() = + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id"), "Number Auto ID with 6 should return false") + + @Test + @DisplayName("needsAutoId returns true for Number strategy and long ID of 0") + fun needsAutoIdTrueForLongWithZero() = + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id"), "Number Auto ID with 0 should return true") + + @Test + @DisplayName("needsAutoId returns false for Number strategy and long ID of non-0") + fun needsAutoIdFalseForLongWithNonZero() = + assertFalse( + AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id"), + "Number Auto ID with 2 should return false" + ) + + @Test + @DisplayName("needsAutoId fails for Number strategy and non-number ID") + fun needsAutoIdFailsForNumberWithStringId() { + assertThrows { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } + } + + @Test + @DisplayName("needsAutoId returns true for UUID strategy and blank ID") + fun needsAutoIdTrueForUUIDWithBlank() = + assertTrue( + AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id"), + "UUID Auto ID with blank should return true" + ) + + @Test + @DisplayName("needsAutoId returns false for UUID strategy and non-blank ID") + fun needsAutoIdFalseForUUIDNotBlank() = + assertFalse( + AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id"), + "UUID Auto ID with non-blank should return false" + ) + + @Test + @DisplayName("needsAutoId fails for UUID strategy and non-string ID") + fun needsAutoIdFailsForUUIDNonString() { + assertThrows { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } + } + + @Test + @DisplayName("needsAutoId returns true for Random String strategy and blank ID") + fun needsAutoIdTrueForRandomWithBlank() = + assertTrue( + AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id"), + "Random String Auto ID with blank should return true" + ) + + @Test + @DisplayName("needsAutoId returns false for Random String strategy and non-blank ID") + fun needsAutoIdFalseForRandomNotBlank() = + assertFalse( + AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id"), + "Random String Auto ID with non-blank should return false" + ) + + @Test + @DisplayName("needsAutoId fails for Random String strategy and non-string ID") + fun needsAutoIdFailsForRandomNonString() { + assertThrows { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } + } +} + +data class ByteIdClass(val id: Byte) +data class ShortIdClass(val id: Short) +data class IntIdClass(val id: Int) +data class LongIdClass(val id: Long) +data class StringIdClass(val id: String) diff --git a/src/core/src/test/kotlin/ComparisonTest.kt b/src/core/src/test/kotlin/ComparisonTest.kt new file mode 100644 index 0000000..397ad3f --- /dev/null +++ b/src/core/src/test/kotlin/ComparisonTest.kt @@ -0,0 +1,169 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.* +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Unit tests for the `ComparisonBetween` class + */ +@DisplayName("Core | Kotlin | ComparisonBetween") +class ComparisonBetweenTest { + + @Test + @DisplayName("op is set to BETWEEN") + fun op() = + assertEquals(Op.BETWEEN, ComparisonBetween(Pair(0, 0)).op, "Between comparison should have BETWEEN op") + + @Test + @DisplayName("isNumeric is false with strings") + fun isNumericFalseForStringsAndBetween() = + assertFalse( + ComparisonBetween(Pair("eh", "zed")).isNumeric, + "A BETWEEN with strings should not be numeric") + + @Test + @DisplayName("isNumeric is true with bytes") + fun isNumericTrueForByteAndBetween() = + assertTrue(ComparisonBetween(Pair(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric") + + @Test + @DisplayName("isNumeric is true with shorts") + fun isNumericTrueForShortAndBetween() = + assertTrue( + ComparisonBetween(Pair(0, 9)).isNumeric, + "A BETWEEN with shorts should be numeric") + + @Test + @DisplayName("isNumeric is true with ints") + fun isNumericTrueForIntAndBetween() = + assertTrue(ComparisonBetween(Pair(15, 44)).isNumeric, "A BETWEEN with ints should be numeric") + + @Test + @DisplayName("isNumeric is true with longs") + fun isNumericTrueForLongAndBetween() = + assertTrue(ComparisonBetween(Pair(9L, 12L)).isNumeric, "A BETWEEN with longs should be numeric") +} + +/** + * Unit tests for the `ComparisonIn` class + */ +@DisplayName("Core | Kotlin | ComparisonIn") +class ComparisonInTest { + + @Test + @DisplayName("op is set to IN") + fun op() = + assertEquals(Op.IN, ComparisonIn(listOf()).op, "In comparison should have IN op") + + @Test + @DisplayName("isNumeric is false for empty list of values") + fun isNumericFalseForEmptyList() = + assertFalse(ComparisonIn(listOf()).isNumeric, "An IN with empty list should not be numeric") + + @Test + @DisplayName("isNumeric is false with strings") + fun isNumericFalseForStringsAndIn() = + assertFalse(ComparisonIn(listOf("a", "b", "c")).isNumeric, "An IN with strings should not be numeric") + + @Test + @DisplayName("isNumeric is true with bytes") + fun isNumericTrueForByteAndIn() = + assertTrue(ComparisonIn(listOf(4, 8)).isNumeric, "An IN with bytes should be numeric") + + @Test + @DisplayName("isNumeric is true with shorts") + fun isNumericTrueForShortAndIn() = + assertTrue(ComparisonIn(listOf(18, 22)).isNumeric, "An IN with shorts should be numeric") + + @Test + @DisplayName("isNumeric is true with ints") + fun isNumericTrueForIntAndIn() = + assertTrue(ComparisonIn(listOf(7, 8, 9)).isNumeric, "An IN with ints should be numeric") + + @Test + @DisplayName("isNumeric is true with longs") + fun isNumericTrueForLongAndIn() = + assertTrue(ComparisonIn(listOf(3L)).isNumeric, "An IN with longs should be numeric") +} + +/** + * Unit tests for the `ComparisonInArray` class + */ +@DisplayName("Core | Kotlin | ComparisonInArray") +class ComparisonInArrayTest { + + @Test + @DisplayName("op is set to IN_ARRAY") + fun op() = + assertEquals( + Op.IN_ARRAY, + ComparisonInArray(Pair("tbl", listOf())).op, + "InArray comparison should have IN_ARRAY op" + ) + + @Test + @DisplayName("isNumeric is false for empty list of values") + fun isNumericFalseForEmptyList() = + assertFalse(ComparisonIn(listOf()).isNumeric, "An IN_ARRAY with empty list should not be numeric") + + @Test + @DisplayName("isNumeric is false with strings") + fun isNumericFalseForStringsAndIn() = + assertFalse(ComparisonIn(listOf("a", "b", "c")).isNumeric, "An IN_ARRAY with strings should not be numeric") + + @Test + @DisplayName("isNumeric is false with bytes") + fun isNumericTrueForByteAndIn() = + assertTrue(ComparisonIn(listOf(4, 8)).isNumeric, "An IN_ARRAY with bytes should not be numeric") + + @Test + @DisplayName("isNumeric is false with shorts") + fun isNumericTrueForShortAndIn() = + assertTrue(ComparisonIn(listOf(18, 22)).isNumeric, "An IN_ARRAY with shorts should not be numeric") + + @Test + @DisplayName("isNumeric is false with ints") + fun isNumericTrueForIntAndIn() = + assertTrue(ComparisonIn(listOf(7, 8, 9)).isNumeric, "An IN_ARRAY with ints should not be numeric") + + @Test + @DisplayName("isNumeric is false with longs") + fun isNumericTrueForLongAndIn() = + assertTrue(ComparisonIn(listOf(3L)).isNumeric, "An IN_ARRAY with longs should not be numeric") +} + +/** + * Unit tests for the `ComparisonSingle` class + */ +@DisplayName("Core | Kotlin | ComparisonSingle") +class ComparisonSingleTest { + + @Test + @DisplayName("isNumeric is false for string value") + fun isNumericFalseForString() = + assertFalse(ComparisonSingle(Op.EQUAL, "80").isNumeric, "A string should not be numeric") + + @Test + @DisplayName("isNumeric is true for byte value") + fun isNumericTrueForByte() = + assertTrue(ComparisonSingle(Op.EQUAL, 47.toByte()).isNumeric, "A byte should be numeric") + + @Test + @DisplayName("isNumeric is true for short value") + fun isNumericTrueForShort() = + assertTrue(ComparisonSingle(Op.EQUAL, 2.toShort()).isNumeric, "A short should be numeric") + + @Test + @DisplayName("isNumeric is true for int value") + fun isNumericTrueForInt() = + assertTrue(ComparisonSingle(Op.EQUAL, 555).isNumeric, "An int should be numeric") + + @Test + @DisplayName("isNumeric is true for long value") + fun isNumericTrueForLong() = + assertTrue(ComparisonSingle(Op.EQUAL, 82L).isNumeric, "A long should be numeric") +} diff --git a/src/core/src/test/kotlin/ConfigurationTest.kt b/src/core/src/test/kotlin/ConfigurationTest.kt new file mode 100644 index 0000000..a740366 --- /dev/null +++ b/src/core/src/test/kotlin/ConfigurationTest.kt @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import kotlin.test.assertEquals + +/** + * Unit tests for the `Configuration` object + */ +@DisplayName("Core | Kotlin | Configuration") +class ConfigurationTest { + + @Test + @DisplayName("Default ID field is `id`") + fun defaultIdField() { + assertEquals("id", Configuration.idField, "Default ID field incorrect") + } + + @Test + @DisplayName("Default Auto ID strategy is `DISABLED`") + fun defaultAutoId() { + assertEquals(AutoId.DISABLED, Configuration.autoIdStrategy, "Default Auto ID strategy should be `disabled`") + } + + @Test + @DisplayName("Default ID string length should be 16") + fun defaultIdStringLength() { + assertEquals(16, Configuration.idStringLength, "Default ID string length should be 16") + } + + @Test + @DisplayName("Dialect is derived from connection string") + fun dialectIsDerived() { + try { + assertThrows { Configuration.dialect() } + Configuration.connectionString = "jdbc:postgresql:db" + assertEquals(Dialect.POSTGRESQL, Configuration.dialect()) + } finally { + Configuration.connectionString = null + } + } +} diff --git a/src/core/src/test/kotlin/CountQueryTest.kt b/src/core/src/test/kotlin/CountQueryTest.kt new file mode 100644 index 0000000..d820473 --- /dev/null +++ b/src/core/src/test/kotlin/CountQueryTest.kt @@ -0,0 +1,90 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.CountQuery +import kotlin.test.assertEquals + +/** + * Unit tests for the `Count` object + */ +@DisplayName("Core | Kotlin | Query | CountQuery") +class CountQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("all generates correctly") + fun all() = + assertEquals( + "SELECT COUNT(*) AS it FROM $TEST_TABLE", + CountQuery.all(TEST_TABLE), + "Count query not constructed correctly" + ) + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + fun byFieldsPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0", + CountQuery.byFields(TEST_TABLE, listOf(Field.equal("test", "", ":field0"))), + "Count query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + fun byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals( + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0", + CountQuery.byFields(TEST_TABLE, listOf(Field.equal("test", "", ":field0"))), + "Count query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + fun byContainsPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria", CountQuery.byContains(TEST_TABLE), + "Count query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains fails | SQLite") + fun byContainsSQLite() { + ForceDialect.sqlite() + assertThrows { CountQuery.byContains(TEST_TABLE) } + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + fun byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)", + CountQuery.byJsonPath(TEST_TABLE), "Count query not constructed correctly" + ) + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + fun byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows { CountQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/core/src/test/kotlin/DefinitionQueryTest.kt b/src/core/src/test/kotlin/DefinitionQueryTest.kt new file mode 100644 index 0000000..697fffa --- /dev/null +++ b/src/core/src/test/kotlin/DefinitionQueryTest.kt @@ -0,0 +1,137 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.query.DefinitionQuery +import kotlin.test.assertEquals + +/** + * Unit tests for the `Definition` object + */ +@DisplayName("Core | Kotlin | Query | DefinitionQuery") +class DefinitionQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("ensureTableFor generates correctly") + fun ensureTableFor() = + assertEquals( + "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", + DefinitionQuery.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly" + ) + + @Test + @DisplayName("ensureTable generates correctly | PostgreSQL") + fun ensureTablePostgres() { + ForceDialect.postgres() + assertEquals( + "CREATE TABLE IF NOT EXISTS $TEST_TABLE (data JSONB NOT NULL)", + DefinitionQuery.ensureTable(TEST_TABLE) + ) + } + + @Test + @DisplayName("ensureTable generates correctly | SQLite") + fun ensureTableSQLite() { + ForceDialect.sqlite() + assertEquals( + "CREATE TABLE IF NOT EXISTS $TEST_TABLE (data TEXT NOT NULL)", + DefinitionQuery.ensureTable(TEST_TABLE) + ) + } + + @Test + @DisplayName("ensureTable fails when no dialect is set") + fun ensureTableFailsUnknown() { + assertThrows { DefinitionQuery.ensureTable(TEST_TABLE) } + } + + @Test + @DisplayName("ensureKey generates correctly with schema") + fun ensureKeyWithSchema() = + assertEquals( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", + DefinitionQuery.ensureKey("test.table", Dialect.POSTGRESQL), + "CREATE INDEX for key statement with schema not constructed correctly" + ) + + @Test + @DisplayName("ensureKey generates correctly without schema") + fun ensureKeyWithoutSchema() = + assertEquals( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))", + DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE), + "CREATE INDEX for key statement without schema not constructed correctly" + ) + + @Test + @DisplayName("ensureIndexOn generates multiple fields and directions") + fun ensureIndexOnMultipleFields() = + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", + DefinitionQuery.ensureIndexOn( + "test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"), + Dialect.POSTGRESQL + ), + "CREATE INDEX for multiple field statement not constructed correctly" + ) + + @Test + @DisplayName("ensureIndexOn generates nested field | PostgreSQL") + fun ensureIndexOnNestedPostgres() = + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data#>>'{a,b,c}'))", + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", listOf("a.b.c"), Dialect.POSTGRESQL), + "CREATE INDEX for nested PostgreSQL field incorrect" + ) + + @Test + @DisplayName("ensureIndexOn generates nested field | SQLite") + fun ensureIndexOnNestedSQLite() = + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data->'a'->'b'->>'c'))", + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", listOf("a.b.c"), Dialect.SQLITE), + "CREATE INDEX for nested SQLite field incorrect" + ) + + @Test + @DisplayName("ensureDocumentIndexOn generates Full | PostgreSQL") + fun ensureDocumentIndexOnFullPostgres() { + ForceDialect.postgres() + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)", + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL), + "CREATE INDEX for full document index incorrect" + ) + } + + @Test + @DisplayName("ensureDocumentIndexOn generates Optimized | PostgreSQL") + fun ensureDocumentIndexOnOptimizedPostgres() { + ForceDialect.postgres() + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data jsonb_path_ops)", + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED), + "CREATE INDEX for optimized document index incorrect" + ) + } + + @Test + @DisplayName("ensureDocumentIndexOn fails | SQLite") + fun ensureDocumentIndexOnFailsSQLite() { + ForceDialect.sqlite() + assertThrows { DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) } + } +} diff --git a/src/core/src/test/kotlin/DeleteQueryTest.kt b/src/core/src/test/kotlin/DeleteQueryTest.kt new file mode 100644 index 0000000..f33721b --- /dev/null +++ b/src/core/src/test/kotlin/DeleteQueryTest.kt @@ -0,0 +1,103 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.DeleteQuery +import kotlin.test.assertEquals + +/** + * Unit tests for the `Delete` object + */ +@DisplayName("Core | Kotlin | Query | DeleteQuery") +class DeleteQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + fun byIdPostgres() { + ForceDialect.postgres() + assertEquals( + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", + DeleteQuery.byId(TEST_TABLE), "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byId generates correctly | SQLite") + fun byIdSQLite() { + ForceDialect.sqlite() + assertEquals( + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", + DeleteQuery.byId(TEST_TABLE), "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + fun byFieldsPostgres() { + ForceDialect.postgres() + assertEquals( + "DELETE FROM $TEST_TABLE WHERE data->>'a' = :b", + DeleteQuery.byFields(TEST_TABLE, listOf(Field.equal("a", "", ":b"))), + "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + fun byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals( + "DELETE FROM $TEST_TABLE WHERE data->>'a' = :b", + DeleteQuery.byFields(TEST_TABLE, listOf(Field.equal("a", "", ":b"))), + "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + fun byContainsPostgres() { + ForceDialect.postgres() + assertEquals( + "DELETE FROM $TEST_TABLE WHERE data @> :criteria", + DeleteQuery.byContains(TEST_TABLE), + "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains fails | SQLite") + fun byContainsSQLite() { + ForceDialect.sqlite() + assertThrows { DeleteQuery.byContains(TEST_TABLE) } + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + fun byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals( + "DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)", + DeleteQuery.byJsonPath(TEST_TABLE), + "Delete query not constructed correctly" + ) + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + fun byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows { DeleteQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/core/src/test/kotlin/DialectTest.kt b/src/core/src/test/kotlin/DialectTest.kt new file mode 100644 index 0000000..ef7e916 --- /dev/null +++ b/src/core/src/test/kotlin/DialectTest.kt @@ -0,0 +1,44 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail + +/** + * Unit tests for the `Dialect` enum + */ +@DisplayName("Core | Kotlin | Dialect") +class DialectTest { + + @Test + @DisplayName("deriveFromConnectionString derives PostgreSQL correctly") + fun derivesPostgres() = + assertEquals( + Dialect.POSTGRESQL, Dialect.deriveFromConnectionString("jdbc:postgresql:db"), + "Dialect should have been PostgreSQL") + + @Test + @DisplayName("deriveFromConnectionString derives SQLite correctly") + fun derivesSQLite() = + assertEquals( + Dialect.SQLITE, Dialect.deriveFromConnectionString("jdbc:sqlite:memory"), + "Dialect should have been SQLite") + + @Test + @DisplayName("deriveFromConnectionString fails when the connection string is unknown") + fun deriveFailsWhenUnknown() { + try { + Dialect.deriveFromConnectionString("SQL Server") + fail("Dialect derivation should have failed") + } catch (ex: DocumentException) { + assertNotNull(ex.message, "The exception message should not have been null") + assertTrue(ex.message!!.contains("[SQL Server]"), + "The connection string should have been in the exception message") + } + } +} diff --git a/src/core/src/test/kotlin/DocumentIndexTest.kt b/src/core/src/test/kotlin/DocumentIndexTest.kt new file mode 100644 index 0000000..5ee6ef0 --- /dev/null +++ b/src/core/src/test/kotlin/DocumentIndexTest.kt @@ -0,0 +1,25 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentIndex +import kotlin.test.assertEquals + +/** + * Unit tests for the `DocumentIndex` enum + */ +@DisplayName("Core | Kotlin | DocumentIndex") +class DocumentIndexTest { + + @Test + @DisplayName("FULL uses proper SQL") + fun fullSQL() { + assertEquals("", DocumentIndex.FULL.sql, "The SQL for Full is incorrect") + } + + @Test + @DisplayName("OPTIMIZED uses proper SQL") + fun optimizedSQL() { + assertEquals(" jsonb_path_ops", DocumentIndex.OPTIMIZED.sql, "The SQL for Optimized is incorrect") + } +} diff --git a/src/core/src/test/kotlin/DocumentQueryTest.kt b/src/core/src/test/kotlin/DocumentQueryTest.kt new file mode 100644 index 0000000..3565b41 --- /dev/null +++ b/src/core/src/test/kotlin/DocumentQueryTest.kt @@ -0,0 +1,152 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.query.DocumentQuery +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Unit tests for the `Document` object + */ +@DisplayName("Core | Kotlin | Query | DocumentQuery") +class DocumentQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("insert generates no auto ID | PostgreSQL") + fun insertNoAutoPostgres() { + ForceDialect.postgres() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data)", DocumentQuery.insert(TEST_TABLE)) + } + + @Test + @DisplayName("insert generates no auto ID | SQLite") + fun insertNoAutoSQLite() { + ForceDialect.sqlite() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data)", DocumentQuery.insert(TEST_TABLE)) + } + + @Test + @DisplayName("insert generates auto number | PostgreSQL") + fun insertAutoNumberPostgres() { + ForceDialect.postgres() + assertEquals( + "INSERT INTO $TEST_TABLE VALUES (:data::jsonb || ('{\"id\":' " + + "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $TEST_TABLE) || '}')::jsonb)", + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER) + ) + } + + @Test + @DisplayName("insert generates auto number | SQLite") + fun insertAutoNumberSQLite() { + ForceDialect.sqlite() + assertEquals( + "INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$.id', " + + "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $TEST_TABLE)))", + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER) + ) + } + + @Test + @DisplayName("insert generates auto UUID | PostgreSQL") + fun insertAutoUUIDPostgres() { + ForceDialect.postgres() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + assertTrue( + query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\""), + "Query start not correct (actual: $query)" + ) + assertTrue(query.endsWith("\"}')"), "Query end not correct") + } + + @Test + @DisplayName("insert generates auto UUID | SQLite") + fun insertAutoUUIDSQLite() { + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + assertTrue( + query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$.id', '"), + "Query start not correct (actual: $query)" + ) + assertTrue(query.endsWith("'))"), "Query end not correct") + } + + @Test + @DisplayName("insert generates auto random string | PostgreSQL") + fun insertAutoRandomPostgres() { + try { + ForceDialect.postgres() + Configuration.idStringLength = 8 + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + assertTrue( + query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\""), + "Query start not correct (actual: $query)" + ) + assertTrue(query.endsWith("\"}')"), "Query end not correct") + assertEquals( + 8, + query.replace("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", "") + .replace("\"}')", "").length, + "Random string length incorrect" + ) + } finally { + Configuration.idStringLength = 16 + } + } + + @Test + @DisplayName("insert generates auto random string | SQLite") + fun insertAutoRandomSQLite() { + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + assertTrue( + query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$.id', '"), + "Query start not correct (actual: $query)" + ) + assertTrue(query.endsWith("'))"), "Query end not correct") + assertEquals( + Configuration.idStringLength, + query.replace("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length, + "Random string length incorrect" + ) + } + + @Test + @DisplayName("insert fails when no dialect is set") + fun insertFailsUnknown() { + assertThrows { DocumentQuery.insert(TEST_TABLE) } + } + + @Test + @DisplayName("save generates correctly") + fun save() { + ForceDialect.postgres() + assertEquals( + "INSERT INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", + DocumentQuery.save(TEST_TABLE), "INSERT ON CONFLICT UPDATE statement not constructed correctly" + ) + } + + @Test + @DisplayName("update generates successfully") + fun update() = + assertEquals( + "UPDATE $TEST_TABLE SET data = :data", + DocumentQuery.update(TEST_TABLE), + "Update query not constructed correctly" + ) +} diff --git a/src/core/src/test/kotlin/ExistsQueryTest.kt b/src/core/src/test/kotlin/ExistsQueryTest.kt new file mode 100644 index 0000000..bfd066e --- /dev/null +++ b/src/core/src/test/kotlin/ExistsQueryTest.kt @@ -0,0 +1,102 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.ExistsQuery +import kotlin.test.assertEquals + +/** + * Unit tests for the `Exists` object + */ +@DisplayName("Core | Kotlin | Query | ExistsQuery") +class ExistsQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + fun byIdPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it", + ExistsQuery.byId(TEST_TABLE), "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("byId generates correctly | SQLite") + fun byIdSQLite() { + ForceDialect.sqlite() + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it", + ExistsQuery.byId(TEST_TABLE), "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + fun byFieldsPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE (data->>'it')::numeric = :test) AS it", + ExistsQuery.byFields(TEST_TABLE, listOf(Field.equal("it", 7, ":test"))), + "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + fun byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'it' = :test) AS it", + ExistsQuery.byFields(TEST_TABLE, listOf(Field.equal("it", 7, ":test"))), + "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + fun byContainsPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data @> :criteria) AS it", + ExistsQuery.byContains(TEST_TABLE), + "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains fails | SQLite") + fun byContainsSQLite() { + ForceDialect.sqlite() + assertThrows { ExistsQuery.byContains(TEST_TABLE) } + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + fun byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)) AS it", + ExistsQuery.byJsonPath(TEST_TABLE), "Exists query not constructed correctly" + ) + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + fun byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows { ExistsQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/core/src/test/kotlin/FieldMatchTest.kt b/src/core/src/test/kotlin/FieldMatchTest.kt new file mode 100644 index 0000000..25ac7d7 --- /dev/null +++ b/src/core/src/test/kotlin/FieldMatchTest.kt @@ -0,0 +1,25 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.FieldMatch +import kotlin.test.assertEquals + +/** + * Unit tests for the `FieldMatch` enum + */ +@DisplayName("Core | Kotlin | FieldMatch") +class FieldMatchTest { + + @Test + @DisplayName("ANY uses proper SQL") + fun any() { + assertEquals("OR", FieldMatch.ANY.sql, "ANY should use OR") + } + + @Test + @DisplayName("ALL uses proper SQL") + fun all() { + assertEquals("AND", FieldMatch.ALL.sql, "ALL should use AND") + } +} diff --git a/src/core/src/test/kotlin/FieldTest.kt b/src/core/src/test/kotlin/FieldTest.kt new file mode 100644 index 0000000..3753e33 --- /dev/null +++ b/src/core/src/test/kotlin/FieldTest.kt @@ -0,0 +1,595 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.* +import kotlin.test.assertEquals +import kotlin.test.assertNotSame +import kotlin.test.assertNull + +/** + * Unit tests for the `Field` class + */ +@DisplayName("Core | Kotlin | Field") +class FieldTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + // ~~~ INSTANCE METHODS ~~~ + + @Test + @DisplayName("withParameterName fails for invalid name") + fun withParamNameFails() { + assertThrows { Field.equal("it", "").withParameterName("2424") } + } + + @Test + @DisplayName("withParameterName works with colon prefix") + fun withParamNameColon() { + val field = Field.equal("abc", "22").withQualifier("me") + val withParam = field.withParameterName(":test") + assertNotSame(field, withParam, "A new Field instance should have been created") + assertEquals(field.name, withParam.name, "Name should have been preserved") + assertEquals(field.comparison, withParam.comparison, "Comparison should have been preserved") + assertEquals(":test", withParam.parameterName, "Parameter name not set correctly") + assertEquals(field.qualifier, withParam.qualifier, "Qualifier should have been preserved") + } + + @Test + @DisplayName("withParameterName works with at-sign prefix") + fun withParamNameAtSign() { + val field = Field.equal("def", "44") + val withParam = field.withParameterName("@unit") + assertNotSame(field, withParam, "A new Field instance should have been created") + assertEquals(field.name, withParam.name, "Name should have been preserved") + assertEquals(field.comparison, withParam.comparison, "Comparison should have been preserved") + assertEquals("@unit", withParam.parameterName, "Parameter name not set correctly") + assertEquals(field.qualifier, withParam.qualifier, "Qualifier should have been preserved") + } + + @Test + @DisplayName("withQualifier sets qualifier correctly") + fun withQualifier() { + val field = Field.equal("j", "k") + val withQual = field.withQualifier("test") + assertNotSame(field, withQual, "A new Field instance should have been created") + assertEquals(field.name, withQual.name, "Name should have been preserved") + assertEquals(field.comparison, withQual.comparison, "Comparison should have been preserved") + assertEquals(field.parameterName, withQual.parameterName, "Parameter Name should have been preserved") + assertEquals("test", withQual.qualifier, "Qualifier not set correctly") + } + + @Test + @DisplayName("path generates for simple unqualified PostgreSQL field") + fun pathPostgresSimpleUnqualified() = + assertEquals("data->>'SomethingCool'", + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct") + + @Test + @DisplayName("path generates for simple qualified PostgreSQL field") + fun pathPostgresSimpleQualified() = + assertEquals("this.data->>'SomethingElse'", + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not correct") + + @Test + @DisplayName("path generates for nested unqualified PostgreSQL field") + fun pathPostgresNestedUnqualified() { + assertEquals("data#>>'{My,Nested,Field}'", + Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct") + } + + @Test + @DisplayName("path generates for nested qualified PostgreSQL field") + fun pathPostgresNestedQualified() = + assertEquals("bird.data#>>'{Nest,Away}'", + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not correct") + + @Test + @DisplayName("path generates for simple unqualified SQLite field") + fun pathSQLiteSimpleUnqualified() = + assertEquals("data->>'SomethingCool'", + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct") + + @Test + @DisplayName("path generates for simple qualified SQLite field") + fun pathSQLiteSimpleQualified() = + assertEquals("this.data->>'SomethingElse'", + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL), + "Path not correct") + + @Test + @DisplayName("path generates for nested unqualified SQLite field") + fun pathSQLiteNestedUnqualified() = + assertEquals("data->'My'->'Nested'->>'Field'", + Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct") + + @Test + @DisplayName("path generates for nested qualified SQLite field") + fun pathSQLiteNestedQualified() = + assertEquals("bird.data->'Nest'->>'Away'", + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL), + "Path not correct") + + @Test + @DisplayName("toWhere generates for exists w/o qualifier | PostgreSQL") + fun toWhereExistsNoQualPostgres() { + ForceDialect.postgres() + assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for exists w/o qualifier | SQLite") + fun toWhereExistsNoQualSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for not-exists w/o qualifier | PostgreSQL") + fun toWhereNotExistsNoQualPostgres() { + ForceDialect.postgres() + assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for not-exists w/o qualifier | SQLite") + fun toWhereNotExistsNoQualSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range | PostgreSQL") + fun toWhereBetweenNoQualNumericPostgres() { + ForceDialect.postgres() + assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").toWhere(), "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range | PostgreSQL") + fun toWhereBetweenNoQualAlphaPostgres() { + ForceDialect.postgres() + assertEquals("data->>'city' BETWEEN :citymin AND :citymax", + Field.between("city", "Atlanta", "Chicago", ":city").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier | SQLite") + fun toWhereBetweenNoQualSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range | PostgreSQL") + fun toWhereBetweenQualNumericPostgres() { + ForceDialect.postgres() + assertEquals("(test.data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").withQualifier("test").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range | PostgreSQL") + fun toWhereBetweenQualAlphaPostgres() { + ForceDialect.postgres() + assertEquals("unit.data->>'city' BETWEEN :citymin AND :citymax", + Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier | SQLite") + fun toWhereBetweenQualSQLite() { + ForceDialect.sqlite() + assertEquals("my.data->>'age' BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").withQualifier("my").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for IN/any, numeric values | PostgreSQL") + fun toWhereAnyNumericPostgres() { + ForceDialect.postgres() + assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", + Field.any("even", listOf(2, 4, 6), ":nbr").toWhere(), "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for IN/any, alphanumeric values | PostgreSQL") + fun toWhereAnyAlphaPostgres() { + ForceDialect.postgres() + assertEquals("data->>'test' IN (:city_0, :city_1)", + Field.any("test", listOf("Atlanta", "Chicago"), ":city").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for IN/any | SQLite") + fun toWhereAnySQLite() { + ForceDialect.sqlite() + assertEquals("data->>'test' IN (:city_0, :city_1)", + Field.any("test", listOf("Atlanta", "Chicago"), ":city").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for inArray | PostgreSQL") + fun toWhereInArrayPostgres() { + ForceDialect.postgres() + assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]", + Field.inArray("even", "tbl", listOf(2, 4, 6, 8), ":it").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for inArray | SQLite") + fun toWhereInArraySQLite() { + ForceDialect.sqlite() + assertEquals("EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))", + Field.inArray("test", "tbl", listOf("Atlanta", "Chicago"), ":city").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for others w/o qualifier | PostgreSQL") + fun toWhereOtherNoQualPostgres() { + ForceDialect.postgres() + assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates for others w/o qualifier | SQLite") + fun toWhereOtherNoQualSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates no-parameter w/ qualifier | PostgreSQL") + fun toWhereNoParamWithQualPostgres() { + ForceDialect.postgres() + assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates no-parameter w/ qualifier | SQLite") + fun toWhereNoParamWithQualSQLite() { + ForceDialect.sqlite() + assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates parameter w/ qualifier | PostgreSQL") + fun toWhereParamWithQualPostgres() { + ForceDialect.postgres() + assertEquals("(q.data->>'le_field')::numeric <= :it", + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(), + "Field WHERE clause not generated correctly") + } + + @Test + @DisplayName("toWhere generates parameter w/ qualifier | SQLite") + fun toWhereParamWithQualSQLite() { + ForceDialect.sqlite() + assertEquals("q.data->>'le_field' <= :it", + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(), + "Field WHERE clause not generated correctly") + } + + // ~~~ COMPANION OBJECT TESTS ~~~ + + @Test + @DisplayName("equal constructs a field w/o parameter name") + fun equalCtor() { + val field = Field.equal("Test", 14) + assertEquals("Test", field.name, "Field name not filled correctly") + assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals(14, field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("equal constructs a field w/ parameter name") + fun equalParameterCtor() { + val field = Field.equal("Test", 14, ":w") + assertEquals("Test", field.name, "Field name not filled correctly") + assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals(14, field.comparison.value, "Field comparison value not filled correctly") + assertEquals(":w", field.parameterName, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("greater constructs a field w/o parameter name") + fun greaterCtor() { + val field = Field.greater("Great", "night") + assertEquals("Great", field.name, "Field name not filled correctly") + assertEquals(Op.GREATER, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("night", field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("greater constructs a field w/ parameter name") + fun greaterParameterCtor() { + val field = Field.greater("Great", "night", ":yeah") + assertEquals("Great", field.name, "Field name not filled correctly") + assertEquals(Op.GREATER, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("night", field.comparison.value, "Field comparison value not filled correctly") + assertEquals(":yeah", field.parameterName, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("greaterOrEqual constructs a field w/o parameter name") + fun greaterOrEqualCtor() { + val field = Field.greaterOrEqual("Nice", 88L) + assertEquals("Nice", field.name, "Field name not filled correctly") + assertEquals(Op.GREATER_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals(88L, field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("greaterOrEqual constructs a field w/ parameter name") + fun greaterOrEqualParameterCtor() { + val field = Field.greaterOrEqual("Nice", 88L, ":nice") + assertEquals("Nice", field.name, "Field name not filled correctly") + assertEquals(Op.GREATER_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals(88L, field.comparison.value, "Field comparison value not filled correctly") + assertEquals(":nice", field.parameterName, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("less constructs a field w/o parameter name") + fun lessCtor() { + val field = Field.less("Lesser", "seven") + assertEquals("Lesser", field.name, "Field name not filled correctly") + assertEquals(Op.LESS, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("seven", field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("less constructs a field w/ parameter name") + fun lessParameterCtor() { + val field = Field.less("Lesser", "seven", ":max") + assertEquals("Lesser", field.name, "Field name not filled correctly") + assertEquals(Op.LESS, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("seven", field.comparison.value, "Field comparison value not filled correctly") + assertEquals(":max", field.parameterName, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("lessOrEqual constructs a field w/o parameter name") + fun lessOrEqualCtor() { + val field = Field.lessOrEqual("Nobody", "KNOWS") + assertEquals("Nobody", field.name, "Field name not filled correctly") + assertEquals(Op.LESS_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("KNOWS", field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("lessOrEqual constructs a field w/ parameter name") + fun lessOrEqualParameterCtor() { + val field = Field.lessOrEqual("Nobody", "KNOWS", ":nope") + assertEquals("Nobody", field.name, "Field name not filled correctly") + assertEquals(Op.LESS_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("KNOWS", field.comparison.value, "Field comparison value not filled correctly") + assertEquals(":nope", field.parameterName, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("notEqual constructs a field w/o parameter name") + fun notEqualCtor() { + val field = Field.notEqual("Park", "here") + assertEquals("Park", field.name, "Field name not filled correctly") + assertEquals(Op.NOT_EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("here", field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("notEqual constructs a field w/ parameter name") + fun notEqualParameterCtor() { + val field = Field.notEqual("Park", "here", ":now") + assertEquals("Park", field.name, "Field name not filled correctly") + assertEquals(Op.NOT_EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("here", field.comparison.value, "Field comparison value not filled correctly") + assertEquals(":now", field.parameterName, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("between constructs a field w/o parameter name") + fun betweenCtor() { + val field = Field.between("Age", 18, 49) + assertEquals("Age", field.name, "Field name not filled correctly") + 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") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("between constructs a field w/ parameter name") + fun betweenParameterCtor() { + val field = Field.between("Age", 18, 49, ":limit") + assertEquals("Age", field.name, "Field name not filled correctly") + 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, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("any constructs a field w/o parameter name") + fun anyCtor() { + val field = Field.any("Here", listOf(8, 16, 32)) + assertEquals("Here", field.name, "Field name not filled correctly") + assertEquals(Op.IN, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals(listOf(8, 16, 32), field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("any constructs a field w/ parameter name") + fun anyParameterCtor() { + val field = Field.any("Here", listOf(8, 16, 32), ":list") + assertEquals("Here", field.name, "Field name not filled correctly") + assertEquals(Op.IN, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals(listOf(8, 16, 32), field.comparison.value, "Field comparison value not filled correctly") + assertEquals(":list", field.parameterName, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("inArray constructs a field w/o parameter name") + fun inArrayCtor() { + val field = Field.inArray("ArrayField", "table", listOf("z")) + assertEquals("ArrayField", field.name, "Field name not filled correctly") + assertEquals(Op.IN_ARRAY, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("table", field.comparison.value.first, "Field comparison table not filled correctly") + assertEquals(listOf("z"), field.comparison.value.second, "Field comparison values not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("inArray constructs a field w/ parameter name") + fun inArrayParameterCtor() { + val field = Field.inArray("ArrayField", "table", listOf("z"), ":a") + assertEquals("ArrayField", field.name, "Field name not filled correctly") + assertEquals(Op.IN_ARRAY, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("table", field.comparison.value.first, "Field comparison table not filled correctly") + assertEquals(listOf("z"), field.comparison.value.second, "Field comparison values not filled correctly") + assertEquals(":a", field.parameterName, "Field parameter name not filled correctly") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("exists constructs a field") + fun existsCtor() { + val field = Field.exists("Groovy") + assertEquals("Groovy", field.name, "Field name not filled correctly") + assertEquals(Op.EXISTS, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("", field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("notExists constructs a field") + fun notExistsCtor() { + val field = Field.notExists("Groovy") + assertEquals("Groovy", field.name, "Field name not filled correctly") + assertEquals(Op.NOT_EXISTS, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("", field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("named constructs a field") + fun namedCtor() { + val field = Field.named("Tacos") + assertEquals("Tacos", field.name, "Field name not filled correctly") + assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly") + assertEquals("", field.comparison.value, "Field comparison value not filled correctly") + assertNull(field.parameterName, "The parameter name should have been null") + assertNull(field.qualifier, "The qualifier should have been null") + } + + @Test + @DisplayName("static constructors fail for invalid parameter name") + fun staticCtorsFailOnParamName() { + assertThrows { Field.equal("a", "b", "that ain't it, Jack...") } + } + + @Test + @DisplayName("nameToPath creates a simple PostgreSQL SQL name") + fun nameToPathPostgresSimpleSQL() = + assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a simple SQLite SQL name") + fun nameToPathSQLiteSimpleSQL() = + assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a nested PostgreSQL SQL name") + fun nameToPathPostgresNestedSQL() = + assertEquals("data#>>'{A,Long,Path,to,the,Property}'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a nested SQLite SQL name") + fun nameToPathSQLiteNestedSQL() = + assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a simple PostgreSQL JSON name") + fun nameToPathPostgresSimpleJSON() = + assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a simple SQLite JSON name") + fun nameToPathSQLiteSimpleJSON() = + assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a nested PostgreSQL JSON name") + fun nameToPathPostgresNestedJSON() = + assertEquals("data#>'{A,Long,Path,to,the,Property}'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a nested SQLite JSON name") + fun nameToPathSQLiteNestedJSON() = + assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON), + "Path not constructed correctly") +} diff --git a/src/core/src/test/kotlin/FindQueryTest.kt b/src/core/src/test/kotlin/FindQueryTest.kt new file mode 100644 index 0000000..1f67bdd --- /dev/null +++ b/src/core/src/test/kotlin/FindQueryTest.kt @@ -0,0 +1,107 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.FindQuery +import kotlin.test.assertEquals + +/** + * Unit tests for the `Find` object + */ +@DisplayName("Core | Kotlin | Query | FindQuery") +class FindQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("all generates correctly") + fun all() = + assertEquals("SELECT data FROM $TEST_TABLE", FindQuery.all(TEST_TABLE), "Find query not constructed correctly") + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + fun byIdPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id", + FindQuery.byId(TEST_TABLE), "Find query not constructed correctly" + ) + } + + @Test + @DisplayName("byId generates correctly | SQLite") + fun byIdSQLite() { + ForceDialect.sqlite() + assertEquals( + "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id", + FindQuery.byId(TEST_TABLE), "Find query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + fun byFieldsPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND (data->>'c')::numeric < :d", + FindQuery.byFields(TEST_TABLE, listOf(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))), + "Find query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + fun byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals( + "SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND data->>'c' < :d", + FindQuery.byFields(TEST_TABLE, listOf(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))), + "Find query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + fun byContainsPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT data FROM $TEST_TABLE WHERE data @> :criteria", FindQuery.byContains(TEST_TABLE), + "Find query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains fails | SQLite") + fun byContainsSQLite() { + ForceDialect.sqlite() + assertThrows { FindQuery.byContains(TEST_TABLE) } + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + fun byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals( + "SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)", + FindQuery.byJsonPath(TEST_TABLE), + "Find query not constructed correctly" + ) + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + fun byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows { FindQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/core/src/test/kotlin/ForceDialect.kt b/src/core/src/test/kotlin/ForceDialect.kt new file mode 100644 index 0000000..d77dee1 --- /dev/null +++ b/src/core/src/test/kotlin/ForceDialect.kt @@ -0,0 +1,24 @@ +package solutions.bitbadger.documents.core.tests + +import solutions.bitbadger.documents.Configuration + +/** + * These functions use a dummy connection string to force the given dialect for a given test + */ +object ForceDialect { + + @JvmStatic + fun postgres() { + Configuration.connectionString = ":postgresql:" + } + + @JvmStatic + fun sqlite() { + Configuration.connectionString = ":sqlite:" + } + + @JvmStatic + fun none() { + Configuration.connectionString = null + } +} \ No newline at end of file diff --git a/src/core/src/test/kotlin/OpTest.kt b/src/core/src/test/kotlin/OpTest.kt new file mode 100644 index 0000000..1011512 --- /dev/null +++ b/src/core/src/test/kotlin/OpTest.kt @@ -0,0 +1,79 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Op +import kotlin.test.assertEquals + +/** + * Unit tests for the `Op` enum + */ +@DisplayName("Core | Kotlin | Op") +class OpTest { + + @Test + @DisplayName("EQUAL uses proper SQL") + fun equalSQL() { + assertEquals("=", Op.EQUAL.sql, "The SQL for equal is incorrect") + } + + @Test + @DisplayName("GREATER uses proper SQL") + fun greaterSQL() { + assertEquals(">", Op.GREATER.sql, "The SQL for greater is incorrect") + } + + @Test + @DisplayName("GREATER_OR_EQUAL uses proper SQL") + fun greaterOrEqualSQL() { + assertEquals(">=", Op.GREATER_OR_EQUAL.sql, "The SQL for greater-or-equal is incorrect") + } + + @Test + @DisplayName("LESS uses proper SQL") + fun lessSQL() { + assertEquals("<", Op.LESS.sql, "The SQL for less is incorrect") + } + + @Test + @DisplayName("LESS_OR_EQUAL uses proper SQL") + fun lessOrEqualSQL() { + assertEquals("<=", Op.LESS_OR_EQUAL.sql, "The SQL for less-or-equal is incorrect") + } + + @Test + @DisplayName("NOT_EQUAL uses proper SQL") + fun notEqualSQL() { + assertEquals("<>", Op.NOT_EQUAL.sql, "The SQL for not-equal is incorrect") + } + + @Test + @DisplayName("BETWEEN uses proper SQL") + fun betweenSQL() { + assertEquals("BETWEEN", Op.BETWEEN.sql, "The SQL for between is incorrect") + } + + @Test + @DisplayName("IN uses proper SQL") + fun inSQL() { + assertEquals("IN", Op.IN.sql, "The SQL for in is incorrect") + } + + @Test + @DisplayName("IN_ARRAY uses proper SQL") + fun inArraySQL() { + assertEquals("??|", Op.IN_ARRAY.sql, "The SQL for in-array is incorrect") + } + + @Test + @DisplayName("EXISTS uses proper SQL") + fun existsSQL() { + assertEquals("IS NOT NULL", Op.EXISTS.sql, "The SQL for exists is incorrect") + } + + @Test + @DisplayName("NOT_EXISTS uses proper SQL") + fun notExistsSQL() { + assertEquals("IS NULL", Op.NOT_EXISTS.sql, "The SQL for not-exists is incorrect") + } +} diff --git a/src/core/src/test/kotlin/ParameterNameTest.kt b/src/core/src/test/kotlin/ParameterNameTest.kt new file mode 100644 index 0000000..f6c6f51 --- /dev/null +++ b/src/core/src/test/kotlin/ParameterNameTest.kt @@ -0,0 +1,31 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.ParameterName +import kotlin.test.assertEquals + +/** + * Unit tests for the `ParameterName` class + */ +@DisplayName("Core | Kotlin | ParameterName") +class ParameterNameTest { + + @Test + @DisplayName("derive works when given existing names") + fun withExisting() { + val names = ParameterName() + assertEquals(":taco", names.derive(":taco"), "Name should have been :taco") + assertEquals(":field0", names.derive(null), "Counter should not have advanced for named field") + } + + @Test + @DisplayName("derive works when given all anonymous fields") + fun allAnonymous() { + val names = ParameterName() + assertEquals(":field0", names.derive(null), "Anonymous field name should have been returned") + assertEquals(":field1", names.derive(null), "Counter should have advanced from previous call") + assertEquals(":field2", names.derive(null), "Counter should have advanced from previous call") + assertEquals(":field3", names.derive(null), "Counter should have advanced from previous call") + } +} \ No newline at end of file diff --git a/src/core/src/test/kotlin/ParameterTest.kt b/src/core/src/test/kotlin/ParameterTest.kt new file mode 100644 index 0000000..7b26ebe --- /dev/null +++ b/src/core/src/test/kotlin/ParameterTest.kt @@ -0,0 +1,41 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +/** + * Unit tests for the `Parameter` class + */ +@DisplayName("Core | Kotlin | Parameter") +class ParameterTest { + + @Test + @DisplayName("Construction with colon-prefixed name") + fun ctorWithColon() { + val p = Parameter(":test", ParameterType.STRING, "ABC") + assertEquals(":test", p.name, "Parameter name was incorrect") + assertEquals(ParameterType.STRING, p.type, "Parameter type was incorrect") + assertEquals("ABC", p.value, "Parameter value was incorrect") + } + + @Test + @DisplayName("Construction with at-sign-prefixed name") + fun ctorWithAtSign() { + val p = Parameter("@yo", ParameterType.NUMBER, null) + assertEquals("@yo", p.name, "Parameter name was incorrect") + assertEquals(ParameterType.NUMBER, p.type, "Parameter type was incorrect") + assertNull(p.value, "Parameter value was incorrect") + } + + @Test + @DisplayName("Construction fails with incorrect prefix") + fun ctorFailsForPrefix() { + assertThrows { Parameter("it", ParameterType.JSON, "") } + } +} diff --git a/src/core/src/test/kotlin/ParametersTest.kt b/src/core/src/test/kotlin/ParametersTest.kt new file mode 100644 index 0000000..15dcddc --- /dev/null +++ b/src/core/src/test/kotlin/ParametersTest.kt @@ -0,0 +1,121 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.java.Parameters +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotSame +import kotlin.test.assertSame + +/** + * Unit tests for the `Parameters` object + */ +@DisplayName("Core | Kotlin | Parameters") +class ParametersTest { + + /** + * Reset the dialect + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("nameFields works with no changes") + fun nameFieldsNoChange() { + val fields = listOf(Field.equal("a", "", ":test"), Field.exists("q"), Field.equal("b", "", ":me")) + val named = Parameters.nameFields(fields) + assertEquals(fields.size, named.size, "There should have been 3 fields in the list") + assertSame(fields.elementAt(0), named.elementAt(0), "The first field should be the same") + assertSame(fields.elementAt(1), named.elementAt(1), "The second field should be the same") + assertSame(fields.elementAt(2), named.elementAt(2), "The third field should be the same") + } + + @Test + @DisplayName("nameFields works when changing fields") + fun nameFieldsChange() { + val fields = listOf( + Field.equal("a", ""), Field.equal("e", "", ":hi"), Field.equal("b", ""), Field.notExists("z") + ) + val named = Parameters.nameFields(fields) + assertEquals(fields.size, named.size, "There should have been 4 fields in the list") + assertNotSame(fields.elementAt(0), named.elementAt(0), "The first field should not be the same") + assertEquals(":field0", named.elementAt(0).parameterName, "First parameter name incorrect") + assertSame(fields.elementAt(1), named.elementAt(1), "The second field should be the same") + assertNotSame(fields.elementAt(2), named.elementAt(2), "The third field should not be the same") + assertEquals(":field1", named.elementAt(2).parameterName, "Third parameter name incorrect") + assertSame(fields.elementAt(3), named.elementAt(3), "The fourth field should be the same") + } + + @Test + @DisplayName("replaceNamesInQuery replaces successfully") + fun replaceNamesInQuery() { + val parameters = listOf( + Parameter(":data", ParameterType.JSON, "{}"), + Parameter(":data_ext", ParameterType.STRING, "") + ) + val query = "SELECT data, data_ext FROM tbl WHERE data = :data AND data_ext = :data_ext AND more_data = :data" + assertEquals("SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?", + Parameters.replaceNamesInQuery(query, parameters), "Parameters not replaced correctly") + } + + @Test + @DisplayName("fieldNames generates a single parameter (PostgreSQL)") + fun fieldNamesSinglePostgres() { + ForceDialect.postgres() + val nameParams = Parameters.fieldNames(listOf("test")).toList() + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name", nameParams[0].name, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[0].type, "The parameter type is incorrect") + assertEquals("{test}", nameParams[0].value, "The parameter value is incorrect") + } + + @Test + @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") + fun fieldNamesMultiplePostgres() { + ForceDialect.postgres() + val nameParams = Parameters.fieldNames(listOf("test", "this", "today")).toList() + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name", nameParams[0].name, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[0].type, "The parameter type is incorrect") + assertEquals("{test,this,today}", nameParams[0].value, "The parameter value is incorrect") + } + + @Test + @DisplayName("fieldNames generates a single parameter (SQLite)") + fun fieldNamesSingleSQLite() { + ForceDialect.sqlite() + val nameParams = Parameters.fieldNames(listOf("test")).toList() + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name0", nameParams[0].name, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[0].type, "The parameter type is incorrect") + assertEquals("test", nameParams[0].value, "The parameter value is incorrect") + } + + @Test + @DisplayName("fieldNames generates multiple parameters (SQLite)") + fun fieldNamesMultipleSQLite() { + ForceDialect.sqlite() + val nameParams = Parameters.fieldNames(listOf("test", "this", "today")).toList() + assertEquals(3, nameParams.size, "There should be one name parameter") + assertEquals(":name0", nameParams[0].name, "The first parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[0].type, "The first parameter type is incorrect") + assertEquals("test", nameParams[0].value, "The first parameter value is incorrect") + assertEquals(":name1", nameParams[1].name, "The second parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[1].type, "The second parameter type is incorrect") + assertEquals("this", nameParams[1].value, "The second parameter value is incorrect") + assertEquals(":name2", nameParams[2].name, "The third parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams[2].type, "The third parameter type is incorrect") + assertEquals("today", nameParams[2].value, "The third parameter value is incorrect") + } + + @Test + @DisplayName("fieldNames fails if dialect not set") + fun fieldNamesFails() { + assertThrows { Parameters.fieldNames(listOf()) } + } +} diff --git a/src/core/src/test/kotlin/PatchQueryTest.kt b/src/core/src/test/kotlin/PatchQueryTest.kt new file mode 100644 index 0000000..0d17457 --- /dev/null +++ b/src/core/src/test/kotlin/PatchQueryTest.kt @@ -0,0 +1,101 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.PatchQuery +import kotlin.test.assertEquals + +/** + * Unit tests for the `Patch` object + */ +@DisplayName("Core | Kotlin | Query | PatchQuery") +class PatchQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + fun byIdPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id", + PatchQuery.byId(TEST_TABLE), "Patch query not constructed correctly" + ) + } + + @Test + @DisplayName("byId generates correctly | SQLite") + fun byIdSQLite() { + ForceDialect.sqlite() + assertEquals( + "UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id", + PatchQuery.byId(TEST_TABLE), "Patch query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + fun byFieldsPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y", + PatchQuery.byFields(TEST_TABLE, listOf(Field.equal("z", "", ":y"))), + "Patch query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + fun byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals( + "UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y", + PatchQuery.byFields(TEST_TABLE, listOf(Field.equal("z", "", ":y"))), + "Patch query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + fun byContainsPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria", PatchQuery.byContains(TEST_TABLE), + "Patch query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains fails | SQLite") + fun byContainsSQLite() { + ForceDialect.sqlite() + assertThrows { PatchQuery.byContains(TEST_TABLE) } + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + fun byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)", + PatchQuery.byJsonPath(TEST_TABLE), "Patch query not constructed correctly" + ) + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + fun byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows { PatchQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/core/src/test/kotlin/QueryTest.kt b/src/core/src/test/kotlin/QueryTest.kt new file mode 100644 index 0000000..3bd65f6 --- /dev/null +++ b/src/core/src/test/kotlin/QueryTest.kt @@ -0,0 +1,178 @@ +package solutions.bitbadger.documents.core.tests + +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.* +import kotlin.test.assertEquals + +/** + * Unit tests for the package-level query functions + */ +@DisplayName("Core | Kotlin | Query | Package Functions") +class QueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("statementWhere generates correctly") + fun statementWhere() = + assertEquals("x WHERE y", statementWhere("x", "y"), "Statements not combined correctly") + + @Test + @DisplayName("byId generates a numeric ID query | PostgreSQL") + fun byIdNumericPostgres() { + ForceDialect.postgres() + assertEquals("test WHERE (data->>'id')::numeric = :id", byId("test", 9)) + } + + @Test + @DisplayName("byId generates an alphanumeric ID query | PostgreSQL") + fun byIdAlphaPostgres() { + ForceDialect.postgres() + assertEquals("unit WHERE data->>'id' = :id", byId("unit", "18")) + } + + @Test + @DisplayName("byId generates ID query | SQLite") + fun byIdSQLite() { + ForceDialect.sqlite() + assertEquals("yo WHERE data->>'id' = :id", byId("yo", 27)) + } + + @Test + @DisplayName("byFields generates default field query | PostgreSQL") + fun byFieldsMultipleDefaultPostgres() { + ForceDialect.postgres() + assertEquals( + "this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value", + byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value"))) + ) + } + + @Test + @DisplayName("byFields generates default field query | SQLite") + fun byFieldsMultipleDefaultSQLite() { + ForceDialect.sqlite() + assertEquals( + "this WHERE data->>'a' = :the_a AND data->>'b' = :b_value", + byFields("this", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value"))) + ) + } + + @Test + @DisplayName("byFields generates ANY field query | PostgreSQL") + fun byFieldsMultipleAnyPostgres() { + ForceDialect.postgres() + assertEquals( + "that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value", + byFields( + "that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")), + FieldMatch.ANY + ) + ) + } + + @Test + @DisplayName("byFields generates ANY field query | SQLite") + fun byFieldsMultipleAnySQLite() { + ForceDialect.sqlite() + assertEquals( + "that WHERE data->>'a' = :the_a OR data->>'b' = :b_value", + byFields( + "that", listOf(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")), + FieldMatch.ANY + ) + ) + } + + @Test + @DisplayName("orderBy generates for no fields") + fun orderByNone() { + assertEquals("", orderBy(listOf(), Dialect.POSTGRESQL), "ORDER BY should have been blank (PostgreSQL)") + assertEquals("", orderBy(listOf(), Dialect.SQLITE), "ORDER BY should have been blank (SQLite)") + } + + @Test + @DisplayName("orderBy generates single, no direction | PostgreSQL") + fun orderBySinglePostgres() = + assertEquals( + " ORDER BY data->>'TestField'", + orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly" + ) + + @Test + @DisplayName("orderBy generates single, no direction | SQLite") + fun orderBySingleSQLite() = + assertEquals( + " ORDER BY data->>'TestField'", orderBy(listOf(Field.named("TestField")), Dialect.SQLITE), + "ORDER BY not constructed correctly" + ) + + @Test + @DisplayName("orderBy generates multiple with direction | PostgreSQL") + fun orderByMultiplePostgres() = + assertEquals( + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + orderBy( + listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")), + Dialect.POSTGRESQL + ), + "ORDER BY not constructed correctly" + ) + + @Test + @DisplayName("orderBy generates multiple with direction | SQLite") + fun orderByMultipleSQLite() = + assertEquals( + " ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + orderBy( + listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")), + Dialect.SQLITE + ), + "ORDER BY not constructed correctly" + ) + + @Test + @DisplayName("orderBy generates numeric ordering | PostgreSQL") + fun orderByNumericPostgres() = + assertEquals( + " ORDER BY (data->>'Test')::numeric", + orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly" + ) + + @Test + @DisplayName("orderBy generates numeric ordering | SQLite") + fun orderByNumericSQLite() = + assertEquals( + " ORDER BY data->>'Test'", orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE), + "ORDER BY not constructed correctly" + ) + + @Test + @DisplayName("orderBy generates case-insensitive ordering | PostgreSQL") + fun orderByCIPostgres() = + assertEquals( + " ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", + orderBy(listOf(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL), + "ORDER BY not constructed correctly" + ) + + @Test + @DisplayName("orderBy generates case-insensitive ordering | SQLite") + fun orderByCISQLite() = + assertEquals( + " ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", + orderBy(listOf(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE), + "ORDER BY not constructed correctly" + ) +} diff --git a/src/core/src/test/kotlin/RemoveFieldsQueryTest.kt b/src/core/src/test/kotlin/RemoveFieldsQueryTest.kt new file mode 100644 index 0000000..fde9db1 --- /dev/null +++ b/src/core/src/test/kotlin/RemoveFieldsQueryTest.kt @@ -0,0 +1,118 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.RemoveFieldsQuery +import kotlin.test.assertEquals + +/** + * Unit tests for the `RemoveFields` object + */ +@DisplayName("Core | Kotlin | Query | RemoveFieldsQuery") +class RemoveFieldsQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + fun byIdPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data->>'id' = :id", + RemoveFieldsQuery.byId(TEST_TABLE, listOf(Parameter(":name", ParameterType.STRING, "{a,z}"))), + "Remove Fields query not constructed correctly" + ) + } + + @Test + @DisplayName("byId generates correctly | SQLite") + fun byIdSQLite() { + ForceDialect.sqlite() + assertEquals( + "UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id", + RemoveFieldsQuery.byId( + TEST_TABLE, + listOf( + Parameter(":name0", ParameterType.STRING, "a"), + Parameter(":name1", ParameterType.STRING, "z") + ) + ), + "Remove Field query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + fun byFieldsPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data->>'f' > :g", + RemoveFieldsQuery.byFields( + TEST_TABLE, + listOf(Parameter(":name", ParameterType.STRING, "{b,c}")), + listOf(Field.greater("f", "", ":g")) + ), + "Remove Field query not constructed correctly" + ) + } + + @Test + @DisplayName("byFields generates correctly | SQLite") + fun byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals( + "UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g", + RemoveFieldsQuery.byFields( + TEST_TABLE, + listOf(Parameter(":name0", ParameterType.STRING, "b"), Parameter(":name1", ParameterType.STRING, "c")), + listOf(Field.greater("f", "", ":g")) + ), + "Remove Field query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + fun byContainsPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data @> :criteria", + RemoveFieldsQuery.byContains(TEST_TABLE, listOf(Parameter(":name", ParameterType.STRING, "{m,n}"))), + "Remove Field query not constructed correctly" + ) + } + + @Test + @DisplayName("byContains fails | SQLite") + fun byContainsSQLite() { + ForceDialect.sqlite() + assertThrows { RemoveFieldsQuery.byContains(TEST_TABLE, listOf()) } + } + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + fun byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)", + RemoveFieldsQuery.byJsonPath(TEST_TABLE, listOf(Parameter(":name", ParameterType.STRING, "{o,p}"))), + "Remove Field query not constructed correctly" + ) + } + + @Test + @DisplayName("byJsonPath fails | SQLite") + fun byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows { RemoveFieldsQuery.byJsonPath(TEST_TABLE, listOf()) } + } +} diff --git a/src/core/src/test/kotlin/Types.kt b/src/core/src/test/kotlin/Types.kt new file mode 100644 index 0000000..e074eaf --- /dev/null +++ b/src/core/src/test/kotlin/Types.kt @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.core.tests + +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.Document + +/** The test table name to use for unit/integration tests */ +const val TEST_TABLE = "test_table" + +data class NumIdDocument(val key: Int, val text: String) { + constructor() : this(0, "") +} + +data class SubDocument(val foo: String, val bar: String) { + constructor() : this("", "") +} + +data class ArrayDocument(val id: String, val values: List) { + + constructor() : this("", listOf()) + + companion object { + /** A set of documents used for integration tests */ + val testDocuments = listOf( + ArrayDocument("first", listOf("a", "b", "c")), + ArrayDocument("second", listOf("c", "d", "e")), + ArrayDocument("third", listOf("x", "y", "z")) + ) + } +} + +data class JsonDocument(val id: String, val value: String = "", val numValue: Int = 0, val sub: SubDocument? = null) { + + constructor() : this("") + + companion object { + /** Documents to use for testing */ + private val testDocuments = listOf( + JsonDocument("one", "FIRST!", 0, null), + JsonDocument("two", "another", 10, SubDocument("green", "blue")), + JsonDocument("three", "", 4, null), + JsonDocument("four", "purple", 17, SubDocument("green", "red")), + JsonDocument("five", "purple", 18, null) + ) + + fun load(db: ThrowawayDatabase, tableName: String = TEST_TABLE) = + testDocuments.forEach { Document.insert(tableName, it, db.conn) } + + /** Document ID `one` as a JSON string */ + val one = """{"id":"one","value":"FIRST!","numValue":0,"sub":null}""" + + /** Document ID `two` as a JSON string */ + val two = """{"id":"two","value":"another","numValue":10,"sub":{"foo":"green","bar":"blue"}}""" + + /** Document ID `three` as a JSON string */ + val three = """{"id":"three","value":"","numValue":4,"sub":null}""" + + /** Document ID `four` as a JSON string */ + val four = """{"id":"four","value":"purple","numValue":17,"sub":{"foo":"green","bar":"red"}}""" + + /** Document ID `five` as a JSON string */ + val five = """{"id":"five","value":"purple","numValue":18,"sub":null}""" + } +} diff --git a/src/core/src/test/kotlin/WhereTest.kt b/src/core/src/test/kotlin/WhereTest.kt new file mode 100644 index 0000000..58cc59d --- /dev/null +++ b/src/core/src/test/kotlin/WhereTest.kt @@ -0,0 +1,177 @@ +package solutions.bitbadger.documents.core.tests + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.Where +import kotlin.test.assertEquals + +/** + * Unit tests for the `Where` object + */ +@DisplayName("Core | Kotlin | Query | Where") +class WhereTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName("byFields is blank when given no fields") + fun byFieldsBlankIfEmpty() = + assertEquals("", Where.byFields(listOf())) + + @Test + @DisplayName("byFields generates one numeric field | PostgreSQL") + fun byFieldsOneFieldPostgres() { + ForceDialect.postgres() + assertEquals("(data->>'it')::numeric = :that", Where.byFields(listOf(Field.equal("it", 9, ":that")))) + } + + @Test + @DisplayName("byFields generates one alphanumeric field | PostgreSQL") + fun byFieldsOneAlphaFieldPostgres() { + ForceDialect.postgres() + assertEquals("data->>'it' = :that", Where.byFields(listOf(Field.equal("it", "", ":that")))) + } + + @Test + @DisplayName("byFields generates one field | SQLite") + fun byFieldsOneFieldSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'it' = :that", Where.byFields(listOf(Field.equal("it", "", ":that")))) + } + + @Test + @DisplayName("byFields generates multiple fields w/ default match | PostgreSQL") + fun byFieldsMultipleDefaultPostgres() { + ForceDialect.postgres() + assertEquals( + "data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three", + Where.byFields( + listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")) + ) + ) + } + + @Test + @DisplayName("byFields generates multiple fields w/ default match | SQLite") + fun byFieldsMultipleDefaultSQLite() { + ForceDialect.sqlite() + assertEquals( + "data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three", + Where.byFields( + listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")) + ) + ) + } + + @Test + @DisplayName("byFields generates multiple fields w/ ANY match | PostgreSQL") + fun byFieldsMultipleAnyPostgres() { + ForceDialect.postgres() + assertEquals( + "data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three", + Where.byFields( + listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")), + FieldMatch.ANY + ) + ) + } + + @Test + @DisplayName("byFields generates multiple fields w/ ANY match | SQLite") + fun byFieldsMultipleAnySQLite() { + ForceDialect.sqlite() + assertEquals( + "data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three", + Where.byFields( + listOf(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")), + FieldMatch.ANY + ) + ) + } + + @Test + @DisplayName("byId generates defaults for alphanumeric key | PostgreSQL") + fun byIdDefaultAlphaPostgres() { + ForceDialect.postgres() + assertEquals("data->>'id' = :id", Where.byId(docId = "")) + } + + @Test + @DisplayName("byId generates defaults for numeric key | PostgreSQL") + fun byIdDefaultNumericPostgres() { + ForceDialect.postgres() + assertEquals("(data->>'id')::numeric = :id", Where.byId(docId = 5)) + } + + @Test + @DisplayName("byId generates defaults | SQLite") + fun byIdDefaultSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'id' = :id", Where.byId(docId = "")) + } + + @Test + @DisplayName("byId generates named ID | PostgreSQL") + fun byIdDefaultNamedPostgres() { + ForceDialect.postgres() + assertEquals("data->>'id' = :key", Where.byId(":key")) + } + + @Test + @DisplayName("byId generates named ID | SQLite") + fun byIdDefaultNamedSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'id' = :key", Where.byId(":key")) + } + + @Test + @DisplayName("jsonContains generates defaults | PostgreSQL") + fun jsonContainsDefaultPostgres() { + ForceDialect.postgres() + assertEquals("data @> :criteria", Where.jsonContains()) + } + + @Test + @DisplayName("jsonContains generates named parameter | PostgreSQL") + fun jsonContainsNamedPostgres() { + ForceDialect.postgres() + assertEquals("data @> :it", Where.jsonContains(":it")) + } + + @Test + @DisplayName("jsonContains fails | SQLite") + fun jsonContainsFailsSQLite() { + ForceDialect.sqlite() + assertThrows { Where.jsonContains() } + } + + @Test + @DisplayName("jsonPathMatches generates defaults | PostgreSQL") + fun jsonPathMatchDefaultPostgres() { + ForceDialect.postgres() + assertEquals("jsonb_path_exists(data, :path::jsonpath)", Where.jsonPathMatches()) + } + + @Test + @DisplayName("jsonPathMatches generates named parameter | PostgreSQL") + fun jsonPathMatchNamedPostgres() { + ForceDialect.postgres() + assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Where.jsonPathMatches(":jp")) + } + + @Test + @DisplayName("jsonPathMatches fails | SQLite") + fun jsonPathFailsSQLite() { + ForceDialect.sqlite() + assertThrows { Where.jsonPathMatches() } + } +} diff --git a/src/core/src/test/kotlin/integration/CountFunctions.kt b/src/core/src/test/kotlin/integration/CountFunctions.kt new file mode 100644 index 0000000..48155bf --- /dev/null +++ b/src/core/src/test/kotlin/integration/CountFunctions.kt @@ -0,0 +1,72 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.core.tests.JsonDocument +import solutions.bitbadger.documents.core.tests.TEST_TABLE +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.assertEquals + +/** + * Integration tests for the `Count` object + */ +object CountFunctions { + + fun all(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should have been 5 documents in the table") + } + + fun byFieldsNumeric(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 3L, + db.conn.countByFields(TEST_TABLE, listOf(Field.between("numValue", 10, 20))), + "There should have been 3 matching documents" + ) + } + + fun byFieldsAlpha(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 1L, + db.conn.countByFields(TEST_TABLE, listOf(Field.between("value", "aardvark", "apple"))), + "There should have been 1 matching document" + ) + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 2L, + db.conn.countByContains(TEST_TABLE, mapOf("value" to "purple")), + "There should have been 2 matching documents" + ) + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0L, + db.conn.countByContains(TEST_TABLE, mapOf("value" to "magenta")), + "There should have been no matching documents" + ) + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 2L, + db.conn.countByJsonPath(TEST_TABLE, "$.numValue ? (@ < 5)"), + "There should have been 2 matching documents" + ) + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0L, + db.conn.countByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no matching documents" + ) + } +} diff --git a/src/core/src/test/kotlin/integration/CustomFunctions.kt b/src/core/src/test/kotlin/integration/CustomFunctions.kt new file mode 100644 index 0000000..6ce54d4 --- /dev/null +++ b/src/core/src/test/kotlin/integration/CustomFunctions.kt @@ -0,0 +1,177 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* +import solutions.bitbadger.documents.java.Results +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.query.orderBy +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.test.* + +/** + * Integration tests for the `Custom` object + */ +object CustomFunctions { + + fun listEmpty(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.deleteByFields(TEST_TABLE, listOf(Field.exists(Configuration.idField))) + val result = + db.conn.customList(FindQuery.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData) + assertEquals(0, result.size, "There should have been no results") + } + + fun listAll(db: ThrowawayDatabase) { + JsonDocument.load(db) + val result = + db.conn.customList(FindQuery.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData) + assertEquals(5, result.size, "There should have been 5 results") + } + + fun jsonArrayEmpty(db: ThrowawayDatabase) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), "The test table should be empty") + assertEquals( + "[]", + db.conn.customJsonArray(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), + "An empty list was not represented correctly" + ) + } + + fun jsonArraySingle(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3"))) + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), + "A single document list was not represented correctly" + ) + } + + fun jsonArrayMany(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"first","values":["a","b","c"]},""" + + """{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]"""), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE) + orderBy(listOf(Field.named("id"))), listOf(), + Results::jsonFromData), + "A multiple document list was not represented correctly" + ) + } + + fun writeJsonArrayEmpty(db: ThrowawayDatabase) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), "The test table should be empty") + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), listOf(), writer, Results::jsonFromData) + assertEquals("[]", output.toString(), "An empty list was not represented correctly") + } + + fun writeJsonArraySingle(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3"))) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), listOf(), writer, Results::jsonFromData) + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), + output.toString(), + "A single document list was not represented correctly" + ) + } + + fun writeJsonArrayMany(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE) + orderBy(listOf(Field.named("id"))), listOf(), writer, + Results::jsonFromData) + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"first","values":["a","b","c"]},""" + + """{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]"""), + output.toString(), + "A multiple document list was not represented correctly" + ) + } + + fun singleNone(db: ThrowawayDatabase) = + assertFalse( + db.conn.customSingle( + FindQuery.all(TEST_TABLE), + listOf(), + JsonDocument::class.java, + Results::fromData + ).isPresent, + "There should not have been a document returned" + ) + + fun singleOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue( + db.conn.customSingle( + FindQuery.all(TEST_TABLE), + listOf(), + JsonDocument::class.java, + Results::fromData + ).isPresent, + "There should not have been a document returned" + ) + } + + fun jsonSingleNone(db: ThrowawayDatabase) = + assertEquals("{}", db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), + "An empty document was not represented correctly") + + fun jsonSingleOne(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, ArrayDocument("me", listOf("myself", "i"))) + assertEquals( + JsonFunctions.maybeJsonB("{\"id\":\"me\",\"values\":[\"myself\",\"i\"]}"), + db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), + "A single document was not represented correctly" + ) + } + + fun nonQueryChanges(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), + "There should have been 5 documents in the table" + ) + db.conn.customNonQuery("DELETE FROM $TEST_TABLE") + assertEquals( + 0L, db.conn.customScalar(CountQuery.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), + "There should have been no documents in the table" + ) + } + + fun nonQueryNoChanges(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), + "There should have been 5 documents in the table" + ) + db.conn.customNonQuery( + DeleteQuery.byId(TEST_TABLE, "eighty-two"), + listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) + ) + assertEquals( + 5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), + "There should still have been 5 documents in the table" + ) + } + + fun scalar(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 3L, + db.conn.customScalar( + "SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", + listOf(), + Long::class.java, + Results::toCount + ), + "The number 3 should have been returned" + ) + } +} diff --git a/src/core/src/test/kotlin/integration/DefinitionFunctions.kt b/src/core/src/test/kotlin/integration/DefinitionFunctions.kt new file mode 100644 index 0000000..dde6574 --- /dev/null +++ b/src/core/src/test/kotlin/integration/DefinitionFunctions.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.core.tests.TEST_TABLE +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Definition` object / `ensure*` connection extension functions + */ +object DefinitionFunctions { + + fun ensureTable(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("ensured"), "The 'ensured' table should not exist") + assertFalse(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should not exist") + db.conn.ensureTable("ensured") + assertTrue(db.dbObjectExists("ensured"), "The 'ensured' table should exist") + assertTrue(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should now exist") + } + + fun ensureFieldIndex(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should not exist") + db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category")) + assertTrue(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should now exist") + } + + fun ensureDocumentIndexFull(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist") + db.conn.ensureTable("doc_table") + assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist") + assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist") + db.conn.ensureDocumentIndex("doc_table", DocumentIndex.FULL) + assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist") + } + + fun ensureDocumentIndexOptimized(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist") + db.conn.ensureTable("doc_table") + assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist") + assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist") + db.conn.ensureDocumentIndex("doc_table", DocumentIndex.OPTIMIZED) + assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist") + } +} diff --git a/src/core/src/test/kotlin/integration/DeleteFunctions.kt b/src/core/src/test/kotlin/integration/DeleteFunctions.kt new file mode 100644 index 0000000..919947a --- /dev/null +++ b/src/core/src/test/kotlin/integration/DeleteFunctions.kt @@ -0,0 +1,68 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.assertEquals + +/** + * Integration tests for the `Delete` object + */ +object DeleteFunctions { + + fun byIdMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteById(TEST_TABLE, "four") + assertEquals(4, db.conn.countAll(TEST_TABLE), "There should now be 4 documents in the table") + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteById(TEST_TABLE, "negative four") + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByFields(TEST_TABLE, listOf(Field.notEqual("value", "purple"))) + assertEquals(2, db.conn.countAll(TEST_TABLE), "There should now be 2 documents in the table") + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByFields(TEST_TABLE, listOf(Field.equal("value", "crimson"))) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByContains(TEST_TABLE, mapOf("value" to "purple")) + assertEquals(3, db.conn.countAll(TEST_TABLE), "There should now be 3 documents in the table") + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByContains(TEST_TABLE, mapOf("target" to "acquired")) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByJsonPath(TEST_TABLE, "$.value ? (@ == \"purple\")") + assertEquals(3, db.conn.countAll(TEST_TABLE), "There should now be 3 documents in the table") + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)") + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + } +} diff --git a/src/core/src/test/kotlin/integration/DocumentFunctions.kt b/src/core/src/test/kotlin/integration/DocumentFunctions.kt new file mode 100644 index 0000000..dee2043 --- /dev/null +++ b/src/core/src/test/kotlin/integration/DocumentFunctions.kt @@ -0,0 +1,132 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.* + +/** + * Integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +object DocumentFunctions { + + fun insertDefault(db: ThrowawayDatabase) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) + db.conn.insert(TEST_TABLE, doc) + val after = db.conn.findAll(TEST_TABLE, JsonDocument::class.java) + assertEquals(1, after.size, "There should be one document in the table") + assertEquals(doc, after[0], "The document should be what was inserted") + } + + fun insertDupe(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) + assertThrows("Inserting a document with a duplicate key should have thrown an exception") { + db.conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) + } + } + + fun insertNumAutoId(db: ThrowawayDatabase) { + try { + Configuration.autoIdStrategy = AutoId.NUMBER + Configuration.idField = "key" + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, NumIdDocument(0, "one")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "two")) + db.conn.insert(TEST_TABLE, NumIdDocument(77, "three")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "four")) + + val after = db.conn.findAll(TEST_TABLE, NumIdDocument::class.java, listOf(Field.named("key"))) + assertEquals(4, after.size, "There should have been 4 documents returned") + assertEquals( + "1|2|77|78", after.joinToString("|") { it.key.toString() }, + "The IDs were not generated correctly" + ) + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idField = "id" + } + } + + fun insertUUIDAutoId(db: ThrowawayDatabase) { + try { + Configuration.autoIdStrategy = AutoId.UUID + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, JsonDocument("")) + + val after = db.conn.findAll(TEST_TABLE, JsonDocument::class.java) + assertEquals(1, after.size, "There should have been 1 document returned") + assertEquals(32, after[0].id.length, "The ID was not generated correctly") + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + } + } + + fun insertStringAutoId(db: ThrowawayDatabase) { + try { + Configuration.autoIdStrategy = AutoId.RANDOM_STRING + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, JsonDocument("")) + + Configuration.idStringLength = 21 + db.conn.insert(TEST_TABLE, JsonDocument("")) + + val after = db.conn.findAll(TEST_TABLE, JsonDocument::class.java) + assertEquals(2, after.size, "There should have been 2 documents returned") + assertEquals(16, after[0].id.length, "The first document's ID was not generated correctly") + assertEquals(21, after[1].id.length, "The second document's ID was not generated correctly") + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idStringLength = 16 + } + } + + fun saveMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.save(TEST_TABLE, JsonDocument("two", numValue = 44)) + val tryDoc = db.conn.findById(TEST_TABLE, "two", JsonDocument::class.java) + assertTrue(tryDoc.isPresent, "There should have been a document returned") + val doc = tryDoc.get() + assertEquals("two", doc.id, "An incorrect document was returned") + assertEquals("", doc.value, "The \"value\" field was not updated") + assertEquals(44, doc.numValue, "The \"numValue\" field was not updated") + assertNull(doc.sub, "The \"sub\" field was not updated") + } + + fun saveNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.save(TEST_TABLE, JsonDocument("test", sub = SubDocument("a", "b"))) + assertTrue( + db.conn.findById(TEST_TABLE, "test", JsonDocument::class.java).isPresent, + "The test document should have been saved" + ) + } + + fun updateMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.update(TEST_TABLE, "one", JsonDocument("one", "howdy", 8, SubDocument("y", "z"))) + val tryDoc = db.conn.findById(TEST_TABLE, "one", JsonDocument::class.java) + assertTrue(tryDoc.isPresent, "There should have been a document returned") + val doc = tryDoc.get() + assertEquals("one", doc.id, "An incorrect document was returned") + assertEquals("howdy", doc.value, "The \"value\" field was not updated") + assertEquals(8, doc.numValue, "The \"numValue\" field was not updated") + assertNotNull(doc.sub, "The sub-document should not be null") + assertEquals("y", doc.sub.foo, "The sub-document \"foo\" field was not updated") + assertEquals("z", doc.sub.bar, "The sub-document \"bar\" field was not updated") + } + + fun updateNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse { db.conn.existsById(TEST_TABLE, "two-hundred") } + db.conn.update(TEST_TABLE, "two-hundred", JsonDocument("two-hundred", numValue = 200)) + assertFalse { db.conn.existsById(TEST_TABLE, "two-hundred") } + } +} diff --git a/src/core/src/test/kotlin/integration/ExistsFunctions.kt b/src/core/src/test/kotlin/integration/ExistsFunctions.kt new file mode 100644 index 0000000..194aefb --- /dev/null +++ b/src/core/src/test/kotlin/integration/ExistsFunctions.kt @@ -0,0 +1,65 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Exists` object + */ +object ExistsFunctions { + + fun byIdMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("The document with ID \"three\" should exist") { db.conn.existsById(TEST_TABLE, "three") } + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("The document with ID \"seven\" should not exist") { db.conn.existsById(TEST_TABLE, "seven") } + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByFields(TEST_TABLE, listOf(Field.equal("numValue", 10))) + } + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("No matching documents should have been found") { + db.conn.existsByFields(TEST_TABLE, listOf(Field.equal("nothing", "none"))) + } + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByContains(TEST_TABLE, mapOf("value" to "purple")) + } + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("Matching documents should not have been found") { + db.conn.existsByContains(TEST_TABLE, mapOf("value" to "violet")) + } + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + } + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("Matching documents should not have been found") { + db.conn.existsByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10.1)") + } + } +} diff --git a/src/core/src/test/kotlin/integration/FindFunctions.kt b/src/core/src/test/kotlin/integration/FindFunctions.kt new file mode 100644 index 0000000..23e14a1 --- /dev/null +++ b/src/core/src/test/kotlin/integration/FindFunctions.kt @@ -0,0 +1,334 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.* + +/** + * Integration tests for the `Find` object + */ +object FindFunctions { + + fun allDefault(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 5, + db.conn.findAll(TEST_TABLE, JsonDocument::class.java).size, + "There should have been 5 documents returned" + ) + } + + fun allAscending(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findAll(TEST_TABLE, JsonDocument::class.java, listOf(Field.named("id"))) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals( + "five|four|one|three|two", + docs.joinToString("|") { it.id }, + "The documents were not ordered correctly" + ) + } + + fun allDescending(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findAll(TEST_TABLE, JsonDocument::class.java, listOf(Field.named("id DESC"))) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals( + "two|three|one|four|five", + docs.joinToString("|") { it.id }, + "The documents were not ordered correctly" + ) + } + + fun allNumOrder(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findAll( + TEST_TABLE, + JsonDocument::class.java, + listOf(Field.named("sub.foo NULLS LAST"), Field.named("n:numValue")) + ) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals( + "two|four|one|three|five", + docs.joinToString("|") { it.id }, + "The documents were not ordered correctly" + ) + } + + fun allEmpty(db: ThrowawayDatabase) = + assertEquals( + 0, + db.conn.findAll(TEST_TABLE, JsonDocument::class.java).size, + "There should have been no documents returned" + ) + + fun byIdString(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findById(TEST_TABLE, "two", JsonDocument::class.java) + assertTrue(doc.isPresent, "The document should have been returned") + assertEquals("two", doc.get().id, "An incorrect document was returned") + } + + fun byIdNumber(db: ThrowawayDatabase) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + val doc = db.conn.findById(TEST_TABLE, 18, NumIdDocument::class.java) + assertTrue(doc.isPresent, "The document should have been returned") + } finally { + Configuration.idField = "id" + } + } + + fun byIdNotFound(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse( + db.conn.findById(TEST_TABLE, "x", JsonDocument::class.java).isPresent, + "There should have been no document returned" + ) + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByFields( + TEST_TABLE, + listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), + JsonDocument::class.java, + FieldMatch.ALL + ) + assertEquals(1, docs.size, "There should have been a document returned") + assertEquals("four", docs[0].id, "The incorrect document was returned") + } + + fun byFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByFields( + TEST_TABLE, + listOf(Field.equal("value", "purple")), + JsonDocument::class.java, + orderBy = listOf(Field.named("id")) + ) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("five|four", docs.joinToString("|") { it.id }, "The documents were not ordered correctly") + } + + fun byFieldsMatchNumIn(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByFields( + TEST_TABLE, + listOf(Field.any("numValue", listOf(2, 4, 6, 8))), + JsonDocument::class.java + ) + assertEquals(1, docs.size, "There should have been a document returned") + assertEquals("three", docs[0].id, "The incorrect document was returned") + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0, + db.conn.findByFields(TEST_TABLE, listOf(Field.greater("numValue", 100)), JsonDocument::class.java).size, + "There should have been no documents returned" + ) + } + + fun byFieldsMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val docs = + db.conn.findByFields( + TEST_TABLE, + listOf(Field.inArray("values", TEST_TABLE, listOf("c"))), + ArrayDocument::class.java + ) + assertEquals(2, docs.size, "There should have been two documents returned") + assertTrue(listOf("first", "second").contains(docs[0].id), "An incorrect document was returned (${docs[0].id})") + assertTrue(listOf("first", "second").contains(docs[1].id), "An incorrect document was returned (${docs[1].id})") + } + + fun byFieldsNoMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals( + 0, + db.conn.findByFields( + TEST_TABLE, + listOf(Field.inArray("values", TEST_TABLE, listOf("j"))), + ArrayDocument::class.java + ).size, + "There should have been no documents returned" + ) + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByContains(TEST_TABLE, mapOf("value" to "purple"), JsonDocument::class.java) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertTrue(listOf("four", "five").contains(docs[0].id), "An incorrect document was returned (${docs[0].id})") + assertTrue(listOf("four", "five").contains(docs[1].id), "An incorrect document was returned (${docs[1].id})") + } + + fun byContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByContains( + TEST_TABLE, + mapOf("sub" to mapOf("foo" to "green")), + JsonDocument::class.java, + listOf(Field.named("value")) + ) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("two|four", docs.joinToString("|") { it.id }, "The documents were not ordered correctly") + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0, + db.conn.findByContains(TEST_TABLE, mapOf("value" to "indigo"), JsonDocument::class.java).size, + "There should have been no documents returned" + ) + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", JsonDocument::class.java) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertTrue(listOf("four", "five").contains(docs[0].id), "An incorrect document was returned (${docs[0].id})") + assertTrue(listOf("four", "five").contains(docs[1].id), "An incorrect document was returned (${docs[1].id})") + } + + fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByJsonPath( + TEST_TABLE, + "$.numValue ? (@ > 10)", + JsonDocument::class.java, + listOf(Field.named("id")) + ) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("five|four", docs.joinToString("|") { it.id }, "The documents were not ordered correctly") + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0, + db.conn.findByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument::class.java).size, + "There should have been no documents returned" + ) + } + + fun firstByFieldsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = + db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another")), JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("two", doc.get().id, "The incorrect document was returned") + } + + fun firstByFieldsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = + db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green")), JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertTrue(listOf("two", "four").contains(doc.get().id), "An incorrect document was returned (${doc.get().id})") + } + + fun firstByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), JsonDocument::class.java, orderBy = listOf( + Field.named("n:numValue DESC") + ) + ) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("four", doc.get().id, "An incorrect document was returned") + } + + fun firstByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse( + db.conn.findFirstByFields( + TEST_TABLE, + listOf(Field.equal("value", "absent")), + JsonDocument::class.java + ).isPresent, + "There should have been no document returned" + ) + } + + fun firstByContainsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!"), JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("one", doc.get().id, "An incorrect document was returned") + } + + fun firstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "purple"), JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertTrue( + listOf("four", "five").contains(doc.get().id), + "An incorrect document was returned (${doc.get().id})" + ) + } + + fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByContains( + TEST_TABLE, + mapOf("value" to "purple"), + JsonDocument::class.java, + listOf(Field.named("sub.bar NULLS FIRST")) + ) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("five", doc.get().id, "An incorrect document was returned") + } + + fun firstByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse( + db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "indigo"), JsonDocument::class.java).isPresent, + "There should have been no document returned" + ) + } + + fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)", JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("two", doc.get().id, "An incorrect document was returned") + } + + fun firstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertTrue( + listOf("four", "five").contains(doc.get().id), + "An incorrect document was returned (${doc.get().id})" + ) + } + + fun firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath( + TEST_TABLE, + "$.numValue ? (@ > 10)", + JsonDocument::class.java, + listOf(Field.named("id DESC")) + ) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("four", doc.get().id, "An incorrect document was returned") + } + + fun firstByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse( + db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument::class.java).isPresent, + "There should have been no document returned" + ) + } +} diff --git a/src/core/src/test/kotlin/integration/JacksonDocumentSerializer.kt b/src/core/src/test/kotlin/integration/JacksonDocumentSerializer.kt new file mode 100644 index 0000000..c3d15db --- /dev/null +++ b/src/core/src/test/kotlin/integration/JacksonDocumentSerializer.kt @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.DocumentSerializer +import com.fasterxml.jackson.databind.ObjectMapper + +/** + * A JSON serializer using Jackson's default options + */ +class JacksonDocumentSerializer : DocumentSerializer { + + private val mapper = ObjectMapper() + + override fun serialize(document: TDoc): String = + mapper.writeValueAsString(document) + + override fun deserialize(json: String, clazz: Class): TDoc = + mapper.readValue(json, clazz) +} diff --git a/src/core/src/test/kotlin/integration/JsonFunctions.kt b/src/core/src/test/kotlin/integration/JsonFunctions.kt new file mode 100644 index 0000000..2ef24f1 --- /dev/null +++ b/src/core/src/test/kotlin/integration/JsonFunctions.kt @@ -0,0 +1,715 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.core.tests.ArrayDocument +import solutions.bitbadger.documents.core.tests.JsonDocument +import solutions.bitbadger.documents.core.tests.NumIdDocument +import solutions.bitbadger.documents.core.tests.TEST_TABLE +import solutions.bitbadger.documents.java.extensions.* +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Tests for the JSON-returning functions + * + * NOTE: PostgreSQL JSONB columns do not preserve the original JSON with which a document was stored. These tests are + * the most complex within the library, as they have split testing based on the backing data store. The PostgreSQL tests + * check IDs (and, in the case of ordered queries, which ones occur before which others) vs. the entire JSON string. + * Meanwhile, SQLite stores JSON as text, and will return exactly the JSON it was given when it was originally written. + * These tests can ensure the expected round-trip of the entire JSON string. + */ +object JsonFunctions { + + /** + * PostgreSQL, when returning JSONB as a string, has spaces after commas and colons delineating fields and values. + * This function will do a crude string replacement to match the target string based on the dialect being tested. + * + * @param json The JSON which should be returned + * @return The actual expected JSON based on the database being tested + */ + fun maybeJsonB(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> json + Dialect.POSTGRESQL -> json.replace("\":", "\": ").replace(",\"", ", \"") + } + + /** + * Create a snippet of JSON to find a document ID + * + * @param id The ID of the document + * @return A connection-aware ID to check for presence and positioning + */ + private fun docId(id: String) = + maybeJsonB("{\"id\":\"$id\"") + + private fun checkAllDefault(json: String) { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + when (Configuration.dialect()) { + Dialect.SQLITE -> { + assertTrue(json.contains(JsonDocument.one), "Document 'one' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.two), "Document 'two' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.three), "Document 'three' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found in JSON ($json)") + } + Dialect.POSTGRESQL -> { + assertTrue(json.contains(docId("one")), "Document 'one' not found in JSON ($json)") + assertTrue(json.contains(docId("two")), "Document 'two' not found in JSON ($json)") + assertTrue(json.contains(docId("three")), "Document 'three' not found in JSON ($json)") + assertTrue(json.contains(docId("four")), "Document 'four' not found in JSON ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found in JSON ($json)") + } + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + fun allDefault(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkAllDefault(db.conn.jsonAll(TEST_TABLE)) + } + + fun writeAllDefault(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonAll(TEST_TABLE, writer) + checkAllDefault(output.toString()) + } + + private fun checkAllEmpty(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun allEmpty(db: ThrowawayDatabase) = + checkAllEmpty(db.conn.jsonAll(TEST_TABLE)) + + fun writeAllEmpty(db: ThrowawayDatabase) { + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonAll(TEST_TABLE, writer) + checkAllEmpty(output.toString()) + } + + private fun checkByIdString(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "An incorrect document was returned ($json)") + } + + fun byIdString(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByIdString(db.conn.jsonById(TEST_TABLE, "two")) + } + + fun writeByIdString(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, "two") + checkByIdString(output.toString()) + } + + private fun checkByIdNumber(json: String) = + assertEquals( + maybeJsonB("""{"key":18,"text":"howdy"}"""), + json, + "The document should have been found by numeric ID" + ) + + fun byIdNumber(db: ThrowawayDatabase) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + checkByIdNumber(db.conn.jsonById(TEST_TABLE, 18)) + } finally { + Configuration.idField = "id" + } + } + + fun writeByIdNumber(db: ThrowawayDatabase) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, 18) + checkByIdNumber(output.toString()) + } finally { + Configuration.idField = "id" + } + } + + private fun checkByIdNotFound(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun byIdNotFound(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByIdNotFound(db.conn.jsonById(TEST_TABLE, "x")) + } + + fun writeByIdNotFound(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, "x") + checkByIdNotFound(output.toString()) + } + + private fun checkByFieldsMatch(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals("[${JsonDocument.four}]", json, "The incorrect document was returned") + Dialect.POSTGRESQL -> { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("four")),"The incorrect document was returned ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByFieldsMatch( + db.conn.jsonByFields( + TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL + ) + ) + } + + fun writeByFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields( + TEST_TABLE, + writer, + listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), + FieldMatch.ALL + ) + checkByFieldsMatch(output.toString()) + } + + private fun checkByFieldsMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.five},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + + Dialect.POSTGRESQL -> { + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, "Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, "Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByFieldsMatchOrdered( + db.conn.jsonByFields( + TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + ) + ) + } + + fun writeByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields( + TEST_TABLE, writer, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + ) + checkByFieldsMatchOrdered(output.toString()) + } + + private fun checkByFieldsMatchNumIn(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals("[${JsonDocument.three}]", json, "The incorrect document was returned") + Dialect.POSTGRESQL -> { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("three")), "The incorrect document was returned ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byFieldsMatchNumIn(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByFieldsMatchNumIn(db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8))))) + } + + fun writeByFieldsMatchNumIn(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + checkByFieldsMatchNumIn(output.toString()) + } + + private fun checkByFieldsNoMatch(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByFieldsNoMatch(db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100)))) + } + + fun writeByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, listOf(Field.greater("numValue", 100))) + checkByFieldsNoMatch(output.toString()) + } + + private fun checkByFieldsMatchInArray(json: String) { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("first")), "The 'first' document was not found ($json)") + assertTrue(json.contains(docId("second")), "The 'second' document was not found ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + fun byFieldsMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + checkByFieldsMatchInArray( + db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + ) + } + + fun writeByFieldsMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + checkByFieldsMatchInArray(output.toString()) + } + + private fun checkByFieldsNoMatchInArray(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun byFieldsNoMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + checkByFieldsNoMatchInArray( + db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j")))) + ) + } + + fun writeByFieldsNoMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, listOf(Field.inArray("values", TEST_TABLE, listOf("j")))) + checkByFieldsNoMatchInArray(output.toString()) + } + + private fun checkByContainsMatch(json: String) { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + when (Configuration.dialect()) { + Dialect.SQLITE -> { + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found ($json)") + } + Dialect.POSTGRESQL -> { + assertTrue(json.contains(docId("four")), "Document 'four' not found ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found ($json)") + } + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByContainsMatch(db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "purple"))) + } + + fun writeByContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains(TEST_TABLE, writer, mapOf("value" to "purple")) + checkByContainsMatch(output.toString()) + } + + private fun checkByContainsMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.two},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + Dialect.POSTGRESQL -> { + val twoIdx = json.indexOf(docId("two")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(twoIdx >= 0, "Document 'two' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(twoIdx < fourIdx, "Document 'two' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByContainsMatchOrdered( + db.conn.jsonByContains(TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value"))) + ) + } + + fun writeByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains( + TEST_TABLE, writer, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) + ) + checkByContainsMatchOrdered(output.toString()) + } + + private fun checkByContainsNoMatch(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByContainsNoMatch(db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "indigo"))) + } + + fun writeByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains(TEST_TABLE, writer, mapOf("value" to "indigo")) + checkByContainsNoMatch(output.toString()) + } + + private fun checkByJsonPathMatch(json: String) { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + when (Configuration.dialect()) { + Dialect.SQLITE -> { + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found ($json)") + } + Dialect.POSTGRESQL -> { + assertTrue(json.contains(docId("four")), "Document 'four' not found ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found ($json)") + } + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByJsonPathMatch(db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)")) + } + + fun writeByJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)") + checkByJsonPathMatch(output.toString()) + } + + private fun checkByJsonPathMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.five},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + + Dialect.POSTGRESQL -> { + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, "Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, "Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByJsonPathMatchOrdered( + db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + ) + } + + fun writeByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + checkByJsonPathMatchOrdered(output.toString()) + } + + private fun checkByJsonPathNoMatch(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByJsonPathNoMatch(db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)")) + } + + fun writeByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 100)") + checkByJsonPathNoMatch(output.toString()) + } + + private fun checkFirstByFieldsMatchOne(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "The incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "The incorrect document was returned ($json)") + } + + fun firstByFieldsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByFieldsMatchOne(db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another")))) + } + + fun writeFirstByFieldsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, listOf(Field.equal("value", "another"))) + checkFirstByFieldsMatchOne(output.toString()) + } + + private fun checkFirstByFieldsMatchMany(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.two) || json.contains(JsonDocument.four), + "Expected document 'two' or 'four' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("two")) || json.contains(docId("four")), + "Expected document 'two' or 'four' ($json)" + ) + } + + fun firstByFieldsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByFieldsMatchMany(db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green")))) + } + + fun writeFirstByFieldsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, listOf(Field.equal("sub.foo", "green"))) + checkFirstByFieldsMatchMany(output.toString()) + } + + private fun checkFirstByFieldsMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.four, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("four")), "An incorrect document was returned ($json)") + } + + fun firstByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByFieldsMatchOrdered( + db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) + ) + ) + } + + fun writeFirstByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields( + TEST_TABLE, + writer, + listOf(Field.equal("sub.foo", "green")), + orderBy = listOf(Field.named("n:numValue DESC")) + ) + checkFirstByFieldsMatchOrdered(output.toString()) + } + + private fun checkFirstByFieldsNoMatch(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun firstByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByFieldsNoMatch(db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent")))) + } + + fun writeFirstByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, listOf(Field.equal("value", "absent"))) + checkFirstByFieldsNoMatch(output.toString()) + } + + private fun checkFirstByContainsMatchOne(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.one, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("one")), "An incorrect document was returned ($json)") + } + + fun firstByContainsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsMatchOne(db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!"))) + } + + fun writeFirstByContainsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, mapOf("value" to "FIRST!")) + checkFirstByContainsMatchOne(output.toString()) + } + + private fun checkFirstByContainsMatchMany(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + "Expected document 'four' or 'five' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("four")) || json.contains(docId("five")), + "Expected document 'four' or 'five' ($json)" + ) + } + + fun firstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsMatchMany(db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "purple"))) + } + + fun writeFirstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, mapOf("value" to "purple")) + checkFirstByContainsMatchMany(output.toString()) + } + + private fun checkFirstByContainsMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.five, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("five")), "An incorrect document was returned ($json)") + } + + fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsMatchOrdered( + db.conn.jsonFirstByContains( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) + ) + } + + fun writeFirstByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains( + TEST_TABLE, writer, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) + checkFirstByContainsMatchOrdered(output.toString()) + } + + private fun checkFirstByContainsNoMatch(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun firstByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsNoMatch(db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "indigo"))) + } + + fun writeFirstByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, mapOf("value" to "indigo")) + checkFirstByContainsNoMatch(output.toString()) + } + + private fun checkFirstByJsonPathMatchOne(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "An incorrect document was returned ($json)") + } + + fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathMatchOne(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)")) + } + + fun writeFirstByJsonPathMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ == 10)") + checkFirstByJsonPathMatchOne(output.toString()) + } + + private fun checkFirstByJsonPathMatchMany(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + "Expected document 'four' or 'five' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("four")) || json.contains(docId("five")), + "Expected document 'four' or 'five' ($json)" + ) + } + + fun firstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathMatchMany(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)")) + } + + fun writeFirstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)") + checkFirstByJsonPathMatchMany(output.toString()) + } + + private fun checkFirstByJsonPathMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.four, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("four")), "An incorrect document was returned ($json)") + } + + fun firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathMatchOrdered( + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + ) + } + + fun writeFirstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + checkFirstByJsonPathMatchOrdered(output.toString()) + } + + private fun checkFirstByJsonPathNoMatch(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun firstByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathNoMatch(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)")) + } + + fun writeFirstByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 100)") + checkFirstByJsonPathNoMatch(output.toString()) + } +} diff --git a/src/core/src/test/kotlin/integration/PatchFunctions.kt b/src/core/src/test/kotlin/integration/PatchFunctions.kt new file mode 100644 index 0000000..6f98006 --- /dev/null +++ b/src/core/src/test/kotlin/integration/PatchFunctions.kt @@ -0,0 +1,88 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Patch` object + */ +object PatchFunctions { + + fun byIdMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.patchById(TEST_TABLE, "one", mapOf("numValue" to 44)) + val doc = db.conn.findById(TEST_TABLE, "one", JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("one", doc.get().id, "An incorrect document was returned") + assertEquals(44, doc.get().numValue, "The document was not patched") + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse(db.conn.existsById(TEST_TABLE, "forty-seven"), "Document with ID \"forty-seven\" should not exist") + db.conn.patchById(TEST_TABLE, "forty-seven", mapOf("foo" to "green")) // no exception = pass + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.patchByFields(TEST_TABLE, listOf(Field.equal("value", "purple")), mapOf("numValue" to 77)) + assertEquals( + 2, + db.conn.countByFields(TEST_TABLE, listOf(Field.equal("numValue", 77))), + "There should have been 2 documents with numeric value 77" + ) + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val fields = listOf(Field.equal("value", "burgundy")) + assertFalse( + db.conn.existsByFields(TEST_TABLE, fields), + "There should be no documents with value of \"burgundy\"" + ) + db.conn.patchByFields(TEST_TABLE, fields, mapOf("foo" to "green")) // no exception = pass + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val contains = mapOf("value" to "another") + db.conn.patchByContains(TEST_TABLE, contains, mapOf("numValue" to 12)) + val doc = db.conn.findFirstByContains(TEST_TABLE, contains, JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("two", doc.get().id, "The incorrect document was returned") + assertEquals(12, doc.get().numValue, "The document was not updated") + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val contains = mapOf("value" to "updated") + assertFalse(db.conn.existsByContains(TEST_TABLE, contains), "There should be no matching documents") + db.conn.patchByContains(TEST_TABLE, contains, mapOf("sub.foo" to "green")) // no exception = pass + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val path = "$.numValue ? (@ > 10)" + db.conn.patchByJsonPath(TEST_TABLE, path, mapOf("value" to "blue")) + val docs = db.conn.findByJsonPath(TEST_TABLE, path, JsonDocument::class.java) + assertEquals(2, docs.size, "There should have been two documents returned") + docs.forEach { + assertTrue(listOf("four", "five").contains(it.id), "An incorrect document was returned (${it.id})") + assertEquals("blue", it.value, "The value for ID ${it.id} was incorrect") + } + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val path = "$.numValue ? (@ > 100)" + assertFalse( + db.conn.existsByJsonPath(TEST_TABLE, path), + "There should be no documents with numeric values over 100" + ) + db.conn.patchByJsonPath(TEST_TABLE, path, mapOf("value" to "blue")) // no exception = pass + } +} diff --git a/src/core/src/test/kotlin/integration/PgDB.kt b/src/core/src/test/kotlin/integration/PgDB.kt new file mode 100644 index 0000000..1daed4d --- /dev/null +++ b/src/core/src/test/kotlin/integration/PgDB.kt @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.core.tests.TEST_TABLE +import solutions.bitbadger.documents.java.Results +import solutions.bitbadger.documents.java.extensions.* + +/** + * A wrapper for a throwaway PostgreSQL database + */ +class PgDB : ThrowawayDatabase() { + + init { + Configuration.connectionString = connString("postgres") + Configuration.dbConn().use { it.customNonQuery("CREATE DATABASE $dbName") } + Configuration.connectionString = connString(dbName) + } + + override val conn = Configuration.dbConn() + + init { + conn.ensureTable(TEST_TABLE) + } + + override fun close() { + conn.close() + Configuration.connectionString = connString("postgres") + Configuration.dbConn().use { it.customNonQuery("DROP DATABASE $dbName") } + Configuration.connectionString = null + } + + override fun dbObjectExists(name: String) = + conn.customScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = :name) AS it", + listOf(Parameter(":name", ParameterType.STRING, name)), Boolean::class.java, Results::toExists) + + companion object { + + /** + * Create a connection string for the given database + * + * @param database The database to which the library should connect + * @return The connection string for the database + */ + private fun connString(database: String) = + "jdbc:postgresql://localhost/$database?user=postgres&password=postgres" + } +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt new file mode 100644 index 0000000..6e2690a --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Count") +class PostgreSQLCountIT { + + @Test + @DisplayName("all counts all documents") + fun all() = + PgDB().use(CountFunctions::all) + + @Test + @DisplayName("byFields counts documents by a numeric value") + fun byFieldsNumeric() = + PgDB().use(CountFunctions::byFieldsNumeric) + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + fun byFieldsAlpha() = + PgDB().use(CountFunctions::byFieldsAlpha) + + @Test + @DisplayName("byContains counts documents when matches are found") + fun byContainsMatch() = + PgDB().use(CountFunctions::byContainsMatch) + + @Test + @DisplayName("byContains counts documents when no matches are found") + fun byContainsNoMatch() = + PgDB().use(CountFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath counts documents when matches are found") + fun byJsonPathMatch() = + PgDB().use(CountFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath counts documents when no matches are found") + fun byJsonPathNoMatch() = + PgDB().use(CountFunctions::byJsonPathNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt new file mode 100644 index 0000000..d96b076 --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -0,0 +1,87 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName + +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Custom") +class PostgreSQLCustomIT { + + @Test + @DisplayName("list succeeds with empty list") + fun listEmpty() = + PgDB().use(CustomFunctions::listEmpty) + + @Test + @DisplayName("list succeeds with a non-empty list") + fun listAll() = + PgDB().use(CustomFunctions::listAll) + + @Test + @DisplayName("jsonArray succeeds with empty array") + fun jsonArrayEmpty() = + PgDB().use(CustomFunctions::jsonArrayEmpty) + + @Test + @DisplayName("jsonArray succeeds with a single-item list") + fun jsonArraySingle() = + PgDB().use(CustomFunctions::jsonArraySingle) + + @Test + @DisplayName("jsonArray succeeds with a multi-item list") + fun jsonArrayMany() = + PgDB().use(CustomFunctions::jsonArrayMany) + + @Test + @DisplayName("writeJsonArray succeeds with empty array") + fun writeJsonArrayEmpty() = + PgDB().use(CustomFunctions::writeJsonArrayEmpty) + + @Test + @DisplayName("writeJsonArray succeeds with a single-item list") + fun writeJsonArraySingle() = + PgDB().use(CustomFunctions::writeJsonArraySingle) + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item list") + fun writeJsonArrayMany() = + PgDB().use(CustomFunctions::writeJsonArrayMany) + + @Test + @DisplayName("single succeeds when document not found") + fun singleNone() = + PgDB().use(CustomFunctions::singleNone) + + @Test + @DisplayName("single succeeds when a document is found") + fun singleOne() = + PgDB().use(CustomFunctions::singleOne) + + @Test + @DisplayName("jsonSingle succeeds when document not found") + fun jsonSingleNone() = + PgDB().use(CustomFunctions::jsonSingleNone) + + @Test + @DisplayName("jsonSingle succeeds when a document is found") + fun jsonSingleOne() = + PgDB().use(CustomFunctions::jsonSingleOne) + + @Test + @DisplayName("nonQuery makes changes") + fun nonQueryChanges() = + PgDB().use(CustomFunctions::nonQueryChanges) + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + fun nonQueryNoChanges() = + PgDB().use(CustomFunctions::nonQueryNoChanges) + + @Test + @DisplayName("scalar succeeds") + fun scalar() = + PgDB().use(CustomFunctions::scalar) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt new file mode 100644 index 0000000..e9f71ec --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt @@ -0,0 +1,31 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Definition") +class PostgreSQLDefinitionIT { + + @Test + @DisplayName("ensureTable creates table and index") + fun ensureTable() = + PgDB().use(DefinitionFunctions::ensureTable) + + @Test + @DisplayName("ensureFieldIndex creates an index") + fun ensureFieldIndex() = + PgDB().use(DefinitionFunctions::ensureFieldIndex) + + @Test + @DisplayName("ensureDocumentIndex creates a full index") + fun ensureDocumentIndexFull() = + PgDB().use(DefinitionFunctions::ensureDocumentIndexFull) + + @Test + @DisplayName("ensureDocumentIndex creates an optimized index") + fun ensureDocumentIndexOptimized() = + PgDB().use(DefinitionFunctions::ensureDocumentIndexOptimized) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLDeleteIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLDeleteIT.kt new file mode 100644 index 0000000..6e3e57c --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLDeleteIT.kt @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Delete") +class PostgreSQLDeleteIT { + + @Test + @DisplayName("byId deletes a matching ID") + fun byIdMatch() = + PgDB().use(DeleteFunctions::byIdMatch) + + @Test + @DisplayName("byId succeeds when no ID matches") + fun byIdNoMatch() = + PgDB().use(DeleteFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields deletes matching documents") + fun byFieldsMatch() = + PgDB().use(DeleteFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(DeleteFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains deletes matching documents") + fun byContainsMatch() = + PgDB().use(DeleteFunctions::byContainsMatch) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(DeleteFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath deletes matching documents") + fun byJsonPathMatch() = + PgDB().use(DeleteFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(DeleteFunctions::byJsonPathNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLDocumentIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLDocumentIT.kt new file mode 100644 index 0000000..9696573 --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLDocumentIT.kt @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Document") +class PostgreSQLDocumentIT { + + @Test + @DisplayName("insert works with default values") + fun insertDefault() = + PgDB().use(DocumentFunctions::insertDefault) + + @Test + @DisplayName("insert fails with duplicate key") + fun insertDupe() = + PgDB().use(DocumentFunctions::insertDupe) + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + fun insertNumAutoId() = + PgDB().use(DocumentFunctions::insertNumAutoId) + + @Test + @DisplayName("insert succeeds with UUID auto ID") + fun insertUUIDAutoId() = + PgDB().use(DocumentFunctions::insertUUIDAutoId) + + @Test + @DisplayName("insert succeeds with random string auto ID") + fun insertStringAutoId() = + PgDB().use(DocumentFunctions::insertStringAutoId) + + @Test + @DisplayName("save updates an existing document") + fun saveMatch() = + PgDB().use(DocumentFunctions::saveMatch) + + @Test + @DisplayName("save inserts a new document") + fun saveNoMatch() = + PgDB().use(DocumentFunctions::saveNoMatch) + + @Test + @DisplayName("update replaces an existing document") + fun updateMatch() = + PgDB().use(DocumentFunctions::updateMatch) + + @Test + @DisplayName("update succeeds when no document exists") + fun updateNoMatch() = + PgDB().use(DocumentFunctions::updateNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLExistsIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLExistsIT.kt new file mode 100644 index 0000000..f497f64 --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLExistsIT.kt @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Exists") +class PostgreSQLExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + fun byIdMatch() = + PgDB().use(ExistsFunctions::byIdMatch) + + @Test + @DisplayName("byId returns false when no document matches the ID") + fun byIdNoMatch() = + PgDB().use(ExistsFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields returns true when documents match") + fun byFieldsMatch() = + PgDB().use(ExistsFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields returns false when no documents match") + fun byFieldsNoMatch() = + PgDB().use(ExistsFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains returns true when documents match") + fun byContainsMatch() = + PgDB().use(ExistsFunctions::byContainsMatch) + + @Test + @DisplayName("byContains returns false when no documents match") + fun byContainsNoMatch() = + PgDB().use(ExistsFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath returns true when documents match") + fun byJsonPathMatch() = + PgDB().use(ExistsFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath returns false when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(ExistsFunctions::byJsonPathNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLFindIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLFindIT.kt new file mode 100644 index 0000000..0c84aa0 --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLFindIT.kt @@ -0,0 +1,171 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Find") +class PostgreSQLFindIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + PgDB().use(FindFunctions::allDefault) + + @Test + @DisplayName("all sorts data ascending") + fun allAscending() = + PgDB().use(FindFunctions::allAscending) + + @Test + @DisplayName("all sorts data descending") + fun allDescending() = + PgDB().use(FindFunctions::allDescending) + + @Test + @DisplayName("all sorts data numerically") + fun allNumOrder() = + PgDB().use(FindFunctions::allNumOrder) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + PgDB().use(FindFunctions::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + PgDB().use(FindFunctions::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + PgDB().use(FindFunctions::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + PgDB().use(FindFunctions::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + PgDB().use(FindFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + PgDB().use(FindFunctions::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + PgDB().use(FindFunctions::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(FindFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + PgDB().use(FindFunctions::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + PgDB().use(FindFunctions::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains retrieves matching documents") + fun byContainsMatch() = + PgDB().use(FindFunctions::byContainsMatch) + + @Test + @DisplayName("byContains retrieves ordered matching documents") + fun byContainsMatchOrdered() = + PgDB().use(FindFunctions::byContainsMatchOrdered) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(FindFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath retrieves matching documents") + fun byJsonPathMatch() = + PgDB().use(FindFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + fun byJsonPathMatchOrdered() = + PgDB().use(FindFunctions::byJsonPathMatchOrdered) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(FindFunctions::byJsonPathNoMatch) + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + PgDB().use(FindFunctions::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + PgDB().use(FindFunctions::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + PgDB().use(FindFunctions::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + PgDB().use(FindFunctions::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains retrieves a matching document") + fun firstByContainsMatchOne() = + PgDB().use(FindFunctions::firstByContainsMatchOne) + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + fun firstByContainsMatchMany() = + PgDB().use(FindFunctions::firstByContainsMatchMany) + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + fun firstByContainsMatchOrdered() = + PgDB().use(FindFunctions::firstByContainsMatchOrdered) + + @Test + @DisplayName("firstByContains returns null when no document matches") + fun firstByContainsNoMatch() = + PgDB().use(FindFunctions::firstByContainsNoMatch) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + fun firstByJsonPathMatchOne() = + PgDB().use(FindFunctions::firstByJsonPathMatchOne) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + fun firstByJsonPathMatchMany() = + PgDB().use(FindFunctions::firstByJsonPathMatchMany) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + fun firstByJsonPathMatchOrdered() = + PgDB().use(FindFunctions::firstByJsonPathMatchOrdered) + + @Test + @DisplayName("firstByJsonPath returns null when no document matches") + fun firstByJsonPathNoMatch() = + PgDB().use(FindFunctions::firstByJsonPathNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt new file mode 100644 index 0000000..703078d --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt @@ -0,0 +1,301 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Json") +class PostgreSQLJsonIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + PgDB().use(JsonFunctions::allDefault) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + PgDB().use(JsonFunctions::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + PgDB().use(JsonFunctions::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + PgDB().use(JsonFunctions::byIdNumber) + + @Test + @DisplayName("byId returns an empty document when a matching ID is not found") + fun byIdNotFound() = + PgDB().use(JsonFunctions::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + PgDB().use(JsonFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + PgDB().use(JsonFunctions::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + PgDB().use(JsonFunctions::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(JsonFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + PgDB().use(JsonFunctions::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + PgDB().use(JsonFunctions::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains retrieves matching documents") + fun byContainsMatch() = + PgDB().use(JsonFunctions::byContainsMatch) + + @Test + @DisplayName("byContains retrieves ordered matching documents") + fun byContainsMatchOrdered() = + PgDB().use(JsonFunctions::byContainsMatchOrdered) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(JsonFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath retrieves matching documents") + fun byJsonPathMatch() = + PgDB().use(JsonFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + fun byJsonPathMatchOrdered() = + PgDB().use(JsonFunctions::byJsonPathMatchOrdered) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(JsonFunctions::byJsonPathNoMatch) + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + PgDB().use(JsonFunctions::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + PgDB().use(JsonFunctions::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + PgDB().use(JsonFunctions::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns an empty document when no document matches") + fun firstByFieldsNoMatch() = + PgDB().use(JsonFunctions::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains retrieves a matching document") + fun firstByContainsMatchOne() = + PgDB().use(JsonFunctions::firstByContainsMatchOne) + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + fun firstByContainsMatchMany() = + PgDB().use(JsonFunctions::firstByContainsMatchMany) + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + fun firstByContainsMatchOrdered() = + PgDB().use(JsonFunctions::firstByContainsMatchOrdered) + + @Test + @DisplayName("firstByContains returns an empty document when no document matches") + fun firstByContainsNoMatch() = + PgDB().use(JsonFunctions::firstByContainsNoMatch) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + fun firstByJsonPathMatchOne() = + PgDB().use(JsonFunctions::firstByJsonPathMatchOne) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + fun firstByJsonPathMatchMany() = + PgDB().use(JsonFunctions::firstByJsonPathMatchMany) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + fun firstByJsonPathMatchOrdered() = + PgDB().use(JsonFunctions::firstByJsonPathMatchOrdered) + + @Test + @DisplayName("firstByJsonPath returns an empty document when no document matches") + fun firstByJsonPathNoMatch() = + PgDB().use(JsonFunctions::firstByJsonPathNoMatch) + + @Test + @DisplayName("writeAll retrieves all documents") + fun writeAllDefault() = + PgDB().use(JsonFunctions::writeAllDefault) + + @Test + @DisplayName("writeAll succeeds with an empty table") + fun writeAllEmpty() = + PgDB().use(JsonFunctions::writeAllEmpty) + + @Test + @DisplayName("writeById retrieves a document via a string ID") + fun writeByIdString() = + PgDB().use(JsonFunctions::writeByIdString) + + @Test + @DisplayName("writeById retrieves a document via a numeric ID") + fun writeByIdNumber() = + PgDB().use(JsonFunctions::writeByIdNumber) + + @Test + @DisplayName("writeById writes an empty document when a matching ID is not found") + fun writeByIdNotFound() = + PgDB().use(JsonFunctions::writeByIdNotFound) + + @Test + @DisplayName("writeByFields retrieves matching documents") + fun writeByFieldsMatch() = + PgDB().use(JsonFunctions::writeByFieldsMatch) + + @Test + @DisplayName("writeByFields retrieves ordered matching documents") + fun writeByFieldsMatchOrdered() = + PgDB().use(JsonFunctions::writeByFieldsMatchOrdered) + + @Test + @DisplayName("writeByFields retrieves matching documents with a numeric IN clause") + fun writeByFieldsMatchNumIn() = + PgDB().use(JsonFunctions::writeByFieldsMatchNumIn) + + @Test + @DisplayName("writeByFields succeeds when no documents match") + fun writeByFieldsNoMatch() = + PgDB().use(JsonFunctions::writeByFieldsNoMatch) + + @Test + @DisplayName("writeByFields retrieves matching documents with an IN_ARRAY comparison") + fun writeByFieldsMatchInArray() = + PgDB().use(JsonFunctions::writeByFieldsMatchInArray) + + @Test + @DisplayName("writeByFields succeeds when no documents match an IN_ARRAY comparison") + fun writeByFieldsNoMatchInArray() = + PgDB().use(JsonFunctions::writeByFieldsNoMatchInArray) + + @Test + @DisplayName("writeByContains retrieves matching documents") + fun writeByContainsMatch() = + PgDB().use(JsonFunctions::writeByContainsMatch) + + @Test + @DisplayName("writeByContains retrieves ordered matching documents") + fun writeByContainsMatchOrdered() = + PgDB().use(JsonFunctions::writeByContainsMatchOrdered) + + @Test + @DisplayName("writeByContains succeeds when no documents match") + fun writeByContainsNoMatch() = + PgDB().use(JsonFunctions::writeByContainsNoMatch) + + @Test + @DisplayName("writeByJsonPath retrieves matching documents") + fun writeByJsonPathMatch() = + PgDB().use(JsonFunctions::writeByJsonPathMatch) + + @Test + @DisplayName("writeByJsonPath retrieves ordered matching documents") + fun writeByJsonPathMatchOrdered() = + PgDB().use(JsonFunctions::writeByJsonPathMatchOrdered) + + @Test + @DisplayName("writeByJsonPath succeeds when no documents match") + fun writeByJsonPathNoMatch() = + PgDB().use(JsonFunctions::writeByJsonPathNoMatch) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document") + fun writeFirstByFieldsMatchOne() = + PgDB().use(JsonFunctions::writeFirstByFieldsMatchOne) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many") + fun writeFirstByFieldsMatchMany() = + PgDB().use(JsonFunctions::writeFirstByFieldsMatchMany) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many (ordered)") + fun writeFirstByFieldsMatchOrdered() = + PgDB().use(JsonFunctions::writeFirstByFieldsMatchOrdered) + + @Test + @DisplayName("writeFirstByFields writes an empty document when no document matches") + fun writeFirstByFieldsNoMatch() = + PgDB().use(JsonFunctions::writeFirstByFieldsNoMatch) + + @Test + @DisplayName("writeFirstByContains retrieves a matching document") + fun writeFirstByContainsMatchOne() = + PgDB().use(JsonFunctions::writeFirstByContainsMatchOne) + + @Test + @DisplayName("writeFirstByContains retrieves a matching document among many") + fun writeFirstByContainsMatchMany() = + PgDB().use(JsonFunctions::writeFirstByContainsMatchMany) + + @Test + @DisplayName("writeFirstByContains retrieves a matching document among many (ordered)") + fun writeFirstByContainsMatchOrdered() = + PgDB().use(JsonFunctions::writeFirstByContainsMatchOrdered) + + @Test + @DisplayName("writeFirstByContains writes an empty document when no document matches") + fun writeFirstByContainsNoMatch() = + PgDB().use(JsonFunctions::writeFirstByContainsNoMatch) + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document") + fun writeFirstByJsonPathMatchOne() = + PgDB().use(JsonFunctions::writeFirstByJsonPathMatchOne) + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document among many") + fun writeFirstByJsonPathMatchMany() = + PgDB().use(JsonFunctions::writeFirstByJsonPathMatchMany) + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document among many (ordered)") + fun writeFirstByJsonPathMatchOrdered() = + PgDB().use(JsonFunctions::writeFirstByJsonPathMatchOrdered) + + @Test + @DisplayName("writeFirstByJsonPath writes an empty document when no document matches") + fun writeFirstByJsonPathNoMatch() = + PgDB().use(JsonFunctions::writeFirstByJsonPathNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLPatchIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLPatchIT.kt new file mode 100644 index 0000000..2455b57 --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLPatchIT.kt @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: Patch") +class PostgreSQLPatchIT { + + @Test + @DisplayName("byId patches an existing document") + fun byIdMatch() = + PgDB().use(PatchFunctions::byIdMatch) + + @Test + @DisplayName("byId succeeds for a non-existent document") + fun byIdNoMatch() = + PgDB().use(PatchFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields patches matching document") + fun byFieldsMatch() = + PgDB().use(PatchFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(PatchFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains patches matching document") + fun byContainsMatch() = + PgDB().use(PatchFunctions::byContainsMatch) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(PatchFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath patches matching document") + fun byJsonPathMatch() = + PgDB().use(PatchFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(PatchFunctions::byJsonPathNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt new file mode 100644 index 0000000..9a2e2d1 --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt @@ -0,0 +1,71 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("Core | Kotlin | PostgreSQL: RemoveFields") +class PostgreSQLRemoveFieldsIT { + + @Test + @DisplayName("byId removes fields from an existing document") + fun byIdMatchFields() = + PgDB().use(RemoveFieldsFunctions::byIdMatchFields) + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + fun byIdMatchNoFields() = + PgDB().use(RemoveFieldsFunctions::byIdMatchNoFields) + + @Test + @DisplayName("byId succeeds when no document exists") + fun byIdNoMatch() = + PgDB().use(RemoveFieldsFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields removes fields from matching documents") + fun byFieldsMatchFields() = + PgDB().use(RemoveFieldsFunctions::byFieldsMatchFields) + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + fun byFieldsMatchNoFields() = + PgDB().use(RemoveFieldsFunctions::byFieldsMatchNoFields) + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + fun byFieldsNoMatch() = + PgDB().use(RemoveFieldsFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains removes fields from matching documents") + fun byContainsMatchFields() = + PgDB().use(RemoveFieldsFunctions::byContainsMatchFields) + + @Test + @DisplayName("byContains succeeds when fields do not exist on matching documents") + fun byContainsMatchNoFields() = + PgDB().use(RemoveFieldsFunctions::byContainsMatchNoFields) + + @Test + @DisplayName("byContains succeeds when no matching documents exist") + fun byContainsNoMatch() = + PgDB().use(RemoveFieldsFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath removes fields from matching documents") + fun byJsonPathMatchFields() = + PgDB().use(RemoveFieldsFunctions::byJsonPathMatchFields) + + @Test + @DisplayName("byJsonPath succeeds when fields do not exist on matching documents") + fun byJsonPathMatchNoFields() = + PgDB().use(RemoveFieldsFunctions::byJsonPathMatchNoFields) + + @Test + @DisplayName("byJsonPath succeeds when no matching documents exist") + fun byJsonPathNoMatch() = + PgDB().use(RemoveFieldsFunctions::byJsonPathNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/RemoveFieldsFunctions.kt b/src/core/src/test/kotlin/integration/RemoveFieldsFunctions.kt new file mode 100644 index 0000000..2096c08 --- /dev/null +++ b/src/core/src/test/kotlin/integration/RemoveFieldsFunctions.kt @@ -0,0 +1,107 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.* + +/** + * Integration tests for the `RemoveFields` object + */ +object RemoveFieldsFunctions { + + fun byIdMatchFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.removeFieldsById(TEST_TABLE, "two", listOf("sub", "value")) + val doc = db.conn.findById(TEST_TABLE, "two", JsonDocument::class.java) + assertTrue(doc.isPresent, "There should have been a document returned") + assertEquals("", doc.get().value, "The value should have been empty") + assertNull(doc.get().sub, "The sub-document should have been removed") + } + + fun byIdMatchNoFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, listOf(Field.exists("a_field_that_does_not_exist")))) + db.conn.removeFieldsById(TEST_TABLE, "one", listOf("a_field_that_does_not_exist")) // no exception = pass + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse(db.conn.existsById(TEST_TABLE, "fifty")) + db.conn.removeFieldsById(TEST_TABLE, "fifty", listOf("sub")) // no exception = pass + } + + fun byFieldsMatchFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + val fields = listOf(Field.equal("numValue", 17)) + db.conn.removeFieldsByFields(TEST_TABLE, fields, listOf("sub")) + val doc = db.conn.findFirstByFields(TEST_TABLE, fields, JsonDocument::class.java) + assertTrue(doc.isPresent, "The document should have been returned") + assertEquals("four", doc.get().id, "An incorrect document was returned") + assertNull(doc.get().sub, "The sub-document should have been removed") + } + + fun byFieldsMatchNoFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, listOf(Field.exists("nada")))) + db.conn.removeFieldsByFields(TEST_TABLE, listOf(Field.equal("numValue", 17)), listOf("nada")) // no exn = pass + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val fields = listOf(Field.notEqual("missing", "nope")) + assertFalse(db.conn.existsByFields(TEST_TABLE, fields)) + db.conn.removeFieldsByFields(TEST_TABLE, fields, listOf("value")) // no exception = pass + } + + fun byContainsMatchFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + val criteria = mapOf("sub" to mapOf("foo" to "green")) + db.conn.removeFieldsByContains(TEST_TABLE, criteria, listOf("value")) + val docs = db.conn.findByContains(TEST_TABLE, criteria, JsonDocument::class.java) + assertEquals(2, docs.size, "There should have been 2 documents returned") + docs.forEach { + assertTrue(listOf("two", "four").contains(it.id), "An incorrect document was returned (${it.id})") + assertEquals("", it.value, "The value should have been empty") + } + } + + fun byContainsMatchNoFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, listOf(Field.exists("invalid_field")))) + db.conn.removeFieldsByContains(TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf("invalid_field")) + // no exception = pass + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val contains = mapOf("value" to "substantial") + assertFalse(db.conn.existsByContains(TEST_TABLE, contains)) + db.conn.removeFieldsByContains(TEST_TABLE, contains, listOf("numValue")) + } + + fun byJsonPathMatchFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + val path = "$.value ? (@ == \"purple\")" + db.conn.removeFieldsByJsonPath(TEST_TABLE, path, listOf("sub")) + val docs = db.conn.findByJsonPath(TEST_TABLE, path, JsonDocument::class.java) + assertEquals(2, docs.size, "There should have been 2 documents returned") + docs.forEach { + assertTrue(listOf("four", "five").contains(it.id), "An incorrect document was returned (${it.id})") + assertNull(it.sub, "The sub-document should have been removed") + } + } + + fun byJsonPathMatchNoFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, listOf(Field.exists("submarine")))) + db.conn.removeFieldsByJsonPath(TEST_TABLE, "$.value ? (@ == \"purple\")", listOf("submarine")) // no exn = pass + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val path = "$.value ? (@ == \"mauve\")" + assertFalse(db.conn.existsByJsonPath(TEST_TABLE, path)) + db.conn.removeFieldsByJsonPath(TEST_TABLE, path, listOf("value")) // no exception = pass + } +} diff --git a/src/core/src/test/kotlin/integration/SQLiteCountIT.kt b/src/core/src/test/kotlin/integration/SQLiteCountIT.kt new file mode 100644 index 0000000..08575e4 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteCountIT.kt @@ -0,0 +1,40 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Count") +class SQLiteCountIT { + + @Test + @DisplayName("all counts all documents") + fun all() = + SQLiteDB().use(CountFunctions::all) + + @Test + @DisplayName("byFields counts documents by a numeric value") + fun byFieldsNumeric() = + SQLiteDB().use(CountFunctions::byFieldsNumeric) + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + fun byFieldsAlpha() = + SQLiteDB().use(CountFunctions::byFieldsAlpha) + + @Test + @DisplayName("byContains fails") + fun byContainsMatch() { + assertThrows { SQLiteDB().use(CountFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathMatch() { + assertThrows { SQLiteDB().use(CountFunctions::byJsonPathMatch) } + } +} diff --git a/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt new file mode 100644 index 0000000..01f8ffe --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -0,0 +1,86 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * SQLite integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Custom") +class SQLiteCustomIT { + + @Test + @DisplayName("list succeeds with empty list") + fun listEmpty() = + SQLiteDB().use(CustomFunctions::listEmpty) + + @Test + @DisplayName("list succeeds with a non-empty list") + fun listAll() = + SQLiteDB().use(CustomFunctions::listAll) + + @Test + @DisplayName("jsonArray succeeds with empty array") + fun jsonArrayEmpty() = + SQLiteDB().use(CustomFunctions::jsonArrayEmpty) + + @Test + @DisplayName("jsonArray succeeds with a single-item list") + fun jsonArraySingle() = + SQLiteDB().use(CustomFunctions::jsonArraySingle) + + @Test + @DisplayName("jsonArray succeeds with a multi-item list") + fun jsonArrayMany() = + SQLiteDB().use(CustomFunctions::jsonArrayMany) + + @Test + @DisplayName("writeJsonArray succeeds with empty array") + fun writeJsonArrayEmpty() = + SQLiteDB().use(CustomFunctions::writeJsonArrayEmpty) + + @Test + @DisplayName("writeJsonArray succeeds with a single-item list") + fun writeJsonArraySingle() = + SQLiteDB().use(CustomFunctions::writeJsonArraySingle) + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item list") + fun writeJsonArrayMany() = + SQLiteDB().use(CustomFunctions::writeJsonArrayMany) + + @Test + @DisplayName("single succeeds when document not found") + fun singleNone() = + SQLiteDB().use(CustomFunctions::singleNone) + + @Test + @DisplayName("single succeeds when a document is found") + fun singleOne() = + SQLiteDB().use(CustomFunctions::singleOne) + + @Test + @DisplayName("jsonSingle succeeds when document not found") + fun jsonSingleNone() = + SQLiteDB().use(CustomFunctions::jsonSingleNone) + + @Test + @DisplayName("jsonSingle succeeds when a document is found") + fun jsonSingleOne() = + SQLiteDB().use(CustomFunctions::jsonSingleOne) + + @Test + @DisplayName("nonQuery makes changes") + fun nonQueryChanges() = + SQLiteDB().use(CustomFunctions::nonQueryChanges) + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + fun nonQueryNoChanges() = + SQLiteDB().use(CustomFunctions::nonQueryNoChanges) + + @Test + @DisplayName("scalar succeeds") + fun scalar() = + SQLiteDB().use(CustomFunctions::scalar) +} diff --git a/src/core/src/test/kotlin/integration/SQLiteDB.kt b/src/core/src/test/kotlin/integration/SQLiteDB.kt new file mode 100644 index 0000000..4f9a05f --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteDB.kt @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.core.tests.TEST_TABLE +import solutions.bitbadger.documents.java.Results +import solutions.bitbadger.documents.java.extensions.* +import java.io.File + +/** + * A wrapper for a throwaway SQLite database + */ +class SQLiteDB : ThrowawayDatabase() { + + init { + Configuration.connectionString = "jdbc:sqlite:$dbName" + } + + override val conn = Configuration.dbConn() + + init { + conn.ensureTable(TEST_TABLE) + } + + override fun close() { + conn.close() + File(dbName).delete() + } + + override fun dbObjectExists(name: String) = + conn.customScalar("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = :name) AS it", + listOf(Parameter(":name", ParameterType.STRING, name)), Boolean::class.java, Results::toExists) +} diff --git a/src/core/src/test/kotlin/integration/SQLiteDefinitionIT.kt b/src/core/src/test/kotlin/integration/SQLiteDefinitionIT.kt new file mode 100644 index 0000000..9f0ff14 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteDefinitionIT.kt @@ -0,0 +1,35 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Definition") +class SQLiteDefinitionIT { + + @Test + @DisplayName("ensureTable creates table and index") + fun ensureTable() = + SQLiteDB().use(DefinitionFunctions::ensureTable) + + @Test + @DisplayName("ensureFieldIndex creates an index") + fun ensureFieldIndex() = + SQLiteDB().use(DefinitionFunctions::ensureFieldIndex) + + @Test + @DisplayName("ensureDocumentIndex fails for full index") + fun ensureDocumentIndexFull() { + assertThrows { SQLiteDB().use(DefinitionFunctions::ensureDocumentIndexFull) } + } + + @Test + @DisplayName("ensureDocumentIndex fails for optimized index") + fun ensureDocumentIndexOptimized() { + assertThrows { SQLiteDB().use(DefinitionFunctions::ensureDocumentIndexOptimized) } + } +} diff --git a/src/core/src/test/kotlin/integration/SQLiteDeleteIT.kt b/src/core/src/test/kotlin/integration/SQLiteDeleteIT.kt new file mode 100644 index 0000000..fa1acad --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteDeleteIT.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Delete") +class SQLiteDeleteIT { + + @Test + @DisplayName("byId deletes a matching ID") + fun byIdMatch() = + SQLiteDB().use(DeleteFunctions::byIdMatch) + + @Test + @DisplayName("byId succeeds when no ID matches") + fun byIdNoMatch() = + SQLiteDB().use(DeleteFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields deletes matching documents") + fun byFieldsMatch() = + SQLiteDB().use(DeleteFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(DeleteFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(DeleteFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(DeleteFunctions::byJsonPathMatch) } + } +} diff --git a/src/core/src/test/kotlin/integration/SQLiteDocumentIT.kt b/src/core/src/test/kotlin/integration/SQLiteDocumentIT.kt new file mode 100644 index 0000000..f062725 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteDocumentIT.kt @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Document") +class SQLiteDocumentIT { + + @Test + @DisplayName("insert works with default values") + fun insertDefault() = + SQLiteDB().use(DocumentFunctions::insertDefault) + + @Test + @DisplayName("insert fails with duplicate key") + fun insertDupe() = + SQLiteDB().use(DocumentFunctions::insertDupe) + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + fun insertNumAutoId() = + SQLiteDB().use(DocumentFunctions::insertNumAutoId) + + @Test + @DisplayName("insert succeeds with UUID auto ID") + fun insertUUIDAutoId() = + SQLiteDB().use(DocumentFunctions::insertUUIDAutoId) + + @Test + @DisplayName("insert succeeds with random string auto ID") + fun insertStringAutoId() = + SQLiteDB().use(DocumentFunctions::insertStringAutoId) + + @Test + @DisplayName("save updates an existing document") + fun saveMatch() = + SQLiteDB().use(DocumentFunctions::saveMatch) + + @Test + @DisplayName("save inserts a new document") + fun saveNoMatch() = + SQLiteDB().use(DocumentFunctions::saveNoMatch) + + @Test + @DisplayName("update replaces an existing document") + fun updateMatch() = + SQLiteDB().use(DocumentFunctions::updateMatch) + + @Test + @DisplayName("update succeeds when no document exists") + fun updateNoMatch() = + SQLiteDB().use(DocumentFunctions::updateNoMatch) +} diff --git a/src/core/src/test/kotlin/integration/SQLiteExistsIT.kt b/src/core/src/test/kotlin/integration/SQLiteExistsIT.kt new file mode 100644 index 0000000..e4ad1a9 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteExistsIT.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Exists") +class SQLiteExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + fun byIdMatch() = + SQLiteDB().use(ExistsFunctions::byIdMatch) + + @Test + @DisplayName("byId returns false when no document matches the ID") + fun byIdNoMatch() = + SQLiteDB().use(ExistsFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields returns true when documents match") + fun byFieldsMatch() = + SQLiteDB().use(ExistsFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields returns false when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(ExistsFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(ExistsFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(ExistsFunctions::byJsonPathMatch) } + } +} diff --git a/src/core/src/test/kotlin/integration/SQLiteFindIT.kt b/src/core/src/test/kotlin/integration/SQLiteFindIT.kt new file mode 100644 index 0000000..25f1f02 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteFindIT.kt @@ -0,0 +1,127 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Find") +class SQLiteFindIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + SQLiteDB().use(FindFunctions::allDefault) + + @Test + @DisplayName("all sorts data ascending") + fun allAscending() = + SQLiteDB().use(FindFunctions::allAscending) + + @Test + @DisplayName("all sorts data descending") + fun allDescending() = + SQLiteDB().use(FindFunctions::allDescending) + + @Test + @DisplayName("all sorts data numerically") + fun allNumOrder() = + SQLiteDB().use(FindFunctions::allNumOrder) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + SQLiteDB().use(FindFunctions::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + SQLiteDB().use(FindFunctions::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + SQLiteDB().use(FindFunctions::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + SQLiteDB().use(FindFunctions::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + SQLiteDB().use(FindFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + SQLiteDB().use(FindFunctions::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + SQLiteDB().use(FindFunctions::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(FindFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + SQLiteDB().use(FindFunctions::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + SQLiteDB().use(FindFunctions::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(FindFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(FindFunctions::byJsonPathMatch) } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + SQLiteDB().use(FindFunctions::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + SQLiteDB().use(FindFunctions::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + SQLiteDB().use(FindFunctions::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + SQLiteDB().use(FindFunctions::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains fails") + fun firstByContainsFails() { + assertThrows { SQLiteDB().use(FindFunctions::firstByContainsMatchOne) } + } + + @Test + @DisplayName("firstByJsonPath fails") + fun firstByJsonPathFails() { + assertThrows { SQLiteDB().use(FindFunctions::firstByJsonPathMatchOne) } + } +} diff --git a/src/core/src/test/kotlin/integration/SQLiteJsonIT.kt b/src/core/src/test/kotlin/integration/SQLiteJsonIT.kt new file mode 100644 index 0000000..f416849 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteJsonIT.kt @@ -0,0 +1,211 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Json") +class SQLiteJsonIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + SQLiteDB().use(JsonFunctions::allDefault) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + SQLiteDB().use(JsonFunctions::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + SQLiteDB().use(JsonFunctions::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + SQLiteDB().use(JsonFunctions::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + SQLiteDB().use(JsonFunctions::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + SQLiteDB().use(JsonFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + SQLiteDB().use(JsonFunctions::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + SQLiteDB().use(JsonFunctions::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(JsonFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + SQLiteDB().use(JsonFunctions::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + SQLiteDB().use(JsonFunctions::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(JsonFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(JsonFunctions::byJsonPathMatch) } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + SQLiteDB().use(JsonFunctions::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + SQLiteDB().use(JsonFunctions::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + SQLiteDB().use(JsonFunctions::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + SQLiteDB().use(JsonFunctions::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains fails") + fun firstByContainsFails() { + assertThrows { SQLiteDB().use(JsonFunctions::firstByContainsMatchOne) } + } + + @Test + @DisplayName("firstByJsonPath fails") + fun firstByJsonPathFails() { + assertThrows { SQLiteDB().use(JsonFunctions::firstByJsonPathMatchOne) } + } + + @Test + @DisplayName("writeAll retrieves all documents") + fun writeAllDefault() = + SQLiteDB().use(JsonFunctions::writeAllDefault) + + @Test + @DisplayName("writeAll succeeds with an empty table") + fun writeAllEmpty() = + SQLiteDB().use(JsonFunctions::writeAllEmpty) + + @Test + @DisplayName("writeById retrieves a document via a string ID") + fun writeByIdString() = + SQLiteDB().use(JsonFunctions::writeByIdString) + + @Test + @DisplayName("writeById retrieves a document via a numeric ID") + fun writeByIdNumber() = + SQLiteDB().use(JsonFunctions::writeByIdNumber) + + @Test + @DisplayName("writeById returns null when a matching ID is not found") + fun writeByIdNotFound() = + SQLiteDB().use(JsonFunctions::writeByIdNotFound) + + @Test + @DisplayName("writeByFields retrieves matching documents") + fun writeByFieldsMatch() = + SQLiteDB().use(JsonFunctions::writeByFieldsMatch) + + @Test + @DisplayName("writeByFields retrieves ordered matching documents") + fun writeByFieldsMatchOrdered() = + SQLiteDB().use(JsonFunctions::writeByFieldsMatchOrdered) + + @Test + @DisplayName("writeByFields retrieves matching documents with a numeric IN clause") + fun writeByFieldsMatchNumIn() = + SQLiteDB().use(JsonFunctions::writeByFieldsMatchNumIn) + + @Test + @DisplayName("writeByFields succeeds when no documents match") + fun writeByFieldsNoMatch() = + SQLiteDB().use(JsonFunctions::writeByFieldsNoMatch) + + @Test + @DisplayName("writeByFields retrieves matching documents with an IN_ARRAY comparison") + fun writeByFieldsMatchInArray() = + SQLiteDB().use(JsonFunctions::writeByFieldsMatchInArray) + + @Test + @DisplayName("writeByFields succeeds when no documents match an IN_ARRAY comparison") + fun writeByFieldsNoMatchInArray() = + SQLiteDB().use(JsonFunctions::writeByFieldsNoMatchInArray) + + @Test + @DisplayName("writeByContains fails") + fun writeByContainsFails() { + assertThrows { SQLiteDB().use(JsonFunctions::writeByContainsMatch) } + } + + @Test + @DisplayName("writeByJsonPath fails") + fun writeByJsonPathFails() { + assertThrows { SQLiteDB().use(JsonFunctions::writeByJsonPathMatch) } + } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document") + fun writeFirstByFieldsMatchOne() = + SQLiteDB().use(JsonFunctions::writeFirstByFieldsMatchOne) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many") + fun writeFirstByFieldsMatchMany() = + SQLiteDB().use(JsonFunctions::writeFirstByFieldsMatchMany) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many (ordered)") + fun writeFirstByFieldsMatchOrdered() = + SQLiteDB().use(JsonFunctions::writeFirstByFieldsMatchOrdered) + + @Test + @DisplayName("writeFirstByFields returns null when no document matches") + fun writeFirstByFieldsNoMatch() = + SQLiteDB().use(JsonFunctions::writeFirstByFieldsNoMatch) + + @Test + @DisplayName("writeFirstByContains fails") + fun writeFirstByContainsFails() { + assertThrows { SQLiteDB().use(JsonFunctions::writeFirstByContainsMatchOne) } + } + + @Test + @DisplayName("writeFirstByJsonPath fails") + fun writeFirstByJsonPathFails() { + assertThrows { SQLiteDB().use(JsonFunctions::writeFirstByJsonPathMatchOne) } + } +} diff --git a/src/core/src/test/kotlin/integration/SQLitePatchIT.kt b/src/core/src/test/kotlin/integration/SQLitePatchIT.kt new file mode 100644 index 0000000..abbdcb0 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLitePatchIT.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: Patch") +class SQLitePatchIT { + + @Test + @DisplayName("byId patches an existing document") + fun byIdMatch() = + SQLiteDB().use(PatchFunctions::byIdMatch) + + @Test + @DisplayName("byId succeeds for a non-existent document") + fun byIdNoMatch() = + SQLiteDB().use(PatchFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields patches matching document") + fun byFieldsMatch() = + SQLiteDB().use(PatchFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(PatchFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(PatchFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(PatchFunctions::byJsonPathMatch) } + } +} \ No newline at end of file diff --git a/src/core/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt b/src/core/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt new file mode 100644 index 0000000..5fbc6c0 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt @@ -0,0 +1,55 @@ +package solutions.bitbadger.documents.core.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("Core | Kotlin | SQLite: RemoveFields") +class SQLiteRemoveFieldsIT { + + @Test + @DisplayName("byId removes fields from an existing document") + fun byIdMatchFields() = + SQLiteDB().use(RemoveFieldsFunctions::byIdMatchFields) + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + fun byIdMatchNoFields() = + SQLiteDB().use(RemoveFieldsFunctions::byIdMatchNoFields) + + @Test + @DisplayName("byId succeeds when no document exists") + fun byIdNoMatch() = + SQLiteDB().use(RemoveFieldsFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields removes fields from matching documents") + fun byFieldsMatchFields() = + SQLiteDB().use(RemoveFieldsFunctions::byFieldsMatchFields) + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + fun byFieldsMatchNoFields() = + SQLiteDB().use(RemoveFieldsFunctions::byFieldsMatchNoFields) + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + fun byFieldsNoMatch() = + SQLiteDB().use(RemoveFieldsFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(RemoveFieldsFunctions::byContainsMatchFields) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(RemoveFieldsFunctions::byJsonPathMatchFields) } + } +} diff --git a/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt b/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt new file mode 100644 index 0000000..b2b48e7 --- /dev/null +++ b/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt @@ -0,0 +1,29 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.java.DocumentConfig +import java.sql.Connection + +/** + * Common interface for PostgreSQL and SQLite throwaway databases + */ +abstract class ThrowawayDatabase : AutoCloseable { + + /** The name of the throwaway database */ + protected val dbName = "throwaway_${AutoId.generateRandomString(8)}" + + init { + DocumentConfig.serializer = JacksonDocumentSerializer() + } + + /** The database connection for the throwaway database */ + abstract val conn: Connection + + /** + * Determine if a database object exists + * + * @param name The name of the object whose existence should be checked + * @return True if the object exists, false if not + */ + abstract fun dbObjectExists(name: String): Boolean +} diff --git a/src/groovy/groovy.iml b/src/groovy/groovy.iml new file mode 100644 index 0000000..056f882 --- /dev/null +++ b/src/groovy/groovy.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/groovy/pom.xml b/src/groovy/pom.xml new file mode 100644 index 0000000..5c8dde5 --- /dev/null +++ b/src/groovy/pom.xml @@ -0,0 +1,185 @@ + + + 4.0.0 + + solutions.bitbadger + documents + 1.0.0-RC1 + ../../pom.xml + + + solutions.bitbadger.documents + groovy + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (Groovy Library) + https://relationaldocs.bitbadger.solutions/jvm/ + + + + MIT License + https://www.opensource.org/licenses/mit-license.php + + + + + + Daniel J. Summers + daniel@bitbadger.solutions + Bit Badger Solutions + https://bitbadger.solutions + + + + + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + 4.1.1 + + + + addSources + addTestSources + generateStubs + compile + generateTestStubs + compileTests + removeStubs + removeTestStubs + + + + + + org.apache.groovy + groovy + ${groovy.version} + runtime + pom + + + + + maven-surefire-plugin + ${surefire.version} + + + maven-failsafe-plugin + ${failsafe.version} + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + org.codehaus.mojo + build-helper-maven-plugin + ${buildHelperPlugin.version} + + + generate-sources + + add-source + + + + src/main/groovy + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${sourcePlugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${javaDocPlugin.version} + + + attach-javadocs + + jar + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + Deployment-groovy-${project.version} + central + + + + + + + + solutions.bitbadger.documents + core + ${project.version} + + + org.apache.groovy + groovy + ${groovy.version} + + + org.apache.groovy + groovy-test + ${groovy.version} + test + + + org.apache.groovy + groovy-test-junit5 + ${groovy.version} + test + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + + + + \ No newline at end of file diff --git a/src/groovy/src/main/groovy/.gitkeep b/src/groovy/src/main/groovy/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/groovy/src/main/java/module-info.java b/src/groovy/src/main/java/module-info.java new file mode 100644 index 0000000..80da45b --- /dev/null +++ b/src/groovy/src/main/java/module-info.java @@ -0,0 +1,7 @@ +/** + * This module registers the Kotlin extension methods for the JDBC Connection object with the Groovy + * runtime + */ +module solutions.bitbadger.documents.groovy { + requires solutions.bitbadger.documents.core; +} diff --git a/src/groovy/src/main/java/solutions/bitbadger/documents/groovy/NoClassesHere.java b/src/groovy/src/main/java/solutions/bitbadger/documents/groovy/NoClassesHere.java new file mode 100644 index 0000000..cead2b1 --- /dev/null +++ b/src/groovy/src/main/java/solutions/bitbadger/documents/groovy/NoClassesHere.java @@ -0,0 +1,7 @@ +package solutions.bitbadger.documents.groovy; + +/** + * This library has no classes; it only updates Groovy's configuration + */ +public interface NoClassesHere { +} diff --git a/src/groovy/src/main/java/solutions/bitbadger/documents/groovy/package-info.java b/src/groovy/src/main/java/solutions/bitbadger/documents/groovy/package-info.java new file mode 100644 index 0000000..fed9331 --- /dev/null +++ b/src/groovy/src/main/java/solutions/bitbadger/documents/groovy/package-info.java @@ -0,0 +1,11 @@ +/** + * Groovy extensions on the JDBC Connection object exposing a document store API + *

+ * This package wires up the core extensions to work in Groovy. No imports are needed, and the + * documentation for the solutions.bitbadger.documents.java.extensions package applies to those extensions + * as well. + *

+ * For example, finding all documents in a table, using the document manipulation functions, looks something like + * Find.all(tableName, conn); with the extensions, this becomes conn.findAll(tableName). + */ +package solutions.bitbadger.documents.groovy; diff --git a/src/groovy/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule b/src/groovy/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule new file mode 100644 index 0000000..e5291f8 --- /dev/null +++ b/src/groovy/src/main/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule @@ -0,0 +1,3 @@ +moduleName=Document Extensions for Connection +moduleVersion=4.0.0-alpha1 +extensionClasses=solutions.bitbadger.documents.java.extensions.ConnExt diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/AutoIdTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/AutoIdTest.groovy new file mode 100644 index 0000000..f4501e7 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/AutoIdTest.groovy @@ -0,0 +1,163 @@ +package solutions.bitbadger.documents.groovy.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `AutoId` enum + */ +@DisplayName('Groovy | AutoId') +class AutoIdTest { + + @Test + @DisplayName('Generates a UUID string') + void generateUUID() { + assertEquals 32, AutoId.generateUUID().length(), 'The UUID should have been a 32-character string' + } + + @Test + @DisplayName('Generates a random hex character string of an even length') + void generateRandomStringEven() { + def result = AutoId.generateRandomString 8 + assertEquals 8, result.length(), "There should have been 8 characters in $result" + } + + @Test + @DisplayName('Generates a random hex character string of an odd length') + void generateRandomStringOdd() { + def result = AutoId.generateRandomString 11 + assertEquals 11, result.length(), "There should have been 11 characters in $result" + } + + @Test + @DisplayName('Generates different random hex character strings') + void generateRandomStringIsRandom() { + def result1 = AutoId.generateRandomString 16 + def result2 = AutoId.generateRandomString 16 + assertNotEquals result1, result2, 'There should have been 2 different strings generated' + } + + @Test + @DisplayName('needsAutoId fails for null document') + void needsAutoIdFailsForNullDocument() { + assertThrows(DocumentException) { AutoId.needsAutoId(AutoId.DISABLED, null, 'id') } + } + + @Test + @DisplayName('needsAutoId fails for missing ID property') + void needsAutoIdFailsForMissingId() { + assertThrows(DocumentException) { AutoId.needsAutoId(AutoId.UUID, new IntIdClass(0), 'Id') } + } + + @Test + @DisplayName('needsAutoId returns false if disabled') + void needsAutoIdFalseIfDisabled() { + assertFalse AutoId.needsAutoId(AutoId.DISABLED, '', ''), 'Disabled Auto ID should always return false' + } + + @Test + @DisplayName('needsAutoId returns true for Number strategy and byte ID of 0') + void needsAutoIdTrueForByteWithZero() { + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 0), 'id'), + 'Number Auto ID with 0 should return true') + } + + @Test + @DisplayName('needsAutoId returns false for Number strategy and byte ID of non-0') + void needsAutoIdFalseForByteWithNonZero() { + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 77), 'id'), + 'Number Auto ID with 77 should return false') + } + + @Test + @DisplayName('needsAutoId returns true for Number strategy and short ID of 0') + void needsAutoIdTrueForShortWithZero() { + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 0), 'id'), + 'Number Auto ID with 0 should return true') + } + + @Test + @DisplayName('needsAutoId returns false for Number strategy and short ID of non-0') + void needsAutoIdFalseForShortWithNonZero() { + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 31), 'id'), + 'Number Auto ID with 31 should return false') + } + + @Test + @DisplayName('needsAutoId returns true for Number strategy and int ID of 0') + void needsAutoIdTrueForIntWithZero() { + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(0), 'id'), + 'Number Auto ID with 0 should return true') + } + + @Test + @DisplayName('needsAutoId returns false for Number strategy and int ID of non-0') + void needsAutoIdFalseForIntWithNonZero() { + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(6), 'id'), + 'Number Auto ID with 6 should return false') + } + + @Test + @DisplayName('needsAutoId returns true for Number strategy and long ID of 0') + void needsAutoIdTrueForLongWithZero() { + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(0L), 'id'), + 'Number Auto ID with 0 should return true') + } + + @Test + @DisplayName('needsAutoId returns false for Number strategy and long ID of non-0') + void needsAutoIdFalseForLongWithNonZero() { + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(2L), 'id'), + 'Number Auto ID with 2 should return false') + } + + @Test + @DisplayName('needsAutoId fails for Number strategy and non-number ID') + void needsAutoIdFailsForNumberWithStringId() { + assertThrows(DocumentException) { AutoId.needsAutoId(AutoId.NUMBER, new StringIdClass(''), 'id') } + } + + @Test + @DisplayName('needsAutoId returns true for UUID strategy and blank ID') + void needsAutoIdTrueForUUIDWithBlank() { + assertTrue(AutoId.needsAutoId(AutoId.UUID, new StringIdClass(''), 'id'), + 'UUID Auto ID with blank should return true') + } + + @Test + @DisplayName('needsAutoId returns false for UUID strategy and non-blank ID') + void needsAutoIdFalseForUUIDNotBlank() { + assertFalse(AutoId.needsAutoId(AutoId.UUID, new StringIdClass('howdy'), 'id'), + 'UUID Auto ID with non-blank should return false') + } + + @Test + @DisplayName('needsAutoId fails for UUID strategy and non-string ID') + void needsAutoIdFailsForUUIDNonString() { + assertThrows(DocumentException) { AutoId.needsAutoId(AutoId.UUID, new IntIdClass(5), 'id') } + } + + @Test + @DisplayName('needsAutoId returns true for Random String strategy and blank ID') + void needsAutoIdTrueForRandomWithBlank() { + assertTrue(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass(''), 'id'), + 'Random String Auto ID with blank should return true') + } + + @Test + @DisplayName('needsAutoId returns false for Random String strategy and non-blank ID') + void needsAutoIdFalseForRandomNotBlank() { + assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass('full'), 'id'), + 'Random String Auto ID with non-blank should return false') + } + + @Test + @DisplayName('needsAutoId fails for Random String strategy and non-string ID') + void needsAutoIdFailsForRandomNonString() { + assertThrows(DocumentException) { AutoId.needsAutoId(AutoId.RANDOM_STRING, new ShortIdClass((short) 55), 'id') } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ByteIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ByteIdClass.groovy new file mode 100644 index 0000000..f8506c7 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ByteIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.tests + +class ByteIdClass { + byte id + + ByteIdClass(byte id) { + this.id = id + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ConfigurationTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ConfigurationTest.groovy new file mode 100644 index 0000000..6dc8f3a --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ConfigurationTest.groovy @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.groovy.tests + +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.Dialect +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Configuration` object + */ +@DisplayName('Groovy | Configuration') +class ConfigurationTest { + + @Test + @DisplayName('Default ID field is "id"') + void defaultIdField() { + assertEquals 'id', Configuration.idField, 'Default ID field incorrect' + } + + @Test + @DisplayName('Default Auto ID strategy is DISABLED') + void defaultAutoId() { + assertEquals AutoId.DISABLED, Configuration.autoIdStrategy, 'Default Auto ID strategy should be DISABLED' + } + + @Test + @DisplayName('Default ID string length should be 16') + void defaultIdStringLength() { + assertEquals 16, Configuration.idStringLength, 'Default ID string length should be 16' + } + + @Test + @DisplayName('Dialect is derived from connection string') + void dialectIsDerived() { + try { + assertThrows(DocumentException) { Configuration.dialect() } + Configuration.connectionString = 'jdbc:postgresql:db' + assertEquals Dialect.POSTGRESQL, Configuration.dialect() + } finally { + Configuration.connectionString = null + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/CountQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/CountQueryTest.groovy new file mode 100644 index 0000000..10f69f4 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/CountQueryTest.groovy @@ -0,0 +1,81 @@ +package solutions.bitbadger.documents.groovy.tests + +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 static Types.TEST_TABLE +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Count` object + */ +@DisplayName('Groovy | Query | CountQuery') +class CountQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('all generates correctly') + void all() { + assertEquals("SELECT COUNT(*) AS it FROM $TEST_TABLE".toString(), CountQuery.all(TEST_TABLE), + 'Count query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals("SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0".toString(), + CountQuery.byFields(TEST_TABLE, List.of(Field.equal('test', '', ':field0'))), + 'Count query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals("SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0".toString(), + CountQuery.byFields(TEST_TABLE, List.of(Field.equal('test', '', ':field0'))), + 'Count query not constructed correctly') + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals("SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria".toString(), + CountQuery.byContains(TEST_TABLE), 'Count query not constructed correctly') + } + + @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("SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + CountQuery.byJsonPath(TEST_TABLE), 'Count query not constructed correctly') + } + + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { CountQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DefinitionQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DefinitionQueryTest.groovy new file mode 100644 index 0000000..347d90c --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DefinitionQueryTest.groovy @@ -0,0 +1,127 @@ +package solutions.bitbadger.documents.groovy.tests + +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 static Types.TEST_TABLE +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Definition` object + */ +@DisplayName('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 IF NOT EXISTS my.table (data JSONB NOT NULL)', + DefinitionQuery.ensureTableFor('my.table', 'JSONB'), 'CREATE TABLE statement not constructed correctly') + } + + @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)) + } + + @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 UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", + DefinitionQuery.ensureKey('test.table', Dialect.POSTGRESQL), + 'CREATE INDEX for key statement with schema not constructed correctly') + } + + @Test + @DisplayName('ensureKey generates correctly without schema') + void ensureKeyWithoutSchema() { + assertEquals( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))".toString(), + DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE), + 'CREATE INDEX for key statement without schema not constructed correctly') + } + + @Test + @DisplayName('ensureIndexOn generates multiple fields and directions') + void ensureIndexOnMultipleFields() { + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", + DefinitionQuery.ensureIndexOn('test.table', 'gibberish', List.of('taco', 'guac DESC', 'salsa ASC'), + Dialect.POSTGRESQL), + 'CREATE INDEX for multiple field statement not constructed correctly') + } + + @Test + @DisplayName('ensureIndexOn generates nested field | PostgreSQL') + void ensureIndexOnNestedPostgres() { + assertEquals("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), + 'CREATE INDEX for nested PostgreSQL field incorrect') + } + + @Test + @DisplayName('ensureIndexOn generates nested field | SQLite') + void ensureIndexOnNestedSQLite() { + assertEquals( + "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), + 'CREATE INDEX for nested SQLite field incorrect') + } + + @Test + @DisplayName('ensureDocumentIndexOn generates Full | PostgreSQL') + void ensureDocumentIndexOnFullPostgres() { + ForceDialect.postgres() + assertEquals("CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)".toString(), + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL), + 'CREATE INDEX for full document index incorrect') + } + + @Test + @DisplayName('ensureDocumentIndexOn generates Optimized | PostgreSQL') + void ensureDocumentIndexOnOptimizedPostgres() { + ForceDialect.postgres() + assertEquals( + "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), + 'CREATE INDEX for optimized document index incorrect') + } + + @Test + @DisplayName('ensureDocumentIndexOn fails | SQLite') + void ensureDocumentIndexOnFailsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DeleteQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DeleteQueryTest.groovy new file mode 100644 index 0000000..c2a4128 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DeleteQueryTest.groovy @@ -0,0 +1,90 @@ +package solutions.bitbadger.documents.groovy.tests + +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 static Types.TEST_TABLE +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Delete` object + */ +@DisplayName('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 FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), DeleteQuery.byId(TEST_TABLE), + 'Delete query not constructed correctly') + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals("DELETE FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), DeleteQuery.byId(TEST_TABLE), + 'Delete query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals("DELETE FROM $TEST_TABLE WHERE data->>'a' = :b".toString(), + DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b'))), + 'Delete query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals("DELETE FROM $TEST_TABLE WHERE data->>'a' = :b".toString(), + DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b'))), + 'Delete query not constructed correctly') + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals("DELETE FROM $TEST_TABLE WHERE data @> :criteria".toString(), DeleteQuery.byContains(TEST_TABLE), + 'Delete query not constructed correctly') + } + + @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 FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + DeleteQuery.byJsonPath(TEST_TABLE), 'Delete query not constructed correctly') + } + + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { DeleteQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DialectTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DialectTest.groovy new file mode 100644 index 0000000..f1905ba --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DialectTest.groovy @@ -0,0 +1,42 @@ +package solutions.bitbadger.documents.groovy.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Dialect` enum + */ +@DisplayName('Groovy | Dialect') +class DialectTest { + + @Test + @DisplayName('deriveFromConnectionString derives PostgreSQL correctly') + void derivesPostgres() { + assertEquals(Dialect.POSTGRESQL, Dialect.deriveFromConnectionString('jdbc:postgresql:db'), + 'Dialect should have been PostgreSQL') + } + + @Test + @DisplayName('deriveFromConnectionString derives SQLite correctly') + void derivesSQLite() { + assertEquals(Dialect.SQLITE, Dialect.deriveFromConnectionString('jdbc:sqlite:memory'), + 'Dialect should have been SQLite') + } + + @Test + @DisplayName('deriveFromConnectionString fails when the connection string is unknown') + void deriveFailsWhenUnknown() { + try { + Dialect.deriveFromConnectionString 'SQL Server' + fail 'Dialect derivation should have failed' + } catch (DocumentException ex) { + assertNotNull ex.message, 'The exception message should not have been null' + assertTrue(ex.message.contains('[SQL Server]'), + 'The connection string should have been in the exception message') + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentIndexTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentIndexTest.groovy new file mode 100644 index 0000000..26f54d7 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentIndexTest.groovy @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.groovy.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentIndex + +import static org.junit.jupiter.api.Assertions.assertEquals + +/** + * Unit tests for the `DocumentIndex` enum + */ +@DisplayName('Groovy | DocumentIndex') +class DocumentIndexTest { + + @Test + @DisplayName('FULL uses proper SQL') + void fullSQL() { + assertEquals '', DocumentIndex.FULL.sql, 'The SQL for Full is incorrect' + } + + @Test + @DisplayName('OPTIMIZED uses proper SQL') + void optimizedSQL() { + assertEquals ' jsonb_path_ops', DocumentIndex.OPTIMIZED.sql, 'The SQL for Optimized is incorrect' + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentQueryTest.groovy new file mode 100644 index 0000000..d912547 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentQueryTest.groovy @@ -0,0 +1,135 @@ +package solutions.bitbadger.documents.groovy.tests + +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 static Types.TEST_TABLE +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Document` object + */ +@DisplayName('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.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\""), + "Query start not correct (actual: $query)") + assertTrue query.endsWith("\"}')"), 'Query end not correct' + } + + @Test + @DisplayName('insert generates auto UUID | SQLite') + void insertAutoUUIDSQLite() { + ForceDialect.sqlite() + def query = DocumentQuery.insert TEST_TABLE, AutoId.UUID + assertTrue(query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '"), + "Query start not correct (actual: $query)") + assertTrue query.endsWith("'))"), 'Query end not correct' + } + + @Test + @DisplayName('insert generates auto random string | PostgreSQL') + void insertAutoRandomPostgres() { + try { + ForceDialect.postgres() + Configuration.idStringLength = 8 + def query = DocumentQuery.insert TEST_TABLE, AutoId.RANDOM_STRING + assertTrue(query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\""), + "Query start not correct (actual: $query)") + assertTrue query.endsWith("\"}')"), 'Query end not correct' + assertEquals(8, + query.replace("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", '') + .replace("\"}')", '').length(), + 'Random string length incorrect') + } 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.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '"), + "Query start not correct (actual: $query)") + assertTrue query.endsWith("'))"), 'Query end not correct' + assertEquals(Configuration.idStringLength, + query.replace("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '", '').replace("'))", '') + .length(), + 'Random string length incorrect') + } + + @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 INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data" + .toString(), + DocumentQuery.save(TEST_TABLE), 'INSERT ON CONFLICT UPDATE statement not constructed correctly') + } + + @Test + @DisplayName('update generates successfully') + void update() { + assertEquals("UPDATE $TEST_TABLE SET data = :data".toString(), DocumentQuery.update(TEST_TABLE), + 'Update query not constructed correctly') + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldMatchTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldMatchTest.groovy new file mode 100644 index 0000000..2be46d8 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldMatchTest.groovy @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.groovy.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.FieldMatch + +import static org.junit.jupiter.api.Assertions.assertEquals + +/** + * Unit tests for the `FieldMatch` enum + */ +@DisplayName('Groovy | FieldMatch') +class FieldMatchTest { + + @Test + @DisplayName('ANY uses proper SQL') + void anySQL() { + assertEquals 'OR', FieldMatch.ANY.sql, 'ANY should use OR' + } + + @Test + @DisplayName('ALL uses proper SQL') + void allSQL() { + assertEquals 'AND', FieldMatch.ALL.sql, 'ALL should use AND' + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldTest.groovy new file mode 100644 index 0000000..82abe77 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldTest.groovy @@ -0,0 +1,612 @@ +package solutions.bitbadger.documents.groovy.tests + +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 static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Field` class + */ +@DisplayName('Groovy | Field') +class FieldTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + // ~~~ INSTANCE METHODS ~~~ + + @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 field, withParam, 'A new Field instance should have been created' + assertEquals field.name, withParam.name, 'Name should have been preserved' + assertEquals field.comparison, withParam.comparison, 'Comparison should have been preserved' + assertEquals ':test', withParam.parameterName, 'Parameter name not set correctly' + assertEquals field.qualifier, withParam.qualifier, 'Qualifier should have been preserved' + } + + @Test + @DisplayName('withParameterName works with at-sign prefix') + void withParamNameAtSign() { + def field = Field.equal 'def', '44' + def withParam = field.withParameterName '@unit' + assertNotSame field, withParam, 'A new Field instance should have been created' + assertEquals field.name, withParam.name, 'Name should have been preserved' + assertEquals field.comparison, withParam.comparison, 'Comparison should have been preserved' + assertEquals '@unit', withParam.parameterName, 'Parameter name not set correctly' + assertEquals field.qualifier, withParam.qualifier, 'Qualifier should have been preserved' + } + + @Test + @DisplayName('withQualifier sets qualifier correctly') + void withQualifier() { + def field = Field.equal 'j', 'k' + def withQual = field.withQualifier 'test' + assertNotSame field, withQual, 'A new Field instance should have been created' + assertEquals field.name, withQual.name, 'Name should have been preserved' + assertEquals field.comparison, withQual.comparison, 'Comparison should have been preserved' + assertEquals field.parameterName, withQual.parameterName, 'Parameter Name should have been preserved' + assertEquals 'test', withQual.qualifier, 'Qualifier not set correctly' + } + + @Test + @DisplayName('path generates for simple unqualified PostgreSQL field') + void pathPostgresSimpleUnqualified() { + assertEquals("data->>'SomethingCool'", + Field.greaterOrEqual('SomethingCool', 18).path(Dialect.POSTGRESQL, FieldFormat.SQL), 'Path not correct') + } + + @Test + @DisplayName('path generates for simple qualified PostgreSQL field') + void pathPostgresSimpleQualified() { + assertEquals("this.data->>'SomethingElse'", + Field.less('SomethingElse', 9).withQualifier('this').path(Dialect.POSTGRESQL, FieldFormat.SQL), + 'Path not correct') + } + + @Test + @DisplayName('path generates for nested unqualified PostgreSQL field') + void pathPostgresNestedUnqualified() { + assertEquals("data#>>'{My,Nested,Field}'", + Field.equal('My.Nested.Field', 'howdy').path(Dialect.POSTGRESQL, FieldFormat.SQL), 'Path not correct') + } + + @Test + @DisplayName('path generates for nested qualified PostgreSQL field') + void pathPostgresNestedQualified() { + assertEquals("bird.data#>>'{Nest,Away}'", + Field.equal('Nest.Away', 'doc').withQualifier('bird').path(Dialect.POSTGRESQL, FieldFormat.SQL), + 'Path not correct') + } + + @Test + @DisplayName('path generates for simple unqualified SQLite field') + void pathSQLiteSimpleUnqualified() { + assertEquals("data->>'SomethingCool'", + Field.greaterOrEqual('SomethingCool', 18).path(Dialect.SQLITE, FieldFormat.SQL), 'Path not correct') + } + + @Test + @DisplayName('path generates for simple qualified SQLite field') + void pathSQLiteSimpleQualified() { + assertEquals("this.data->>'SomethingElse'", + Field.less('SomethingElse', 9).withQualifier('this').path(Dialect.SQLITE, FieldFormat.SQL), + 'Path not correct') + } + + @Test + @DisplayName('path generates for nested unqualified SQLite field') + void pathSQLiteNestedUnqualified() { + assertEquals("data->'My'->'Nested'->>'Field'", + Field.equal('My.Nested.Field', 'howdy').path(Dialect.SQLITE, FieldFormat.SQL), 'Path not correct') + } + + @Test + @DisplayName('path generates for nested qualified SQLite field') + void pathSQLiteNestedQualified() { + assertEquals("bird.data->'Nest'->>'Away'", + Field.equal('Nest.Away', 'doc').withQualifier('bird').path(Dialect.SQLITE, FieldFormat.SQL), + 'Path not correct') + } + + @Test + @DisplayName('toWhere generates for exists w/o qualifier | PostgreSQL') + void toWhereExistsNoQualPostgres() { + ForceDialect.postgres() + assertEquals("data->>'that_field' IS NOT NULL", Field.exists('that_field').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for exists w/o qualifier | SQLite') + void toWhereExistsNoQualSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'that_field' IS NOT NULL", Field.exists('that_field').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for not-exists w/o qualifier | PostgreSQL') + void toWhereNotExistsNoQualPostgres() { + ForceDialect.postgres() + assertEquals("data->>'a_field' IS NULL", Field.notExists('a_field').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for not-exists w/o qualifier | SQLite') + void toWhereNotExistsNoQualSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'a_field' IS NULL", Field.notExists('a_field').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier, numeric range | PostgreSQL') + void toWhereBetweenNoQualNumericPostgres() { + ForceDialect.postgres() + assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').toWhere(), 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier, alphanumeric range | PostgreSQL') + void toWhereBetweenNoQualAlphaPostgres() { + ForceDialect.postgres() + assertEquals("data->>'city' BETWEEN :citymin AND :citymax", + Field.between('city', 'Atlanta', 'Chicago', ':city').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier | SQLite') + void toWhereBetweenNoQualSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field.between('age', 13, 17, '@age').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier, numeric range | PostgreSQL') + void toWhereBetweenQualNumericPostgres() { + ForceDialect.postgres() + assertEquals("(test.data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').withQualifier('test').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier, alphanumeric range | PostgreSQL') + void toWhereBetweenQualAlphaPostgres() { + ForceDialect.postgres() + assertEquals("unit.data->>'city' BETWEEN :citymin AND :citymax", + Field.between('city', 'Atlanta', 'Chicago', ':city').withQualifier('unit').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier | SQLite') + void toWhereBetweenQualSQLite() { + ForceDialect.sqlite() + assertEquals("my.data->>'age' BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').withQualifier('my').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for IN/any, numeric values | PostgreSQL') + void toWhereAnyNumericPostgres() { + ForceDialect.postgres() + assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", + Field.any('even', List.of(2, 4, 6), ':nbr').toWhere(), 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for IN/any, alphanumeric values | PostgreSQL') + void toWhereAnyAlphaPostgres() { + ForceDialect.postgres() + assertEquals("data->>'test' IN (:city_0, :city_1)", + Field.any('test', List.of('Atlanta', 'Chicago'), ':city').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for IN/any | SQLite') + void toWhereAnySQLite() { + ForceDialect.sqlite() + assertEquals("data->>'test' IN (:city_0, :city_1)", + Field.any('test', List.of('Atlanta', 'Chicago'), ':city').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for inArray | PostgreSQL') + void toWhereInArrayPostgres() { + ForceDialect.postgres() + assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]", + Field.inArray('even', 'tbl', List.of(2, 4, 6, 8), ':it').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for inArray | SQLite') + void toWhereInArraySQLite() { + ForceDialect.sqlite() + assertEquals("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(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for others w/o qualifier | PostgreSQL') + void toWhereOtherNoQualPostgres() { + ForceDialect.postgres() + assertEquals("data->>'some_field' = :value", Field.equal('some_field', '', ':value').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates for others w/o qualifier | SQLite') + void toWhereOtherNoQualSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'some_field' = :value", Field.equal('some_field', '', ':value').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates no-parameter w/ qualifier | PostgreSQL') + void toWhereNoParamWithQualPostgres() { + ForceDialect.postgres() + assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists('no_field').withQualifier('test').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates no-parameter w/ qualifier | SQLite') + void toWhereNoParamWithQualSQLite() { + ForceDialect.sqlite() + assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists('no_field').withQualifier('test').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates parameter w/ qualifier | PostgreSQL') + void toWhereParamWithQualPostgres() { + ForceDialect.postgres() + assertEquals("(q.data->>'le_field')::numeric <= :it", + Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere(), + 'Field WHERE clause not generated correctly') + } + + @Test + @DisplayName('toWhere generates parameter w/ qualifier | SQLite') + void toWhereParamWithQualSQLite() { + ForceDialect.sqlite() + assertEquals("q.data->>'le_field' <= :it", + Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere(), + 'Field WHERE clause not generated correctly') + } + + // ~~~ STATIC CONSTRUCTOR TESTS ~~~ + + @Test + @DisplayName('equal constructs a field w/o parameter name') + void equalCtor() { + def field = Field.equal 'Test', 14 + assertEquals 'Test', field.name, 'Field name not filled correctly' + assertEquals Op.EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 14, field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('equal constructs a field w/ parameter name') + void equalParameterCtor() { + def field = Field.equal 'Test', 14, ':w' + assertEquals 'Test', field.name, 'Field name not filled correctly' + assertEquals Op.EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 14, field.comparison.value, 'Field comparison value not filled correctly' + assertEquals ':w', field.parameterName, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('greater constructs a field w/o parameter name') + void greaterCtor() { + def field = Field.greater 'Great', 'night' + assertEquals 'Great', field.name, 'Field name not filled correctly' + assertEquals Op.GREATER, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'night', field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('greater constructs a field w/ parameter name') + void greaterParameterCtor() { + def field = Field.greater 'Great', 'night', ':yeah' + assertEquals 'Great', field.name, 'Field name not filled correctly' + assertEquals Op.GREATER, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'night', field.comparison.value, 'Field comparison value not filled correctly' + assertEquals ':yeah', field.parameterName, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('greaterOrEqual constructs a field w/o parameter name') + void greaterOrEqualCtor() { + def field = Field.greaterOrEqual 'Nice', 88L + assertEquals 'Nice', field.name, 'Field name not filled correctly' + assertEquals Op.GREATER_OR_EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 88L, field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('greaterOrEqual constructs a field w/ parameter name') + void greaterOrEqualParameterCtor() { + def field = Field.greaterOrEqual 'Nice', 88L, ':nice' + assertEquals 'Nice', field.name, 'Field name not filled correctly' + assertEquals Op.GREATER_OR_EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 88L, field.comparison.value, 'Field comparison value not filled correctly' + assertEquals ':nice', field.parameterName, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('less constructs a field w/o parameter name') + void lessCtor() { + def field = Field.less 'Lesser', 'seven' + assertEquals 'Lesser', field.name, 'Field name not filled correctly' + assertEquals Op.LESS, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'seven', field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('less constructs a field w/ parameter name') + void lessParameterCtor() { + def field = Field.less 'Lesser', 'seven', ':max' + assertEquals 'Lesser', field.name, 'Field name not filled correctly' + assertEquals Op.LESS, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'seven', field.comparison.value, 'Field comparison value not filled correctly' + assertEquals ':max', field.parameterName, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('lessOrEqual constructs a field w/o parameter name') + void lessOrEqualCtor() { + def field = Field.lessOrEqual 'Nobody', 'KNOWS' + assertEquals 'Nobody', field.name, 'Field name not filled correctly' + assertEquals Op.LESS_OR_EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'KNOWS', field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('lessOrEqual constructs a field w/ parameter name') + void lessOrEqualParameterCtor() { + def field = Field.lessOrEqual 'Nobody', 'KNOWS', ':nope' + assertEquals 'Nobody', field.name, 'Field name not filled correctly' + assertEquals Op.LESS_OR_EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'KNOWS', field.comparison.value, 'Field comparison value not filled correctly' + assertEquals ':nope', field.parameterName, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('notEqual constructs a field w/o parameter name') + void notEqualCtor() { + def field = Field.notEqual 'Park', 'here' + assertEquals 'Park', field.name, 'Field name not filled correctly' + assertEquals Op.NOT_EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'here', field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('notEqual constructs a field w/ parameter name') + void notEqualParameterCtor() { + def field = Field.notEqual 'Park', 'here', ':now' + assertEquals 'Park', field.name, 'Field name not filled correctly' + assertEquals Op.NOT_EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'here', field.comparison.value, 'Field comparison value not filled correctly' + assertEquals ':now', field.parameterName, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('between constructs a field w/o parameter name') + void betweenCtor() { + def field = Field.between 'Age', 18, 49 + assertEquals 'Age', field.name, 'Field name not filled correctly' + 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' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('between constructs a field w/ parameter name') + void betweenParameterCtor() { + def field = Field.between 'Age', 18, 49, ':limit' + assertEquals 'Age', field.name, 'Field name not filled correctly' + 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, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('any constructs a field w/o parameter name') + void anyCtor() { + def field = Field.any 'Here', List.of(8, 16, 32) + assertEquals 'Here', field.name, 'Field name not filled correctly' + assertEquals Op.IN, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals List.of(8, 16, 32), field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('any constructs a field w/ parameter name') + void anyParameterCtor() { + def field = Field.any 'Here', List.of(8, 16, 32), ':list' + assertEquals 'Here', field.name, 'Field name not filled correctly' + assertEquals Op.IN, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals List.of(8, 16, 32), field.comparison.value, 'Field comparison value not filled correctly' + assertEquals ':list', field.parameterName, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('inArray constructs a field w/o parameter name') + void inArrayCtor() { + def field = Field.inArray 'ArrayField', 'table', List.of('z') + assertEquals 'ArrayField', field.name, 'Field name not filled correctly' + assertEquals Op.IN_ARRAY, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'table', field.comparison.value.first, 'Field comparison table not filled correctly' + assertEquals List.of('z'), field.comparison.value.second, 'Field comparison values not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('inArray constructs a field w/ parameter name') + void inArrayParameterCtor() { + def field = Field.inArray 'ArrayField', 'table', List.of('z'), ':a' + assertEquals 'ArrayField', field.name, 'Field name not filled correctly' + assertEquals Op.IN_ARRAY, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals 'table', field.comparison.value.first, 'Field comparison table not filled correctly' + assertEquals List.of('z'), field.comparison.value.second, 'Field comparison values not filled correctly' + assertEquals ':a', field.parameterName, 'Field parameter name not filled correctly' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('exists constructs a field') + void existsCtor() { + def field = Field.exists 'Groovy' + assertEquals 'Groovy', field.name, 'Field name not filled correctly' + assertEquals Op.EXISTS, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals '', field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('notExists constructs a field') + void notExistsCtor() { + def field = Field.notExists 'Groovy' + assertEquals 'Groovy', field.name, 'Field name not filled correctly' + assertEquals Op.NOT_EXISTS, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals '', field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @Test + @DisplayName('named constructs a field') + void namedCtor() { + def field = Field.named 'Tacos' + assertEquals 'Tacos', field.name, 'Field name not filled correctly' + assertEquals Op.EQUAL, field.comparison.op, 'Field comparison operation not filled correctly' + assertEquals '', field.comparison.value, 'Field comparison value not filled correctly' + assertNull field.parameterName, 'The parameter name should have been null' + assertNull field.qualifier, 'The qualifier should have been null' + } + + @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("data->>'Simple'", Field.nameToPath('Simple', Dialect.POSTGRESQL, FieldFormat.SQL), + 'Path not constructed correctly') + } + + @Test + @DisplayName('nameToPath creates a simple SQLite SQL name') + void nameToPathSQLiteSimpleSQL() { + assertEquals("data->>'Simple'", Field.nameToPath('Simple', Dialect.SQLITE, FieldFormat.SQL), + 'Path not constructed correctly') + } + + @Test + @DisplayName('nameToPath creates a nested PostgreSQL SQL name') + void nameToPathPostgresNestedSQL() { + assertEquals("data#>>'{A,Long,Path,to,the,Property}'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.POSTGRESQL, FieldFormat.SQL), + 'Path not constructed correctly') + } + + @Test + @DisplayName('nameToPath creates a nested SQLite SQL name') + void nameToPathSQLiteNestedSQL() { + assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.SQLITE, FieldFormat.SQL), + 'Path not constructed correctly') + } + + @Test + @DisplayName('nameToPath creates a simple PostgreSQL JSON name') + void nameToPathPostgresSimpleJSON() { + assertEquals("data->'Simple'", Field.nameToPath('Simple', Dialect.POSTGRESQL, FieldFormat.JSON), + 'Path not constructed correctly') + } + + @Test + @DisplayName('nameToPath creates a simple SQLite JSON name') + void nameToPathSQLiteSimpleJSON() { + assertEquals("data->'Simple'", Field.nameToPath('Simple', Dialect.SQLITE, FieldFormat.JSON), + 'Path not constructed correctly') + } + + @Test + @DisplayName('nameToPath creates a nested PostgreSQL JSON name') + void nameToPathPostgresNestedJSON() { + assertEquals("data#>'{A,Long,Path,to,the,Property}'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.POSTGRESQL, FieldFormat.JSON), + 'Path not constructed correctly') + } + + @Test + @DisplayName('nameToPath creates a nested SQLite JSON name') + void nameToPathSQLiteNestedJSON() { + assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.SQLITE, FieldFormat.JSON), + 'Path not constructed correctly') + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FindQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FindQueryTest.groovy new file mode 100644 index 0000000..bec1d7b --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FindQueryTest.groovy @@ -0,0 +1,97 @@ +package solutions.bitbadger.documents.groovy.tests + +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 static Types.TEST_TABLE +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Find` object + */ +@DisplayName('Groovy | Query | FindQuery') +class FindQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('all generates correctly') + void all() { + assertEquals("SELECT data FROM $TEST_TABLE".toString(), FindQuery.all(TEST_TABLE), + 'Find query not constructed correctly') + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals("SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), FindQuery.byId(TEST_TABLE), + 'Find query not constructed correctly') + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals("SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), FindQuery.byId(TEST_TABLE), + 'Find query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals("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'))), + 'Find query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals("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'))), + 'Find query not constructed correctly') + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals("SELECT data FROM $TEST_TABLE WHERE data @> :criteria".toString(), + FindQuery.byContains(TEST_TABLE), 'Find query not constructed correctly') + } + + @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("SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + FindQuery.byJsonPath(TEST_TABLE), 'Find query not constructed correctly') + } + + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { FindQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ForceDialect.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ForceDialect.groovy new file mode 100644 index 0000000..5784dd9 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ForceDialect.groovy @@ -0,0 +1,19 @@ +package solutions.bitbadger.documents.groovy.tests + +import solutions.bitbadger.documents.Configuration + +final class ForceDialect { + + static void postgres() { + Configuration.connectionString = ":postgresql:" + } + + static void sqlite() { + Configuration.connectionString = ":sqlite:" + } + + static void none() { + Configuration.connectionString = null + } + +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/IntIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/IntIdClass.groovy new file mode 100644 index 0000000..867b0ff --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/IntIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.tests + +class IntIdClass { + int id + + IntIdClass(int id) { + this.id = id + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/LongIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/LongIdClass.groovy new file mode 100644 index 0000000..a6b1f07 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/LongIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.tests + +class LongIdClass { + long id + + LongIdClass(long id) { + this.id = id + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/OpTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/OpTest.groovy new file mode 100644 index 0000000..84c2d9e --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/OpTest.groovy @@ -0,0 +1,80 @@ +package solutions.bitbadger.documents.groovy.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Op + +import static org.junit.jupiter.api.Assertions.assertEquals + +/** + * Unit tests for the `Op` enum + */ +@DisplayName('Groovy | Op') +class OpTest { + + @Test + @DisplayName('EQUAL uses proper SQL') + void equalSQL() { + assertEquals '=', Op.EQUAL.sql, 'The SQL for equal is incorrect' + } + + @Test + @DisplayName('GREATER uses proper SQL') + void greaterSQL() { + assertEquals '>', Op.GREATER.sql, 'The SQL for greater is incorrect' + } + + @Test + @DisplayName('GREATER_OR_EQUAL uses proper SQL') + void greaterOrEqualSQL() { + assertEquals '>=', Op.GREATER_OR_EQUAL.sql, 'The SQL for greater-or-equal is incorrect' + } + + @Test + @DisplayName('LESS uses proper SQL') + void lessSQL() { + assertEquals '<', Op.LESS.sql, 'The SQL for less is incorrect' + } + + @Test + @DisplayName('LESS_OR_EQUAL uses proper SQL') + void lessOrEqualSQL() { + assertEquals '<=', Op.LESS_OR_EQUAL.sql, 'The SQL for less-or-equal is incorrect' + } + + @Test + @DisplayName('NOT_EQUAL uses proper SQL') + void notEqualSQL() { + assertEquals '<>', Op.NOT_EQUAL.sql, 'The SQL for not-equal is incorrect' + } + + @Test + @DisplayName('BETWEEN uses proper SQL') + void betweenSQL() { + assertEquals 'BETWEEN', Op.BETWEEN.sql, 'The SQL for between is incorrect' + } + + @Test + @DisplayName('IN uses proper SQL') + void inSQL() { + assertEquals 'IN', Op.IN.sql, 'The SQL for in is incorrect' + } + + @Test + @DisplayName('IN_ARRAY uses proper SQL') + void inArraySQL() { + assertEquals '??|', Op.IN_ARRAY.sql, 'The SQL for in-array is incorrect' + } + + @Test + @DisplayName('EXISTS uses proper SQL') + void existsSQL() { + assertEquals 'IS NOT NULL', Op.EXISTS.sql, 'The SQL for exists is incorrect' + } + + @Test + @DisplayName('NOT_EXISTS uses proper SQL') + void notExistsSQL() { + assertEquals 'IS NULL', Op.NOT_EXISTS.sql, 'The SQL for not-exists is incorrect' + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterNameTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterNameTest.groovy new file mode 100644 index 0000000..947dac9 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterNameTest.groovy @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.groovy.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.ParameterName + +import static org.junit.jupiter.api.Assertions.assertEquals + +/** + * Unit tests for the `ParameterName` class + */ +@DisplayName('Groovy | ParameterName') +class ParameterNameTest { + + @Test + @DisplayName('derive works when given existing names') + void withExisting() { + def names = new ParameterName() + assertEquals ':taco', names.derive(':taco'), 'Name should have been :taco' + assertEquals ':field0', names.derive(null), 'Counter should not have advanced for named field' + } + + @Test + @DisplayName('derive works when given all anonymous fields') + void allAnonymous() { + def names = new ParameterName() + assertEquals ':field0', names.derive(null), 'Anonymous field name should have been returned' + assertEquals ':field1', names.derive(null), 'Counter should have advanced from previous call' + assertEquals ':field2', names.derive(null), 'Counter should have advanced from previous call' + assertEquals ':field3', names.derive(null), 'Counter should have advanced from previous call' + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterTest.groovy new file mode 100644 index 0000000..83d373a --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterTest.groovy @@ -0,0 +1,40 @@ +package solutions.bitbadger.documents.groovy.tests + +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 org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Parameter` class + */ +@DisplayName('Groovy | Parameter') +class ParameterTest { + + @Test + @DisplayName('Construction with colon-prefixed name') + void ctorWithColon() { + def p = new Parameter(':test', ParameterType.STRING, 'ABC') + assertEquals ':test', p.name, 'Parameter name was incorrect' + assertEquals ParameterType.STRING, p.type, 'Parameter type was incorrect' + assertEquals 'ABC', p.value, 'Parameter value was incorrect' + } + + @Test + @DisplayName('Construction with at-sign-prefixed name') + void ctorWithAtSign() { + def p = new Parameter('@yo', ParameterType.NUMBER, null) + assertEquals '@yo', p.name, 'Parameter name was incorrect' + assertEquals ParameterType.NUMBER, p.type, 'Parameter type was incorrect' + assertNull p.value, 'Parameter value was incorrect' + } + + @Test + @DisplayName('Construction fails with incorrect prefix') + void ctorFailsForPrefix() { + assertThrows(DocumentException) { new Parameter('it', ParameterType.JSON, '') } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParametersTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParametersTest.groovy new file mode 100644 index 0000000..499a2d1 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParametersTest.groovy @@ -0,0 +1,119 @@ +package solutions.bitbadger.documents.groovy.tests + +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.Parameter +import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.java.Parameters + +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Parameters` object + */ +@DisplayName('Groovy | Parameters') +class ParametersTest { + + /** + * Reset the dialect + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('nameFields works with no changes') + void nameFieldsNoChange() { + def fields = List.of(Field.equal('a', '', ':test'), Field.exists('q'), Field.equal('b', '', ':me')) + def named = Parameters.nameFields(fields).toList() + assertEquals fields.size(), named.size(), 'There should have been 3 fields in the list' + assertSame fields[0], named[0], 'The first field should be the same' + assertSame fields[1], named[1], 'The second field should be the same' + assertSame fields[2], named[2], 'The third field should be the same' + } + + @Test + @DisplayName('nameFields works when changing fields') + void nameFieldsChange() { + def fields = List.of(Field.equal('a', ''), Field.equal('e', '', ':hi'), Field.equal('b', ''), + Field.notExists('z')) + def named = Parameters.nameFields(fields).toList() + assertEquals fields.size(), named.size(), 'There should have been 4 fields in the list' + assertNotSame fields[0], named[0], 'The first field should not be the same' + assertEquals ':field0', named[0].parameterName, 'First parameter name incorrect' + assertSame fields[1], named[1], 'The second field should be the same' + assertNotSame fields[2], named[2], 'The third field should not be the same' + assertEquals ':field1', named[2].parameterName, 'Third parameter name incorrect' + assertSame fields[3], named[3], 'The fourth field should be the same' + } + + @Test + @DisplayName('replaceNamesInQuery replaces successfully') + void replaceNamesInQuery() { + def parameters = List.of(new Parameter(':data', ParameterType.JSON, '{}'), + new Parameter(':data_ext', ParameterType.STRING, '')) + def query = 'SELECT data, data_ext FROM tbl WHERE data = :data AND data_ext = :data_ext AND more_data = :data' + assertEquals('SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?', + Parameters.replaceNamesInQuery(query, parameters), 'Parameters not replaced correctly') + } + + @Test + @DisplayName('fieldNames generates a single parameter (PostgreSQL)') + void fieldNamesSinglePostgres() { + ForceDialect.postgres() + def nameParams = Parameters.fieldNames(List.of('test')).toList() + assertEquals 1, nameParams.size(), 'There should be one name parameter' + assertEquals ':name', nameParams[0].name, 'The parameter name is incorrect' + assertEquals ParameterType.STRING, nameParams[0].type, 'The parameter type is incorrect' + assertEquals '{test}', nameParams[0].value, 'The parameter value is incorrect' + } + + @Test + @DisplayName('fieldNames generates multiple parameters (PostgreSQL)') + void fieldNamesMultiplePostgres() { + ForceDialect.postgres() + def nameParams = Parameters.fieldNames(List.of('test', 'this', 'today')).toList() + assertEquals 1, nameParams.size(), 'There should be one name parameter' + assertEquals ':name', nameParams[0].name, 'The parameter name is incorrect' + assertEquals ParameterType.STRING, nameParams[0].type, 'The parameter type is incorrect' + assertEquals '{test,this,today}', nameParams[0].value, 'The parameter value is incorrect' + } + + @Test + @DisplayName('fieldNames generates a single parameter (SQLite)') + void fieldNamesSingleSQLite() { + ForceDialect.sqlite() + def nameParams = Parameters.fieldNames(List.of('test')).toList() + assertEquals 1, nameParams.size(), 'There should be one name parameter' + assertEquals ':name0', nameParams[0].name, 'The parameter name is incorrect' + assertEquals ParameterType.STRING, nameParams[0].type, 'The parameter type is incorrect' + assertEquals 'test', nameParams[0].value, 'The parameter value is incorrect' + } + + @Test + @DisplayName('fieldNames generates multiple parameters (SQLite)') + void fieldNamesMultipleSQLite() { + ForceDialect.sqlite() + def nameParams = Parameters.fieldNames(List.of('test', 'this', 'today')).toList() + assertEquals 3, nameParams.size(), 'There should be one name parameter' + assertEquals ':name0', nameParams[0].name, 'The first parameter name is incorrect' + assertEquals ParameterType.STRING, nameParams[0].type, 'The first parameter type is incorrect' + assertEquals 'test', nameParams[0].value, 'The first parameter value is incorrect' + assertEquals ':name1', nameParams[1].name, 'The second parameter name is incorrect' + assertEquals ParameterType.STRING, nameParams[1].type, 'The second parameter type is incorrect' + assertEquals 'this', nameParams[1].value, 'The second parameter value is incorrect' + assertEquals ':name2', nameParams[2].name, 'The third parameter name is incorrect' + assertEquals ParameterType.STRING, nameParams[2].type, 'The third parameter type is incorrect' + assertEquals 'today', nameParams[2].value, 'The third parameter value is incorrect' + } + + @Test + @DisplayName('fieldNames fails if dialect not set') + void fieldNamesFails() { + assertThrows(DocumentException) { Parameters.fieldNames(List.of()) } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/PatchQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/PatchQueryTest.groovy new file mode 100644 index 0000000..0a00a6f --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/PatchQueryTest.groovy @@ -0,0 +1,91 @@ +package solutions.bitbadger.documents.groovy.tests + +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 static Types.TEST_TABLE +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Patch` object + */ +@DisplayName('Groovy | Query | PatchQuery') +class PatchQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals("UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id".toString(), + PatchQuery.byId(TEST_TABLE), 'Patch query not constructed correctly') + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals("UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id".toString(), + PatchQuery.byId(TEST_TABLE), 'Patch query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals("UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y".toString(), + PatchQuery.byFields(TEST_TABLE, List.of(Field.equal('z', '', ':y'))), + 'Patch query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals("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'))), + 'Patch query not constructed correctly') + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals("UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria".toString(), + PatchQuery.byContains(TEST_TABLE), 'Patch query not constructed correctly') + } + + @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( + "UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + PatchQuery.byJsonPath(TEST_TABLE), 'Patch query not constructed correctly') + } + + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { PatchQuery.byJsonPath(TEST_TABLE) } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/QueryUtilsTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/QueryUtilsTest.groovy new file mode 100644 index 0000000..300eccf --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/QueryUtilsTest.groovy @@ -0,0 +1,162 @@ +package solutions.bitbadger.documents.groovy.tests + +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 static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `QueryUtils` class + */ +@DisplayName('Groovy | Query | QueryUtils') +class QueryUtilsTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('statementWhere generates correctly') + void statementWhere() { + assertEquals 'x WHERE y', QueryUtils.statementWhere('x', 'y'), 'Statements not combined correctly' + } + + @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('', QueryUtils.orderBy(List.of(), Dialect.POSTGRESQL), + 'ORDER BY should have been blank (PostgreSQL)') + assertEquals '', QueryUtils.orderBy(List.of(), Dialect.SQLITE), 'ORDER BY should have been blank (SQLite)' + } + + @Test + @DisplayName('orderBy generates single, no direction | PostgreSQL') + void orderBySinglePostgres() { + assertEquals(" ORDER BY data->>'TestField'", + QueryUtils.orderBy(List.of(Field.named('TestField')), Dialect.POSTGRESQL), + 'ORDER BY not constructed correctly') + } + + @Test + @DisplayName('orderBy generates single, no direction | SQLite') + void orderBySingleSQLite() { + assertEquals(" ORDER BY data->>'TestField'", + QueryUtils.orderBy(List.of(Field.named('TestField')), Dialect.SQLITE), + 'ORDER BY not constructed correctly') + } + + @Test + @DisplayName('orderBy generates multiple with direction | PostgreSQL') + void orderByMultiplePostgres() { + assertEquals(" 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), + 'ORDER BY not constructed correctly') + } + + @Test + @DisplayName('orderBy generates multiple with direction | SQLite') + void orderByMultipleSQLite() { + assertEquals(" 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), + 'ORDER BY not constructed correctly') + } + + @Test + @DisplayName('orderBy generates numeric ordering | PostgreSQL') + void orderByNumericPostgres() { + assertEquals(" ORDER BY (data->>'Test')::numeric", + QueryUtils.orderBy(List.of(Field.named('n:Test')), Dialect.POSTGRESQL), + 'ORDER BY not constructed correctly') + } + + @Test + @DisplayName('orderBy generates numeric ordering | SQLite') + void orderByNumericSQLite() { + assertEquals(" ORDER BY data->>'Test'", QueryUtils.orderBy(List.of(Field.named('n:Test')), Dialect.SQLITE), + 'ORDER BY not constructed correctly') + } + + @Test + @DisplayName('orderBy generates case-insensitive ordering | PostgreSQL') + void orderByCIPostgres() { + assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", + QueryUtils.orderBy(List.of(Field.named('i:Test.Field DESC NULLS FIRST')), Dialect.POSTGRESQL), + 'ORDER BY not constructed correctly') + } + + @Test + @DisplayName('orderBy generates case-insensitive ordering | SQLite') + void orderByCISQLite() { + assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", + QueryUtils.orderBy(List.of(Field.named('i:Test.Field ASC NULLS LAST')), Dialect.SQLITE), + 'ORDER BY not constructed correctly') + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/RemoveFieldsQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/RemoveFieldsQueryTest.groovy new file mode 100644 index 0000000..0c4c5e2 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/RemoveFieldsQueryTest.groovy @@ -0,0 +1,106 @@ +package solutions.bitbadger.documents.groovy.tests + +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.Parameter +import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.query.RemoveFieldsQuery + +import static Types.TEST_TABLE +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `RemoveFields` object + */ +@DisplayName('Groovy | Query | RemoveFieldsQuery') +class RemoveFieldsQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals("UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data->>'id' = :id".toString(), + RemoveFieldsQuery.byId(TEST_TABLE, List.of(new Parameter(':name', ParameterType.STRING, '{a,z}'))), + 'Remove Fields query not constructed correctly') + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals( + "UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id".toString(), + RemoveFieldsQuery.byId(TEST_TABLE, List.of(new Parameter(':name0', ParameterType.STRING, 'a'), + new Parameter(':name1', ParameterType.STRING, 'z'))), + 'Remove Field query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals("UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data->>'f' > :g".toString(), + RemoveFieldsQuery.byFields(TEST_TABLE, List.of(new Parameter(':name', ParameterType.STRING, '{b,c}')), + List.of(Field.greater('f', '', ':g'))), + 'Remove Field query not constructed correctly') + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals("UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g".toString(), + RemoveFieldsQuery.byFields(TEST_TABLE, + List.of(new Parameter(':name0', ParameterType.STRING, 'b'), + new Parameter(':name1', ParameterType.STRING, 'c')), + List.of(Field.greater('f', '', ':g'))), + 'Remove Field query not constructed correctly') + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals("UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data @> :criteria".toString(), + RemoveFieldsQuery.byContains(TEST_TABLE, + List.of(new Parameter(':name', ParameterType.STRING, '{m,n}'))), + 'Remove Field query not constructed correctly') + } + + @Test + @DisplayName('byContains fails | SQLite') + void byContainsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { RemoveFieldsQuery.byContains(TEST_TABLE, List.of()) } + } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals( + "UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)" + .toString(), + RemoveFieldsQuery.byJsonPath(TEST_TABLE, + List.of(new Parameter(':name', ParameterType.STRING, '{o,p}'))), + 'Remove Field query not constructed correctly') + } + + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { RemoveFieldsQuery.byJsonPath(TEST_TABLE, List.of()) } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ShortIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ShortIdClass.groovy new file mode 100644 index 0000000..bf03a6a --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ShortIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.tests + +class ShortIdClass { + short id + + ShortIdClass(short id) { + this.id = id + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/StringIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/StringIdClass.groovy new file mode 100644 index 0000000..544cf37 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/StringIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.tests + +class StringIdClass { + String id + + StringIdClass(String id) { + this.id = id + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/Types.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/Types.groovy new file mode 100644 index 0000000..5d2f67e --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/Types.groovy @@ -0,0 +1,6 @@ +package solutions.bitbadger.documents.groovy.tests + +final class Types { + + public static final String TEST_TABLE = 'test_table' +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/WhereTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/WhereTest.groovy new file mode 100644 index 0000000..170b6a6 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/WhereTest.groovy @@ -0,0 +1,172 @@ +package solutions.bitbadger.documents.groovy.tests + +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.FieldMatch +import solutions.bitbadger.documents.query.Where + +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `Where` object + */ +@DisplayName('Groovy | Query | Where') +class WhereTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('byFields is blank when given no fields') + void byFieldsBlankIfEmpty() { + assertEquals '', Where.byFields(List.of()) + } + + @Test + @DisplayName('byFields generates one numeric field | PostgreSQL') + void byFieldsOneFieldPostgres() { + ForceDialect.postgres() + assertEquals "(data->>'it')::numeric = :that", Where.byFields(List.of(Field.equal('it', 9, ':that'))) + } + + @Test + @DisplayName('byFields generates one alphanumeric field | PostgreSQL') + void byFieldsOneAlphaFieldPostgres() { + ForceDialect.postgres() + assertEquals "data->>'it' = :that", Where.byFields(List.of(Field.equal('it', '', ':that'))) + } + + @Test + @DisplayName('byFields generates one field | SQLite') + void byFieldsOneFieldSQLite() { + ForceDialect.sqlite() + assertEquals "data->>'it' = :that", Where.byFields(List.of(Field.equal('it', '', ':that'))) + } + + @Test + @DisplayName('byFields generates multiple fields w/ default match | PostgreSQL') + void byFieldsMultipleDefaultPostgres() { + ForceDialect.postgres() + assertEquals("data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three", + Where.byFields( + List.of(Field.equal('1', '', ':one'), Field.equal('2', 0L, ':two'), + Field.equal('3', '', ':three')))) + } + + @Test + @DisplayName('byFields generates multiple fields w/ default match | SQLite') + void byFieldsMultipleDefaultSQLite() { + ForceDialect.sqlite() + assertEquals("data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three", + Where.byFields( + List.of(Field.equal('1', '', ':one'), Field.equal('2', 0L, ':two'), + Field.equal('3', '', ':three')))) + } + + @Test + @DisplayName('byFields generates multiple fields w/ ANY match | PostgreSQL') + void byFieldsMultipleAnyPostgres() { + ForceDialect.postgres() + assertEquals("data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three", + Where.byFields( + List.of(Field.equal('1', '', ':one'), Field.equal('2', 0L, ':two'), + Field.equal('3', '', ':three')), + FieldMatch.ANY)) + } + + @Test + @DisplayName('byFields generates multiple fields w/ ANY match | SQLite') + void byFieldsMultipleAnySQLite() { + ForceDialect.sqlite() + assertEquals("data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three", + Where.byFields( + List.of(Field.equal('1', '', ':one'), Field.equal('2', 0L, ':two'), + Field.equal('3', '', ':three')), + FieldMatch.ANY)) + } + + @Test + @DisplayName('byId generates defaults for alphanumeric key | PostgreSQL') + void byIdDefaultAlphaPostgres() { + ForceDialect.postgres() + assertEquals "data->>'id' = :id", Where.byId() + } + + @Test + @DisplayName('byId generates defaults for numeric key | PostgreSQL') + void byIdDefaultNumericPostgres() { + ForceDialect.postgres() + assertEquals "(data->>'id')::numeric = :id", Where.byId(":id", 5) + } + + @Test + @DisplayName('byId generates defaults | SQLite') + void byIdDefaultSQLite() { + ForceDialect.sqlite() + assertEquals "data->>'id' = :id", Where.byId() + } + + @Test + @DisplayName('byId generates named ID | PostgreSQL') + void byIdDefaultNamedPostgres() { + ForceDialect.postgres() + assertEquals "data->>'id' = :key", Where.byId(':key') + } + + @Test + @DisplayName('byId generates named ID | SQLite') + void byIdDefaultNamedSQLite() { + ForceDialect.sqlite() + assertEquals "data->>'id' = :key", Where.byId(':key') + } + + @Test + @DisplayName('jsonContains generates defaults | PostgreSQL') + void jsonContainsDefaultPostgres() { + ForceDialect.postgres() + assertEquals 'data @> :criteria', Where.jsonContains() + } + + @Test + @DisplayName('jsonContains generates named parameter | PostgreSQL') + void jsonContainsNamedPostgres() { + ForceDialect.postgres() + assertEquals 'data @> :it', Where.jsonContains(':it') + } + + @Test + @DisplayName('jsonContains fails | SQLite') + void jsonContainsFailsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { Where.jsonContains() } + } + + @Test + @DisplayName('jsonPathMatches generates defaults | PostgreSQL') + void jsonPathMatchDefaultPostgres() { + ForceDialect.postgres() + assertEquals 'jsonb_path_exists(data, :path::jsonpath)', Where.jsonPathMatches() + } + + @Test + @DisplayName('jsonPathMatches generates named parameter | PostgreSQL') + void jsonPathMatchNamedPostgres() { + ForceDialect.postgres() + assertEquals 'jsonb_path_exists(data, :jp::jsonpath)', Where.jsonPathMatches(':jp') + } + + @Test + @DisplayName('jsonPathMatches fails | SQLite') + void jsonPathFailsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { Where.jsonPathMatches() } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ArrayDocument.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ArrayDocument.groovy new file mode 100644 index 0000000..59af483 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ArrayDocument.groovy @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +class ArrayDocument { + + String id + List values + + ArrayDocument(String id = '', List values = List.of()) { + this.id = id + this.values = values + } + + /** A set of documents used for integration tests */ + static List testDocuments = List.of( + new ArrayDocument("first", List.of("a", "b", "c")), + new ArrayDocument("second", List.of("c", "d", "e")), + new ArrayDocument("third", List.of("x", "y", "z"))) +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CountFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CountFunctions.groovy new file mode 100644 index 0000000..5f9e852 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CountFunctions.groovy @@ -0,0 +1,50 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Field + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class CountFunctions { + + static void all(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should have been 5 documents in the table' + } + + static void byFieldsNumeric(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(3L, db.conn.countByFields(TEST_TABLE, List.of(Field.between('numValue', 10, 20))), + 'There should have been 3 matching documents') + } + + static void byFieldsAlpha(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(1L, db.conn.countByFields(TEST_TABLE, List.of(Field.between('value', 'aardvark', 'apple'))), + 'There should have been 1 matching document') + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(2L, db.conn.countByContains(TEST_TABLE, Map.of('value', 'purple')), + 'There should have been 2 matching documents') + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(0L, db.conn.countByContains(TEST_TABLE, Map.of('value', 'magenta')), + 'There should have been no matching documents') + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(2L, db.conn.countByJsonPath(TEST_TABLE, '$.numValue ? (@ < 5)'), + 'There should have been 2 matching documents') + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(0L, db.conn.countByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)'), + 'There should have been no matching documents') + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CustomFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CustomFunctions.groovy new file mode 100644 index 0000000..70732bb --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CustomFunctions.groovy @@ -0,0 +1,132 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.java.Results +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.query.QueryUtils + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class CustomFunctions { + + static void listEmpty(ThrowawayDatabase db) { + JsonDocument.load db + db.conn.deleteByFields TEST_TABLE, List.of(Field.exists(Configuration.idField)) + def result = db.conn.customList FindQuery.all(TEST_TABLE), List.of(), JsonDocument, Results.&fromData + assertEquals 0, result.size(), 'There should have been no results' + } + + static void listAll(ThrowawayDatabase db) { + JsonDocument.load db + def result = db.conn.customList FindQuery.all(TEST_TABLE), List.of(), JsonDocument, Results.&fromData + assertEquals 5, result.size(), 'There should have been 5 results' + } + + static void jsonArrayEmpty(ThrowawayDatabase db) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), 'The test table should be empty') + assertEquals('[]', db.conn.customJsonArray(FindQuery.all(TEST_TABLE), List.of(), Results.&jsonFromData), + 'An empty list was not represented correctly'); + } + + static void jsonArraySingle(ThrowawayDatabase db) { + db.conn.insert(TEST_TABLE, new ArrayDocument("one", List.of("2", "3"))) + assertEquals(JsonFunctions.maybeJsonB('[{"id":"one","values":["2","3"]}]'), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE), List.of(), Results.&jsonFromData), + 'A single document list was not represented correctly') + } + + static void jsonArrayMany(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals(JsonFunctions.maybeJsonB('[{"id":"first","values":["a","b","c"]},' + + '{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]'), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE) + QueryUtils.orderBy(List.of(Field.named("id"))), + List.of(), Results.&jsonFromData), + 'A multiple document list was not represented correctly') + } + + static void writeJsonArrayEmpty(ThrowawayDatabase db) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), 'The test table should be empty') + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), List.of(), writer, Results.&jsonFromData) + assertEquals("[]", output.toString(), 'An empty list was not represented correctly') + } + + static void writeJsonArraySingle(ThrowawayDatabase db) { + db.conn.insert(TEST_TABLE, new ArrayDocument("one", List.of("2", "3"))) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), List.of(), writer, Results.&jsonFromData) + assertEquals(JsonFunctions.maybeJsonB('[{"id":"one","values":["2","3"]}]'), output.toString(), + 'A single document list was not represented correctly') + } + + static void writeJsonArrayMany(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE) + QueryUtils.orderBy(List.of(Field.named("id"))), + List.of(), writer, Results.&jsonFromData) + assertEquals(JsonFunctions.maybeJsonB('[{"id":"first","values":["a","b","c"]},' + + '{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]'), + output.toString(), "A multiple document list was not represented correctly") + } + + static void singleNone(ThrowawayDatabase db) { + assertFalse(db.conn.customSingle(FindQuery.all(TEST_TABLE), List.of(), JsonDocument, Results.&fromData) + .isPresent(), + 'There should not have been a document returned') + + } + + static void singleOne(ThrowawayDatabase db) { + JsonDocument.load db + assertTrue(db.conn.customSingle(FindQuery.all(TEST_TABLE), List.of(), JsonDocument, Results.&fromData) + .isPresent(), + 'There should not have been a document returned') + } + + static void jsonSingleNone(ThrowawayDatabase db) { + assertEquals('{}', db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), List.of(), Results.&jsonFromData), + 'An empty document was not represented correctly') + } + + static void jsonSingleOne(ThrowawayDatabase db) { + db.conn.insert(TEST_TABLE, new ArrayDocument("me", List.of("myself", "i"))) + assertEquals(JsonFunctions.maybeJsonB('{"id":"me","values":["myself","i"]}'), + db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), List.of(), Results.&jsonFromData), + 'A single document was not represented correctly'); + } + + static void nonQueryChanges(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), List.of(), Long, Results.&toCount), + 'There should have been 5 documents in the table') + db.conn.customNonQuery("DELETE FROM $TEST_TABLE") + assertEquals(0L, db.conn.customScalar(CountQuery.all(TEST_TABLE), List.of(), Long, Results.&toCount), + 'There should have been no documents in the table') + } + + static void nonQueryNoChanges(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), List.of(), Long, Results.&toCount), + 'There should have been 5 documents in the table') + db.conn.customNonQuery(DeleteQuery.byId(TEST_TABLE, 'eighty-two'), + List.of(new Parameter(':id', ParameterType.STRING, 'eighty-two'))) + assertEquals(5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), List.of(), Long, Results.&toCount), + 'There should still have been 5 documents in the table') + } + + static void scalar(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(3L, + db.conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", List.of(), Long, Results.&toCount), + 'The number 3 should have been returned') + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DefinitionFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DefinitionFunctions.groovy new file mode 100644 index 0000000..7108a70 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DefinitionFunctions.groovy @@ -0,0 +1,41 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.DocumentIndex + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class DefinitionFunctions { + + static void ensureTable(ThrowawayDatabase db) { + assertFalse db.dbObjectExists('ensured'), 'The "ensured" table should not exist' + assertFalse db.dbObjectExists('idx_ensured_key'), 'The PK index for the "ensured" table should not exist' + db.conn.ensureTable 'ensured' + assertTrue db.dbObjectExists('ensured'), 'The "ensured" table should exist' + assertTrue db.dbObjectExists('idx_ensured_key'), 'The PK index for the "ensured" table should now exist' + } + + static void ensureFieldIndex(ThrowawayDatabase db) { + assertFalse db.dbObjectExists("idx_${TEST_TABLE}_test"), 'The test index should not exist' + db.conn.ensureFieldIndex TEST_TABLE, 'test', List.of('id', 'category') + assertTrue db.dbObjectExists("idx_${TEST_TABLE}_test"), 'The test index should now exist' + } + + static void ensureDocumentIndexFull(ThrowawayDatabase db) { + assertFalse db.dbObjectExists('doc_table'), 'The "doc_table" table should not exist' + db.conn.ensureTable 'doc_table' + assertTrue db.dbObjectExists('doc_table'), 'The "doc_table" table should exist' + assertFalse db.dbObjectExists('idx_doc_table_document'), 'The document index should not exist' + db.conn.ensureDocumentIndex 'doc_table', DocumentIndex.FULL + assertTrue db.dbObjectExists('idx_doc_table_document'), 'The document index should exist' + } + + static void ensureDocumentIndexOptimized(ThrowawayDatabase db) { + assertFalse db.dbObjectExists('doc_table'), 'The "doc_table" table should not exist' + db.conn.ensureTable 'doc_table' + assertTrue db.dbObjectExists('doc_table'), 'The "doc_table" table should exist' + assertFalse db.dbObjectExists('idx_doc_table_document'), 'The document index should not exist' + db.conn.ensureDocumentIndex 'doc_table', DocumentIndex.OPTIMIZED + assertTrue db.dbObjectExists('idx_doc_table_document'), 'The document index should exist' + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DeleteFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DeleteFunctions.groovy new file mode 100644 index 0000000..6d51048 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DeleteFunctions.groovy @@ -0,0 +1,65 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Field + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class DeleteFunctions { + + static void byIdMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should be 5 documents in the table' + db.conn.deleteById TEST_TABLE, 'four' + assertEquals 4L, db.conn.countAll(TEST_TABLE), 'There should now be 4 documents in the table' + } + + static void byIdNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should be 5 documents in the table' + db.conn.deleteById TEST_TABLE, 'negative four' + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should still be 5 documents in the table' + } + + static void byFieldsMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should be 5 documents in the table' + db.conn.deleteByFields TEST_TABLE, List.of(Field.notEqual('value', 'purple')) + assertEquals 2L, db.conn.countAll(TEST_TABLE), 'There should now be 2 documents in the table' + } + + static void byFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should be 5 documents in the table' + db.conn.deleteByFields TEST_TABLE, List.of(Field.equal('value', 'crimson')) + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should still be 5 documents in the table' + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should be 5 documents in the table' + db.conn.deleteByContains TEST_TABLE, Map.of('value', 'purple') + assertEquals 3L, db.conn.countAll(TEST_TABLE), 'There should now be 3 documents in the table' + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should be 5 documents in the table' + db.conn.deleteByContains TEST_TABLE, Map.of('target', 'acquired') + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should still be 5 documents in the table' + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should be 5 documents in the table' + db.conn.deleteByJsonPath TEST_TABLE, '$.value ? (@ == "purple")' + assertEquals 3L, db.conn.countAll(TEST_TABLE), 'There should now be 3 documents in the table' + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should be 5 documents in the table' + db.conn.deleteByJsonPath TEST_TABLE, '$.numValue ? (@ > 100)' + assertEquals 5L, db.conn.countAll(TEST_TABLE), 'There should still be 5 documents in the table' + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DocumentFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DocumentFunctions.groovy new file mode 100644 index 0000000..5330ebc --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/DocumentFunctions.groovy @@ -0,0 +1,129 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class DocumentFunctions { + + static void insertDefault(ThrowawayDatabase db) { + assertEquals 0L, db.conn.countAll(TEST_TABLE), 'There should be no documents in the table' + def doc = new JsonDocument('turkey', 'yum', 5, new SubDocument('gobble', 'gobble!')) + db.conn.insert TEST_TABLE, doc + def after = db.conn.findAll TEST_TABLE, JsonDocument + assertEquals 1, after.size(), 'There should be one document in the table' + assertEquals doc.id, after[0].id, 'The document ID was not inserted correctly' + assertEquals doc.value, after[0].value, 'The document value was not inserted correctly' + assertEquals doc.numValue, after[0].numValue, 'The document numValue was not inserted correctly' + assertNotNull doc.sub, "The subdocument was not inserted" + assertEquals doc.sub.foo, after[0].sub.foo, 'The subdocument "foo" property was not inserted correctly' + assertEquals doc.sub.bar, after[0].sub.bar, 'The subdocument "bar" property was not inserted correctly' + } + + static void insertDupe(ThrowawayDatabase db) { + db.conn.insert TEST_TABLE, new JsonDocument('a', '', 0, null) + assertThrows(DocumentException, () -> db.conn.insert(TEST_TABLE, new JsonDocument('a', 'b', 22, null)), + 'Inserting a document with a duplicate key should have thrown an exception') + } + + static void insertNumAutoId(ThrowawayDatabase db) { + try { + Configuration.autoIdStrategy = AutoId.NUMBER + Configuration.idField = 'key' + assertEquals 0L, db.conn.countAll(TEST_TABLE), 'There should be no documents in the table' + + db.conn.insert TEST_TABLE, new NumIdDocument(0, 'one') + db.conn.insert TEST_TABLE, new NumIdDocument(0, 'two') + db.conn.insert TEST_TABLE, new NumIdDocument(77, 'three') + db.conn.insert TEST_TABLE, new NumIdDocument(0, 'four') + + def after = db.conn.findAll TEST_TABLE, NumIdDocument, List.of(Field.named('key')) + assertEquals 4, after.size(), 'There should have been 4 documents returned' + assertEquals('1|2|77|78', + after*.key*.toString().inject('') { acc, it -> acc == '' ? it : "$acc|$it" }.toString(), + 'The IDs were not generated correctly') + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idField = 'id' + } + } + + static void insertUUIDAutoId(ThrowawayDatabase db) { + try { + Configuration.autoIdStrategy = AutoId.UUID + assertEquals 0L, db.conn.countAll(TEST_TABLE), 'There should be no documents in the table' + + db.conn.insert TEST_TABLE, new JsonDocument('') + + def after = db.conn.findAll TEST_TABLE, JsonDocument + assertEquals 1, after.size(), 'There should have been 1 document returned' + assertEquals 32, after[0].id.length(), 'The ID was not generated correctly' + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + } + } + + static void insertStringAutoId(ThrowawayDatabase db) { + try { + Configuration.autoIdStrategy = AutoId.RANDOM_STRING + assertEquals 0L, db.conn.countAll(TEST_TABLE), 'There should be no documents in the table' + + db.conn.insert TEST_TABLE, new JsonDocument('') + + Configuration.idStringLength = 21 + db.conn.insert TEST_TABLE, new JsonDocument('') + + def after = db.conn.findAll TEST_TABLE, JsonDocument + assertEquals 2, after.size(), 'There should have been 2 documents returned' + assertEquals 16, after[0].id.length(), "The first document's ID was not generated correctly" + assertEquals 21, after[1].id.length(), "The second document's ID was not generated correctly" + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idStringLength = 16 + } + } + + static void saveMatch(ThrowawayDatabase db) { + JsonDocument.load db + db.conn.save TEST_TABLE, new JsonDocument('two', '', 44) + def tryDoc = db.conn.findById TEST_TABLE, 'two', JsonDocument + assertTrue tryDoc.isPresent(), 'There should have been a document returned' + def doc = tryDoc.get() + assertEquals 'two', doc.id, 'An incorrect document was returned' + assertEquals '', doc.value, 'The "value" field was not updated' + assertEquals 44, doc.numValue, 'The "numValue" field was not updated' + assertNull doc.sub, 'The "sub" field was not updated' + } + + static void saveNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + db.conn.save TEST_TABLE, new JsonDocument('test', '', 0, new SubDocument('a', 'b')) + assertTrue(db.conn.findById(TEST_TABLE, 'test', JsonDocument).isPresent(), + 'The test document should have been saved') + } + + static void updateMatch(ThrowawayDatabase db) { + JsonDocument.load db + db.conn.update TEST_TABLE, 'one', new JsonDocument('one', 'howdy', 8, new SubDocument('y', 'z')) + def tryDoc = db.conn.findById TEST_TABLE, 'one', JsonDocument + assertTrue tryDoc.isPresent(), 'There should have been a document returned' + def doc = tryDoc.get() + assertEquals 'one', doc.id, 'An incorrect document was returned' + assertEquals 'howdy', doc.value, 'The "value" field was not updated' + assertEquals 8, doc.numValue, 'The "numValue" field was not updated' + assertNotNull doc.sub, 'The sub-document should not be null' + assertEquals 'y', doc.sub.foo, 'The sub-document "foo" field was not updated' + assertEquals 'z', doc.sub.bar, 'The sub-document "bar" field was not updated' + } + + static void updateNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse db.conn.existsById(TEST_TABLE, 'two-hundred') + db.conn.update TEST_TABLE, 'two-hundred', new JsonDocument('two-hundred', '', 200) + assertFalse db.conn.existsById(TEST_TABLE, 'two-hundred') + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ExistsFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ExistsFunctions.groovy new file mode 100644 index 0000000..115940d --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ExistsFunctions.groovy @@ -0,0 +1,55 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Field + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class ExistsFunctions { + + static void byIdMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertTrue db.conn.existsById(TEST_TABLE, 'three'), 'The document with ID "three" should exist' + } + + static void byIdNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse db.conn.existsById(TEST_TABLE, 'seven'), 'The document with ID "seven" should not exist' + } + + static void byFieldsMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertTrue(db.conn.existsByFields(TEST_TABLE, List.of(Field.equal('numValue', 10))), + 'Matching documents should have been found') + } + + static void byFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse(db.conn.existsByFields(TEST_TABLE, List.of(Field.equal('nothing', 'none'))), + 'No matching documents should have been found') + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertTrue(db.conn.existsByContains(TEST_TABLE, Map.of('value', 'purple')), + 'Matching documents should have been found') + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse(db.conn.existsByContains(TEST_TABLE, Map.of('value', 'violet')), + 'Matching documents should not have been found') + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertTrue(db.conn.existsByJsonPath(TEST_TABLE, '$.numValue ? (@ == 10)'), + 'Matching documents should have been found') + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse(db.conn.existsByJsonPath(TEST_TABLE, '$.numValue ? (@ == 10.1)'), + 'Matching documents should not have been found') + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/FindFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/FindFunctions.groovy new file mode 100644 index 0000000..570f1da --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/FindFunctions.groovy @@ -0,0 +1,249 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class FindFunctions { + + private static String docIds(List docs) { + return docs*.id.inject('') { acc, it -> acc == '' ? it : "$acc|$it" } + } + + static void allDefault(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals 5, db.conn.findAll(TEST_TABLE, JsonDocument).size(), 'There should have been 5 documents returned' + } + + static void allAscending(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findAll TEST_TABLE, JsonDocument, List.of(Field.named('id')) + assertEquals 5, docs.size(), 'There should have been 5 documents returned' + assertEquals 'five|four|one|three|two', docIds(docs), 'The documents were not ordered correctly' + } + + static void allDescending(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findAll TEST_TABLE, JsonDocument, List.of(Field.named('id DESC')) + assertEquals 5, docs.size(), 'There should have been 5 documents returned' + assertEquals 'two|three|one|four|five', docIds(docs), 'The documents were not ordered correctly' + } + + static void allNumOrder(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findAll(TEST_TABLE, JsonDocument, + List.of(Field.named('sub.foo NULLS LAST'), Field.named('n:numValue'))) + assertEquals 5, docs.size(), 'There should have been 5 documents returned' + assertEquals 'two|four|one|three|five', docIds(docs), 'The documents were not ordered correctly' + } + + static void allEmpty(ThrowawayDatabase db) { + assertEquals 0, db.conn.findAll(TEST_TABLE, JsonDocument).size(), 'There should have been no documents returned' + } + + static void byIdString(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findById TEST_TABLE, 'two', JsonDocument + assertTrue doc.isPresent(), 'The document should have been returned' + assertEquals 'two', doc.get().id, 'An incorrect document was returned' + } + + static void byIdNumber(ThrowawayDatabase db) { + Configuration.idField = 'key' + try { + db.conn.insert TEST_TABLE, new NumIdDocument(18, 'howdy') + assertTrue(db.conn.findById(TEST_TABLE, 18, NumIdDocument).isPresent(), + 'The document should have been returned') + } finally { + Configuration.idField = 'id' + } + } + + static void byIdNotFound(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse(db.conn.findById(TEST_TABLE, 'x', JsonDocument).isPresent(), + 'There should have been no document returned') + } + + static void byFieldsMatch(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findByFields(TEST_TABLE, + List.of(Field.any('value', List.of('blue', 'purple')), Field.exists('sub')), JsonDocument, + FieldMatch.ALL) + assertEquals 1, docs.size(), 'There should have been a document returned' + assertEquals 'four', docs[0].id, 'The incorrect document was returned' + } + + static void byFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findByFields(TEST_TABLE, List.of(Field.equal('value', 'purple')), JsonDocument, null, + List.of(Field.named('id'))) + assertEquals 2, docs.size(), 'There should have been 2 documents returned' + assertEquals 'five|four', docIds(docs), 'The documents were not ordered correctly' + } + + static void byFieldsMatchNumIn(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findByFields TEST_TABLE, List.of(Field.any('numValue', List.of(2, 4, 6, 8))), JsonDocument + assertEquals 1, docs.size(), 'There should have been a document returned' + assertEquals 'three', docs[0].id, 'The incorrect document was returned' + } + + static void byFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(0, db.conn.findByFields(TEST_TABLE, List.of(Field.greater('numValue', 100)), JsonDocument).size(), + 'There should have been no documents returned') + } + + static void byFieldsMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert TEST_TABLE, it } + def docs = db.conn.findByFields(TEST_TABLE, List.of(Field.inArray('values', TEST_TABLE, List.of('c'))), + ArrayDocument) + assertEquals 2, docs.size(), 'There should have been two documents returned' + assertTrue(List.of('first', 'second').contains(docs[0].id), + "An incorrect document was returned (${docs[0].id})") + assertTrue(List.of('first', 'second').contains(docs[1].id), + "An incorrect document was returned (${docs[1].id})") + } + + static void byFieldsNoMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert TEST_TABLE, it } + assertEquals(0, + db.conn.findByFields(TEST_TABLE, List.of(Field.inArray('values', TEST_TABLE, List.of('j'))), + ArrayDocument).size(), + 'There should have been no documents returned') + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findByContains TEST_TABLE, Map.of('value', 'purple'), JsonDocument + assertEquals 2, docs.size(), 'There should have been 2 documents returned' + assertTrue List.of('four', 'five').contains(docs[0].id), "An incorrect document was returned (${docs[0].id})" + assertTrue List.of('four', 'five').contains(docs[1].id), "An incorrect document was returned (${docs[1].id})" + } + + static void byContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findByContains(TEST_TABLE, Map.of('sub', Map.of('foo', 'green')), JsonDocument, + List.of(Field.named('value'))) + assertEquals 2, docs.size(), 'There should have been 2 documents returned' + assertEquals 'two|four', docIds(docs), 'The documents were not ordered correctly' + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(0, db.conn.findByContains(TEST_TABLE, Map.of('value', 'indigo'), JsonDocument).size(), + 'There should have been no documents returned') + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findByJsonPath TEST_TABLE, '$.numValue ? (@ > 10)', JsonDocument + assertEquals 2, docs.size(), 'There should have been 2 documents returned' + assertTrue List.of('four', 'five').contains(docs[0].id), "An incorrect document was returned (${docs[0].id})" + assertTrue List.of('four', 'five').contains(docs[1].id), "An incorrect document was returned (${docs[1].id})" + } + + static void byJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load db + def docs = db.conn.findByJsonPath TEST_TABLE, '$.numValue ? (@ > 10)', JsonDocument, List.of(Field.named('id')) + assertEquals 2, docs.size(), 'There should have been 2 documents returned' + assertEquals 'five|four', docIds(docs), 'The documents were not ordered correctly' + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertEquals(0, db.conn.findByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)', JsonDocument).size(), + 'There should have been no documents returned') + } + + static void firstByFieldsMatchOne(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByFields TEST_TABLE, List.of(Field.equal('value', 'another')), JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals 'two', doc.get().id, 'The incorrect document was returned' + } + + static void firstByFieldsMatchMany(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByFields TEST_TABLE, List.of(Field.equal('sub.foo', 'green')), JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertTrue List.of('two', 'four').contains(doc.get().id), "An incorrect document was returned (${doc.get().id})" + } + + static void firstByFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByFields(TEST_TABLE, List.of(Field.equal('sub.foo', 'green')), JsonDocument, null, + List.of(Field.named('n:numValue DESC'))) + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals 'four', doc.get().id, 'An incorrect document was returned' + } + + static void firstByFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse(db.conn.findFirstByFields(TEST_TABLE, List.of(Field.equal('value', 'absent')), JsonDocument) + .isPresent(), + 'There should have been no document returned') + } + + static void firstByContainsMatchOne(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByContains TEST_TABLE, Map.of('value', 'FIRST!'), JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals 'one', doc.get().id, 'An incorrect document was returned' + } + + static void firstByContainsMatchMany(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByContains TEST_TABLE, Map.of('value', 'purple'), JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertTrue(List.of('four', 'five').contains(doc.get().id), + "An incorrect document was returned (${doc.get().id})") + } + + static void firstByContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByContains(TEST_TABLE, Map.of('value', 'purple'), JsonDocument, + List.of(Field.named('sub.bar NULLS FIRST'))) + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals 'five', doc.get().id, 'An incorrect document was returned' + } + + static void firstByContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse(db.conn.findFirstByContains(TEST_TABLE, Map.of('value', 'indigo'), JsonDocument).isPresent(), + 'There should have been no document returned') + } + + static void firstByJsonPathMatchOne(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByJsonPath TEST_TABLE, '$.numValue ? (@ == 10)', JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals 'two', doc.get().id, 'An incorrect document was returned' + } + + static void firstByJsonPathMatchMany(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByJsonPath TEST_TABLE, '$.numValue ? (@ > 10)', JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertTrue(List.of('four', 'five').contains(doc.get().id), + "An incorrect document was returned (${doc.get().id})") + } + + static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load db + def doc = db.conn.findFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)', JsonDocument, + List.of(Field.named('id DESC'))) + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals 'four', doc.get().id, 'An incorrect document was returned' + } + + static void firstByJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse(db.conn.findFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)', JsonDocument).isPresent(), + 'There should have been no document returned') + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JacksonDocumentSerializer.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JacksonDocumentSerializer.groovy new file mode 100644 index 0000000..cd9db60 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JacksonDocumentSerializer.groovy @@ -0,0 +1,22 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import com.fasterxml.jackson.databind.ObjectMapper +import solutions.bitbadger.documents.DocumentSerializer + +/** + * A JSON serializer using Jackson's default options + */ +class JacksonDocumentSerializer implements DocumentSerializer { + + private def mapper = new ObjectMapper() + + @Override + def String serialize(TDoc document) { + return mapper.writeValueAsString(document) + } + + @Override + def TDoc deserialize(String json, Class clazz) { + return mapper.readValue(json, clazz) + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy new file mode 100644 index 0000000..db6c1bb --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy @@ -0,0 +1,43 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +class JsonDocument { + String id + String value + int numValue + SubDocument sub + + JsonDocument(String id = null, String value = "", int numValue = 0, SubDocument sub = null) { + this.id = id + this.value = value + this.numValue = numValue + this.sub = sub + } + + private static final List testDocuments = List.of( + new JsonDocument("one", "FIRST!", 0), + new JsonDocument("two", "another", 10, new SubDocument("green", "blue")), + new JsonDocument("three", "", 4), + new JsonDocument("four", "purple", 17, new SubDocument("green", "red")), + new JsonDocument("five", "purple", 18)) + + static void load(ThrowawayDatabase db, String tableName = TEST_TABLE) { + testDocuments.forEach { db.conn.insert(tableName, it) } + } + + /** Document ID one as a JSON string */ + static String one = '{"id":"one","value":"FIRST!","numValue":0,"sub":null}' + + /** Document ID two as a JSON string */ + static String two = '{"id":"two","value":"another","numValue":10,"sub":{"foo":"green","bar":"blue"}}' + + /** Document ID three as a JSON string */ + static String three = '{"id":"three","value":"","numValue":4,"sub":null}' + + /** Document ID four as a JSON string */ + static String four = '{"id":"four","value":"purple","numValue":17,"sub":{"foo":"green","bar":"red"}}' + + /** Document ID five as a JSON string */ + static String five = '{"id":"five","value":"purple","numValue":18,"sub":null}' +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy new file mode 100644 index 0000000..186c480 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy @@ -0,0 +1,738 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +/** + * Tests for the JSON-returning functions + * + * NOTE: PostgreSQL JSONB columns do not preserve the original JSON with which a document was stored. These tests are + * the most complex within the library, as they have split testing based on the backing data store. The PostgreSQL tests + * check IDs (and, in the case of ordered queries, which ones occur before which others) vs. the entire JSON string. + * Meanwhile, SQLite stores JSON as text, and will return exactly the JSON it was given when it was originally written. + * These tests can ensure the expected round-trip of the entire JSON string. + */ +final class JsonFunctions { + + /** + * PostgreSQL, when returning JSONB as a string, has spaces after commas and colons delineating fields and values. + * This function will do a crude string replacement to match the target string based on the dialect being tested. + * + * @param json The JSON which should be returned + * @return The actual expected JSON based on the database being tested + */ + static String maybeJsonB(String json) { + return switch (Configuration.dialect()) { + case Dialect.SQLITE -> json + case Dialect.POSTGRESQL -> json.replace ('":', '": ' ).replace (',"', ', "') + } + } + + /** + * Create a snippet of JSON to find a document ID + * + * @param id The ID of the document + * @return A connection-aware ID to check for presence and positioning + */ + private static String docId(String id) { + return maybeJsonB("{\"id\":\"$id\"") + } + + private static void checkAllDefault(String json) { + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertTrue(json.contains(JsonDocument.one), "Document 'one' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.two), "Document 'two' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.three), "Document 'three' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found in JSON ($json)") + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('one')), "Document 'one' not found in JSON ($json)") + assertTrue(json.contains(docId('two')), "Document 'two' not found in JSON ($json)") + assertTrue(json.contains(docId('three')), "Document 'three' not found in JSON ($json)") + assertTrue(json.contains(docId('four')), "Document 'four' not found in JSON ($json)") + assertTrue(json.contains(docId('five')), "Document 'five' not found in JSON ($json)") + break + } + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + } + + static void allDefault(ThrowawayDatabase db) { + JsonDocument.load(db) + checkAllDefault(db.conn.jsonAll(TEST_TABLE)) + } + + static void writeAllDefault(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonAll(TEST_TABLE, writer) + checkAllDefault(output.toString()) + } + + private static void checkAllEmpty(String json) { + assertEquals('[]', json, 'There should have been no documents returned') + } + + static void allEmpty(ThrowawayDatabase db) { + checkAllEmpty(db.conn.jsonAll(TEST_TABLE)) + } + + static void writeAllEmpty(ThrowawayDatabase db) { + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonAll(TEST_TABLE, writer) + checkAllEmpty(output.toString()) + } + + private static void checkByIdString(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals(JsonDocument.two, json, 'An incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('two')), "An incorrect document was returned ($json)") + break + } + } + + static void byIdString(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByIdString(db.conn.jsonById(TEST_TABLE, 'two')) + } + + static void writeByIdString(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, 'two') + checkByIdString(output.toString()) + } + + private static void checkByIdNumber(String json) { + assertEquals(maybeJsonB('{"key":18,"text":"howdy"}'), json, 'The document should have been found by numeric ID') + } + + static void byIdNumber(ThrowawayDatabase db) { + Configuration.idField = 'key' + try { + db.conn.insert(TEST_TABLE, new NumIdDocument(18, 'howdy')) + checkByIdNumber(db.conn.jsonById(TEST_TABLE, 18)) + } finally { + Configuration.idField = 'id' + } + } + + static void writeByIdNumber(ThrowawayDatabase db) { + Configuration.idField = 'key' + try { + db.conn.insert(TEST_TABLE, new NumIdDocument(18, 'howdy')) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, 18) + checkByIdNumber(output.toString()) + } finally { + Configuration.idField = 'id' + } + } + + private static void checkByIdNotFound(String json) { + assertEquals('{}', json, 'There should have been no document returned') + } + + static void byIdNotFound(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByIdNotFound(db.conn.jsonById(TEST_TABLE, 'x')) + } + + static void writeByIdNotFound(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, 'x') + checkByIdNotFound(output.toString()) + } + + private static void checkByFieldsMatch(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals("[${JsonDocument.four}]".toString(), json, 'The incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId('four')), "The incorrect document was returned ($json)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + break + } + } + + static void byFieldsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByFieldsMatch(db.conn.jsonByFields(TEST_TABLE, List.of(Field.any('value', List.of('blue', 'purple')), + Field.exists('sub')), FieldMatch.ALL)) + } + + static void writeByFieldsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, List.of(Field.any('value', List.of('blue', 'purple')), + Field.exists('sub')), FieldMatch.ALL) + checkByFieldsMatch(output.toString()) + } + + private static checkByFieldsMatchOrdered(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals("[${JsonDocument.five},${JsonDocument.four}]".toString(), json, + 'The documents were not ordered correctly') + break + case Dialect.POSTGRESQL: + int fiveIdx = json.indexOf(docId('five')) + int fourIdx = json.indexOf(docId('four')) + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, "Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, "Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + break + } + } + + static void byFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByFieldsMatchOrdered(db.conn.jsonByFields(TEST_TABLE, List.of(Field.equal('value', 'purple')), null, + List.of(Field.named('id')))) + } + + static void writeByFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, List.of(Field.equal('value', 'purple')), null, + List.of(Field.named('id'))) + checkByFieldsMatchOrdered(output.toString()) + } + + private static void checkByFieldsMatchNumIn(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals("[${JsonDocument.three}]".toString(), json, 'The incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId('three')), "The incorrect document was returned ($json)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + break + } + } + + static void byFieldsMatchNumIn(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByFieldsMatchNumIn(db.conn.jsonByFields(TEST_TABLE, List.of(Field.any('numValue', List.of(2, 4, 6, 8))))) + } + + static void writeByFieldsMatchNumIn(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, List.of(Field.any('numValue', List.of(2, 4, 6, 8)))) + checkByFieldsMatchNumIn(output.toString()) + } + + private static void checkByFieldsNoMatch(String json) { + assertEquals('[]', json, 'There should have been no documents returned') + } + + static void byFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByFieldsNoMatch(db.conn.jsonByFields(TEST_TABLE, List.of(Field.greater('numValue', 100)))) + } + + static void writeByFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, List.of(Field.greater('numValue', 100))) + checkByFieldsNoMatch(output.toString()) + } + + private static void checkByFieldsMatchInArray(String json) { + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId('first')), "The 'first' document was not found ($json)") + assertTrue(json.contains(docId('second')), "The 'second' document was not found ($json)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + } + + static void byFieldsMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + checkByFieldsMatchInArray(db.conn.jsonByFields(TEST_TABLE, + List.of(Field.inArray('values', TEST_TABLE, List.of('c'))))) + } + + static void writeByFieldsMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, List.of(Field.inArray('values', TEST_TABLE, List.of('c')))) + checkByFieldsMatchInArray(output.toString()) + } + + private static void checkByFieldsNoMatchInArray(String json) { + assertEquals('[]', json, 'There should have been no documents returned') + } + + static void byFieldsNoMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + checkByFieldsNoMatchInArray(db.conn.jsonByFields(TEST_TABLE, + List.of(Field.inArray('values', TEST_TABLE, List.of('j'))))) + } + + static void writeByFieldsNoMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, List.of(Field.inArray('values', TEST_TABLE, List.of('j')))) + checkByFieldsNoMatchInArray(output.toString()) + } + + private static void checkByContainsMatch(String json) { + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found ($json)") + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('four')), "Document 'four' not found ($json)") + assertTrue(json.contains(docId('five')), "Document 'five' not found ($json)") + break + } + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByContainsMatch(db.conn.jsonByContains(TEST_TABLE, Map.of('value', 'purple'))) + } + + static void writeByContainsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByContains(TEST_TABLE, writer, Map.of('value', 'purple')) + checkByContainsMatch(output.toString()) + } + + private static void checkByContainsMatchOrdered(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals("[${JsonDocument.two},${JsonDocument.four}]", json, + 'The documents were not ordered correctly') + break + case Dialect.POSTGRESQL: + int twoIdx = json.indexOf(docId('two')) + int fourIdx = json.indexOf(docId('four')) + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + assertTrue(twoIdx >= 0, "Document 'two' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(twoIdx < fourIdx, "Document 'two' should have been before 'four' ($json)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + break + } + } + + static void byContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByContainsMatchOrdered(db.conn.jsonByContains(TEST_TABLE, Map.of('sub', Map.of('foo', 'green')), + List.of(Field.named('value')))) + } + + static void writeByContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByContains(TEST_TABLE, writer, Map.of('sub', Map.of('foo', 'green')), + List.of(Field.named('value'))) + checkByContainsMatchOrdered(output.toString()) + } + + private static void checkByContainsNoMatch(String json) { + assertEquals('[]', json, 'There should have been no documents returned') + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByContainsNoMatch(db.conn.jsonByContains(TEST_TABLE, Map.of('value', 'indigo'))) + } + + static void writeByContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByContains(TEST_TABLE, writer, Map.of('value', 'indigo')) + checkByContainsNoMatch(output.toString()) + } + + private static void checkByJsonPathMatch(String json) { + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found ($json)") + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('four')), "Document 'four' not found ($json)") + assertTrue(json.contains(docId('five')), "Document 'five' not found ($json)") + break + } + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByJsonPathMatch(db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)')) + } + + static void writeByJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, '$.numValue ? (@ > 10)') + checkByJsonPathMatch(output.toString()) + } + + private static void checkByJsonPathMatchOrdered(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals("[${JsonDocument.five},${JsonDocument.four}]", json, + 'The documents were not ordered correctly') + break + case Dialect.POSTGRESQL: + int fiveIdx = json.indexOf(docId('five')) + int fourIdx = json.indexOf(docId('four')) + assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, "Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, "Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + break + } + } + + static void byJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByJsonPathMatchOrdered(db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)', + List.of(Field.named('id')))) + } + + static void writeByJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, '$.numValue ? (@ > 10)', List.of(Field.named('id'))) + checkByJsonPathMatchOrdered(output.toString()) + } + + private static void checkByJsonPathNoMatch(String json) { + assertEquals('[]', json, 'There should have been no documents returned') + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkByJsonPathNoMatch(db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)')) + } + + static void writeByJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, '$.numValue ? (@ > 100)') + checkByJsonPathNoMatch(output.toString()) + } + + private static void checkFirstByFieldsMatchOne(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals(JsonDocument.two, json, 'The incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('two')), "The incorrect document was returned ($json)") + break + } + } + + static void firstByFieldsMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByFieldsMatchOne(db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('value', 'another')))) + } + + static void writeFirstByFieldsMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, List.of(Field.equal('value', 'another'))) + checkFirstByFieldsMatchOne(output.toString()) + } + + private static void checkFirstByFieldsMatchMany(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertTrue(json.contains(JsonDocument.two) || json.contains(JsonDocument.four), + "Expected document 'two' or 'four' ($json)") + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('two')) || json.contains(docId('four')), + "Expected document 'two' or 'four' ($json)") + break + } + } + + static void firstByFieldsMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByFieldsMatchMany(db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('sub.foo', 'green')))) + } + + static void writeFirstByFieldsMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, List.of(Field.equal('sub.foo', 'green'))) + checkFirstByFieldsMatchMany(output.toString()) + } + + private static void checkFirstByFieldsMatchOrdered(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals(JsonDocument.four, json, 'An incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('four')), "An incorrect document was returned ($json)") + break + } + } + + static void firstByFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByFieldsMatchOrdered(db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('sub.foo', 'green')), + null, List.of(Field.named('n:numValue DESC')))) + } + + static void writeFirstByFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, List.of(Field.equal('sub.foo', 'green')), + null, List.of(Field.named('n:numValue DESC'))) + checkFirstByFieldsMatchOrdered(output.toString()) + } + + private static void checkFirstByFieldsNoMatch(String json) { + assertEquals('{}', json, 'There should have been no document returned') + } + + static void firstByFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByFieldsNoMatch(db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('value', 'absent')))) + } + + static void writeFirstByFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, List.of(Field.equal('value', 'absent'))) + checkFirstByFieldsNoMatch(output.toString()) + } + + private static void checkFirstByContainsMatchOne(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals(JsonDocument.one, json, 'An incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('one')), "An incorrect document was returned ($json)") + break + } + } + + static void firstByContainsMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByContainsMatchOne(db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'FIRST!'))) + } + + static void writeFirstByContainsMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, Map.of('value', 'FIRST!')) + checkFirstByContainsMatchOne(output.toString()) + } + + private static void checkFirstByContainsMatchMany(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertTrue(json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + "Expected document 'four' or 'five' ($json)") + break + case Dialect.POSTGRESQL: + assertTrue( json.contains(docId('four')) || json.contains(docId('five')), + "Expected document 'four' or 'five' ($json)") + break + } + } + + static void firstByContainsMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByContainsMatchMany(db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'purple'))) + } + + static void writeFirstByContainsMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, Map.of('value', 'purple')) + checkFirstByContainsMatchMany(output.toString()) + } + + private static void checkFirstByContainsMatchOrdered(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals(JsonDocument.five, json, 'An incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('five')), "An incorrect document was returned ($json)") + break + } + } + + static void firstByContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByContainsMatchOrdered(db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'purple'), + List.of(Field.named('sub.bar NULLS FIRST')))) + } + + static void writeFirstByContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, Map.of('value', 'purple'), + List.of(Field.named('sub.bar NULLS FIRST'))) + checkFirstByContainsMatchOrdered(output.toString()) + } + + private static void checkFirstByContainsNoMatch(String json) { + assertEquals('{}', json, 'There should have been no document returned') + } + + static void firstByContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByContainsNoMatch(db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'indigo'))) + } + + static void writeFirstByContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, Map.of('value', 'indigo')) + checkFirstByContainsNoMatch(output.toString()) + } + + private static void checkFirstByJsonPathMatchOne(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals(JsonDocument.two, json, 'An incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('two')), "An incorrect document was returned ($json)") + break + } + } + + static void firstByJsonPathMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByJsonPathMatchOne(db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ == 10)')) + } + + static void writeFirstByJsonPathMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, '$.numValue ? (@ == 10)') + checkFirstByJsonPathMatchOne(output.toString()) + } + + private static void checkFirstByJsonPathMatchMany(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertTrue(json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + "Expected document 'four' or 'five' ($json)") + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('four')) || json.contains(docId('five')), + "Expected document 'four' or 'five' ($json)") + break + } + } + + static void firstByJsonPathMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByJsonPathMatchMany(db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)')) + } + + static void writeFirstByJsonPathMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, '$.numValue ? (@ > 10)') + checkFirstByJsonPathMatchMany(output.toString()) + } + + private static void checkFirstByJsonPathMatchOrdered(String json) { + switch (Configuration.dialect()) { + case Dialect.SQLITE: + assertEquals(JsonDocument.four, json, 'An incorrect document was returned') + break + case Dialect.POSTGRESQL: + assertTrue(json.contains(docId('four')), "An incorrect document was returned ($json)") + break + } + } + + static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByJsonPathMatchOrdered(db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)', + List.of(Field.named('id DESC')))) + } + + static void writeFirstByJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, '$.numValue ? (@ > 10)', List.of(Field.named('id DESC'))) + checkFirstByJsonPathMatchOrdered(output.toString()) + } + + private static void checkFirstByJsonPathNoMatch(String json) { + assertEquals('{}', json, 'There should have been no document returned') + } + + static void firstByJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + checkFirstByJsonPathNoMatch(db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)')) + } + + static void writeFirstByJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + def output = new StringWriter() + def writer = new PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, '$.numValue ? (@ > 100)') + checkFirstByJsonPathNoMatch(output.toString()) + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/NumIdDocument.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/NumIdDocument.groovy new file mode 100644 index 0000000..e4439a5 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/NumIdDocument.groovy @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +class NumIdDocument { + int key + String text + + NumIdDocument(int key = 0, String text = "") { + this.key = key + this.text = text + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PatchFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PatchFunctions.groovy new file mode 100644 index 0000000..684a347 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PatchFunctions.groovy @@ -0,0 +1,75 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Field + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class PatchFunctions { + + static void byIdMatch(ThrowawayDatabase db) { + JsonDocument.load db + db.conn.patchById TEST_TABLE, 'one', Map.of('numValue', 44) + def doc = db.conn.findById TEST_TABLE, 'one', JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals 'one', doc.get().id, 'An incorrect document was returned' + assertEquals 44, doc.get().numValue, 'The document was not patched' + } + + static void byIdNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse db.conn.existsById(TEST_TABLE, 'forty-seven'), 'Document with ID "forty-seven" should not exist' + db.conn.patchById TEST_TABLE, 'forty-seven', Map.of('foo', 'green') // no exception = pass + } + + static void byFieldsMatch(ThrowawayDatabase db) { + JsonDocument.load db + db.conn.patchByFields TEST_TABLE, List.of(Field.equal('value', 'purple')), Map.of('numValue', 77) + assertEquals(2, db.conn.countByFields(TEST_TABLE, List.of(Field.equal('numValue', 77))), + 'There should have been 2 documents with numeric value 77') + } + + static void byFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + def fields = List.of Field.equal('value', 'burgundy') + assertFalse db.conn.existsByFields(TEST_TABLE, fields), 'There should be no documents with value of "burgundy"' + db.conn.patchByFields TEST_TABLE, fields, Map.of('foo', 'green') // no exception = pass + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load db + def contains = Map.of 'value', 'another' + db.conn.patchByContains TEST_TABLE, contains, Map.of('numValue', 12) + def doc = db.conn.findFirstByContains TEST_TABLE, contains, JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals 'two', doc.get().id, 'The incorrect document was returned' + assertEquals 12, doc.get().numValue, 'The document was not updated' + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + def contains = Map.of 'value', 'updated' + assertFalse db.conn.existsByContains(TEST_TABLE, contains), 'There should be no matching documents' + db.conn.patchByContains TEST_TABLE, contains, Map.of('sub.foo', 'green') // no exception = pass + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load db + def path = '$.numValue ? (@ > 10)' + db.conn.patchByJsonPath TEST_TABLE, path, Map.of('value', 'blue') + def docs = db.conn.findByJsonPath TEST_TABLE, path, JsonDocument + assertEquals 2, docs.size(), 'There should have been two documents returned' + docs.forEach { + assertTrue List.of('four', 'five').contains(it.id), "An incorrect document was returned (${it.id})" + assertEquals 'blue', it.value, "The value for ID ${it.id} was incorrect" + } + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + def path = '$.numValue ? (@ > 100)' + assertFalse(db.conn.existsByJsonPath(TEST_TABLE, path), + 'There should be no documents with numeric values over 100') + db.conn.patchByJsonPath TEST_TABLE, path, Map.of('value', 'blue') // no exception = pass + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PgDB.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PgDB.groovy new file mode 100644 index 0000000..e60ad50 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PgDB.groovy @@ -0,0 +1,50 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.java.DocumentConfig +import solutions.bitbadger.documents.java.Results + +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +/** + * A wrapper for a throwaway PostgreSQL database + */ +class PgDB implements ThrowawayDatabase { + + PgDB() { + Configuration.setConnectionString connString('postgres') + Configuration.dbConn().withCloseable { it.customNonQuery "CREATE DATABASE $dbName" } + + Configuration.setConnectionString connString(dbName) + conn = Configuration.dbConn() + conn.ensureTable TEST_TABLE + + // Use a Jackson-based document serializer for testing + DocumentConfig.serializer = new JacksonDocumentSerializer() + } + + void close() { + conn.close() + Configuration.setConnectionString connString('postgres') + Configuration.dbConn().withCloseable { it.customNonQuery "DROP DATABASE $dbName" } + Configuration.setConnectionString null + } + + boolean dbObjectExists(String name) { + conn.customScalar('SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = :name) AS it', + List.of(new Parameter(':name', ParameterType.STRING, name)), Boolean, Results.&toExists) + } + + /** + * Create a connection string for the given database + * + * @param database The database to which the library should connect + * @return The connection string for the database + */ + private static String connString(String database) { + return "jdbc:postgresql://localhost/$database?user=postgres&password=postgres" + } + +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCountIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCountIT.groovy new file mode 100644 index 0000000..0cb222a --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCountIT.groovy @@ -0,0 +1,53 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Count') +final class PostgreSQLCountIT { + + @Test + @DisplayName('all counts all documents') + void all() { + new PgDB().withCloseable CountFunctions.&all + } + + @Test + @DisplayName('byFields counts documents by a numeric value') + void byFieldsNumeric() { + new PgDB().withCloseable CountFunctions.&byFieldsNumeric + } + + @Test + @DisplayName('byFields counts documents by a alphanumeric value') + void byFieldsAlpha() { + new PgDB().withCloseable CountFunctions.&byFieldsAlpha + } + + @Test + @DisplayName('byContains counts documents when matches are found') + void byContainsMatch() { + new PgDB().withCloseable CountFunctions.&byContainsMatch + } + + @Test + @DisplayName('byContains counts documents when no matches are found') + void byContainsNoMatch() { + new PgDB().withCloseable CountFunctions.&byContainsNoMatch + } + + @Test + @DisplayName('byJsonPath counts documents when matches are found') + void byJsonPathMatch() { + new PgDB().withCloseable CountFunctions.&byJsonPathMatch + } + + @Test + @DisplayName('byJsonPath counts documents when no matches are found') + void byJsonPathNoMatch() { + new PgDB().withCloseable CountFunctions.&byJsonPathNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCustomIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCustomIT.groovy new file mode 100644 index 0000000..bca62d8 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCustomIT.groovy @@ -0,0 +1,101 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Custom') +final class PostgreSQLCustomIT { + + @Test + @DisplayName('list succeeds with empty list') + void listEmpty() { + new PgDB().withCloseable CustomFunctions.&listEmpty + } + + @Test + @DisplayName('list succeeds with a non-empty list') + void listAll() { + new PgDB().withCloseable CustomFunctions.&listAll + } + + @Test + @DisplayName('jsonArray succeeds with empty array') + void jsonArrayEmpty() { + new PgDB().withCloseable CustomFunctions.&jsonArrayEmpty + } + + @Test + @DisplayName('jsonArray succeeds with a single-item array') + void jsonArraySingle() { + new PgDB().withCloseable CustomFunctions.&jsonArraySingle + } + + @Test + @DisplayName('jsonArray succeeds with a multi-item array') + void jsonArrayMany() { + new PgDB().withCloseable CustomFunctions.&jsonArrayMany + } + + @Test + @DisplayName('writeJsonArray succeeds with empty array') + void writeJsonArrayEmpty() { + new PgDB().withCloseable CustomFunctions.&writeJsonArrayEmpty + } + + @Test + @DisplayName('writeJsonArray succeeds with a single-item array') + void writeJsonArraySingle() { + new PgDB().withCloseable CustomFunctions.&writeJsonArraySingle + } + + @Test + @DisplayName('writeJsonArray succeeds with a multi-item array') + void writeJsonArrayMany() { + new PgDB().withCloseable CustomFunctions.&writeJsonArrayMany + } + + @Test + @DisplayName('single succeeds when document not found') + void singleNone() { + new PgDB().withCloseable CustomFunctions.&singleNone + } + + @Test + @DisplayName('single succeeds when a document is found') + void singleOne() { + new PgDB().withCloseable CustomFunctions.&singleOne + } + + @Test + @DisplayName('jsonSingle succeeds when document not found') + void jsonSingleNone() { + new PgDB().withCloseable CustomFunctions.&jsonSingleNone + } + + @Test + @DisplayName('jsonSingle succeeds when a document is found') + void jsonSingleOne() { + new PgDB().withCloseable CustomFunctions.&jsonSingleOne + } + + @Test + @DisplayName('nonQuery makes changes') + void nonQueryChanges() { + new PgDB().withCloseable CustomFunctions.&nonQueryChanges + } + + @Test + @DisplayName('nonQuery makes no changes when where clause matches nothing') + void nonQueryNoChanges() { + new PgDB().withCloseable CustomFunctions.&nonQueryNoChanges + } + + @Test + @DisplayName('scalar succeeds') + void scalar() { + new PgDB().withCloseable CustomFunctions.&scalar + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDefinitionIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDefinitionIT.groovy new file mode 100644 index 0000000..d5ea0e1 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDefinitionIT.groovy @@ -0,0 +1,35 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Definition') +final class PostgreSQLDefinitionIT { + + @Test + @DisplayName('ensureTable creates table and index') + void ensureTable() { + new PgDB().withCloseable DefinitionFunctions.&ensureTable + } + + @Test + @DisplayName('ensureFieldIndex creates an index') + void ensureFieldIndex() { + new PgDB().withCloseable DefinitionFunctions.&ensureFieldIndex + } + + @Test + @DisplayName('ensureDocumentIndex creates a full index') + void ensureDocumentIndexFull() { + new PgDB().withCloseable DefinitionFunctions.&ensureDocumentIndexFull + } + + @Test + @DisplayName('ensureDocumentIndex creates an optimized index') + void ensureDocumentIndexOptimized() { + new PgDB().withCloseable DefinitionFunctions.&ensureDocumentIndexOptimized + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDeleteIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDeleteIT.groovy new file mode 100644 index 0000000..d0474a7 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDeleteIT.groovy @@ -0,0 +1,59 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Delete') +final class PostgreSQLDeleteIT { + + @Test + @DisplayName('byId deletes a matching ID') + void byIdMatch() { + new PgDB().withCloseable DeleteFunctions.&byIdMatch + } + + @Test + @DisplayName('byId succeeds when no ID matches') + void byIdNoMatch() { + new PgDB().withCloseable DeleteFunctions.&byIdNoMatch + } + + @Test + @DisplayName('byFields deletes matching documents') + void byFieldsMatch() { + new PgDB().withCloseable DeleteFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields succeeds when no documents match') + void byFieldsNoMatch() { + new PgDB().withCloseable DeleteFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byContains deletes matching documents') + void byContainsMatch() { + new PgDB().withCloseable DeleteFunctions.&byContainsMatch + } + + @Test + @DisplayName('byContains succeeds when no documents match') + void byContainsNoMatch() { + new PgDB().withCloseable DeleteFunctions.&byContainsNoMatch + } + + @Test + @DisplayName('byJsonPath deletes matching documents') + void byJsonPathMatch() { + new PgDB().withCloseable DeleteFunctions.&byJsonPathMatch + } + + @Test + @DisplayName('byJsonPath succeeds when no documents match') + void byJsonPathNoMatch() { + new PgDB().withCloseable DeleteFunctions.&byJsonPathNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDocumentIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDocumentIT.groovy new file mode 100644 index 0000000..6b885f1 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLDocumentIT.groovy @@ -0,0 +1,65 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Document') +final class PostgreSQLDocumentIT { + + @Test + @DisplayName('insert works with default values') + void insertDefault() { + new PgDB().withCloseable DocumentFunctions.&insertDefault + } + + @Test + @DisplayName('insert fails with duplicate key') + void insertDupe() { + new PgDB().withCloseable DocumentFunctions.&insertDupe + } + + @Test + @DisplayName('insert succeeds with numeric auto IDs') + void insertNumAutoId() { + new PgDB().withCloseable DocumentFunctions.&insertNumAutoId + } + + @Test + @DisplayName('insert succeeds with UUID auto ID') + void insertUUIDAutoId() { + new PgDB().withCloseable DocumentFunctions.&insertUUIDAutoId + } + + @Test + @DisplayName('insert succeeds with random string auto ID') + void insertStringAutoId() { + new PgDB().withCloseable DocumentFunctions.&insertStringAutoId + } + + @Test + @DisplayName('save updates an existing document') + void saveMatch() { + new PgDB().withCloseable DocumentFunctions.&saveMatch + } + + @Test + @DisplayName('save inserts a new document') + void saveNoMatch() { + new PgDB().withCloseable DocumentFunctions.&saveNoMatch + } + + @Test + @DisplayName('update replaces an existing document') + void updateMatch() { + new PgDB().withCloseable DocumentFunctions.&updateMatch + } + + @Test + @DisplayName('update succeeds when no document exists') + void updateNoMatch() { + new PgDB().withCloseable DocumentFunctions.&updateNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLExistsIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLExistsIT.groovy new file mode 100644 index 0000000..83877cd --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLExistsIT.groovy @@ -0,0 +1,59 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Exists') +final class PostgreSQLExistsIT { + + @Test + @DisplayName('byId returns true when a document matches the ID') + void byIdMatch() { + new PgDB().withCloseable ExistsFunctions.&byIdMatch + } + + @Test + @DisplayName('byId returns false when no document matches the ID') + void byIdNoMatch() { + new PgDB().withCloseable ExistsFunctions.&byIdNoMatch + } + + @Test + @DisplayName('byFields returns true when documents match') + void byFieldsMatch() { + new PgDB().withCloseable ExistsFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields returns false when no documents match') + void byFieldsNoMatch() { + new PgDB().withCloseable ExistsFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byContains returns true when documents match') + void byContainsMatch() { + new PgDB().withCloseable ExistsFunctions.&byContainsMatch + } + + @Test + @DisplayName('byContains returns false when no documents match') + void byContainsNoMatch() { + new PgDB().withCloseable ExistsFunctions.&byContainsNoMatch + } + + @Test + @DisplayName('byJsonPath returns true when documents match') + void byJsonPathMatch() { + new PgDB().withCloseable ExistsFunctions.&byJsonPathMatch + } + + @Test + @DisplayName('byJsonPath returns false when no documents match') + void byJsonPathNoMatch() { + new PgDB().withCloseable ExistsFunctions.&byJsonPathNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLFindIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLFindIT.groovy new file mode 100644 index 0000000..d59cffc --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLFindIT.groovy @@ -0,0 +1,203 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Find') +final class PostgreSQLFindIT { + + @Test + @DisplayName('all retrieves all documents') + void allDefault() { + new PgDB().withCloseable FindFunctions.&allDefault + } + + @Test + @DisplayName('all sorts data ascending') + void allAscending() { + new PgDB().withCloseable FindFunctions.&allAscending + } + + @Test + @DisplayName('all sorts data descending') + void allDescending() { + new PgDB().withCloseable FindFunctions.&allDescending + } + + @Test + @DisplayName('all sorts data numerically') + void allNumOrder() { + new PgDB().withCloseable FindFunctions.&allNumOrder + } + + @Test + @DisplayName('all succeeds with an empty table') + void allEmpty() { + new PgDB().withCloseable FindFunctions.&allEmpty + } + + @Test + @DisplayName('byId retrieves a document via a string ID') + void byIdString() { + new PgDB().withCloseable FindFunctions.&byIdString + } + + @Test + @DisplayName('byId retrieves a document via a numeric ID') + void byIdNumber() { + new PgDB().withCloseable FindFunctions.&byIdNumber + } + + @Test + @DisplayName('byId returns null when a matching ID is not found') + void byIdNotFound() { + new PgDB().withCloseable FindFunctions.&byIdNotFound + } + + @Test + @DisplayName('byFields retrieves matching documents') + void byFieldsMatch() { + new PgDB().withCloseable FindFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields retrieves ordered matching documents') + void byFieldsMatchOrdered() { + new PgDB().withCloseable FindFunctions.&byFieldsMatchOrdered + } + + @Test + @DisplayName('byFields retrieves matching documents with a numeric IN clause') + void byFieldsMatchNumIn() { + new PgDB().withCloseable FindFunctions.&byFieldsMatchNumIn + } + + @Test + @DisplayName('byFields succeeds when no documents match') + void byFieldsNoMatch() { + new PgDB().withCloseable FindFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byFields retrieves matching documents with an IN_ARRAY comparison') + void byFieldsMatchInArray() { + new PgDB().withCloseable FindFunctions.&byFieldsMatchInArray + } + + @Test + @DisplayName('byFields succeeds when no documents match an IN_ARRAY comparison') + void byFieldsNoMatchInArray() { + new PgDB().withCloseable FindFunctions.&byFieldsNoMatchInArray + } + + @Test + @DisplayName('byContains retrieves matching documents') + void byContainsMatch() { + new PgDB().withCloseable FindFunctions.&byContainsMatch + } + + @Test + @DisplayName('byContains retrieves ordered matching documents') + void byContainsMatchOrdered() { + new PgDB().withCloseable FindFunctions.&byContainsMatchOrdered + } + + @Test + @DisplayName('byContains succeeds when no documents match') + void byContainsNoMatch() { + new PgDB().withCloseable FindFunctions.&byContainsNoMatch + } + + @Test + @DisplayName('byJsonPath retrieves matching documents') + void byJsonPathMatch() { + new PgDB().withCloseable FindFunctions.&byJsonPathMatch + } + + @Test + @DisplayName('byJsonPath retrieves ordered matching documents') + void byJsonPathMatchOrdered() { + new PgDB().withCloseable FindFunctions.&byJsonPathMatchOrdered + } + + @Test + @DisplayName('byJsonPath succeeds when no documents match') + void byJsonPathNoMatch() { + new PgDB().withCloseable FindFunctions.&byJsonPathNoMatch + } + + @Test + @DisplayName('firstByFields retrieves a matching document') + void firstByFieldsMatchOne() { + new PgDB().withCloseable FindFunctions.&firstByFieldsMatchOne + } + + @Test + @DisplayName('firstByFields retrieves a matching document among many') + void firstByFieldsMatchMany() { + new PgDB().withCloseable FindFunctions.&firstByFieldsMatchMany + } + + @Test + @DisplayName('firstByFields retrieves a matching document among many (ordered)') + void firstByFieldsMatchOrdered() { + new PgDB().withCloseable FindFunctions.&firstByFieldsMatchOrdered + } + + @Test + @DisplayName('firstByFields returns null when no document matches') + void firstByFieldsNoMatch() { + new PgDB().withCloseable FindFunctions.&firstByFieldsNoMatch + } + + @Test + @DisplayName('firstByContains retrieves a matching document') + void firstByContainsMatchOne() { + new PgDB().withCloseable FindFunctions.&firstByContainsMatchOne + } + + @Test + @DisplayName('firstByContains retrieves a matching document among many') + void firstByContainsMatchMany() { + new PgDB().withCloseable FindFunctions.&firstByContainsMatchMany + } + + @Test + @DisplayName('firstByContains retrieves a matching document among many (ordered)') + void firstByContainsMatchOrdered() { + new PgDB().withCloseable FindFunctions.&firstByContainsMatchOrdered + } + + @Test + @DisplayName('firstByContains returns null when no document matches') + void firstByContainsNoMatch() { + new PgDB().withCloseable FindFunctions.&firstByContainsNoMatch + } + + @Test + @DisplayName('firstByJsonPath retrieves a matching document') + void firstByJsonPathMatchOne() { + new PgDB().withCloseable FindFunctions.&firstByJsonPathMatchOne + } + + @Test + @DisplayName('firstByJsonPath retrieves a matching document among many') + void firstByJsonPathMatchMany() { + new PgDB().withCloseable FindFunctions.&firstByJsonPathMatchMany + } + + @Test + @DisplayName('firstByJsonPath retrieves a matching document among many (ordered)') + void firstByJsonPathMatchOrdered() { + new PgDB().withCloseable FindFunctions.&firstByJsonPathMatchOrdered + } + + @Test + @DisplayName('firstByJsonPath returns null when no document matches') + void firstByJsonPathNoMatch() { + new PgDB().withCloseable FindFunctions.&firstByJsonPathNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLJsonIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLJsonIT.groovy new file mode 100644 index 0000000..690e401 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLJsonIT.groovy @@ -0,0 +1,359 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Json') +final class PostgreSQLJsonIT { + + @Test + @DisplayName('all retrieves all documents') + void allDefault() { + new PgDB().withCloseable JsonFunctions.&allDefault + } + + @Test + @DisplayName('all succeeds with an empty table') + void allEmpty() { + new PgDB().withCloseable JsonFunctions.&allEmpty + } + + @Test + @DisplayName('byId retrieves a document via a string ID') + void byIdString() { + new PgDB().withCloseable JsonFunctions.&byIdString + } + + @Test + @DisplayName('byId retrieves a document via a numeric ID') + void byIdNumber() { + new PgDB().withCloseable JsonFunctions.&byIdNumber + } + + @Test + @DisplayName('byId returns null when a matching ID is not found') + void byIdNotFound() { + new PgDB().withCloseable JsonFunctions.&byIdNotFound + } + + @Test + @DisplayName('byFields retrieves matching documents') + void byFieldsMatch() { + new PgDB().withCloseable JsonFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields retrieves ordered matching documents') + void byFieldsMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&byFieldsMatchOrdered + } + + @Test + @DisplayName('byFields retrieves matching documents with a numeric IN clause') + void byFieldsMatchNumIn() { + new PgDB().withCloseable JsonFunctions.&byFieldsMatchNumIn + } + + @Test + @DisplayName('byFields succeeds when no documents match') + void byFieldsNoMatch() { + new PgDB().withCloseable JsonFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byFields retrieves matching documents with an IN_ARRAY comparison') + void byFieldsMatchInArray() { + new PgDB().withCloseable JsonFunctions.&byFieldsMatchInArray + } + + @Test + @DisplayName('byFields succeeds when no documents match an IN_ARRAY comparison') + void byFieldsNoMatchInArray() { + new PgDB().withCloseable JsonFunctions.&byFieldsNoMatchInArray + } + + @Test + @DisplayName('byContains retrieves matching documents') + void byContainsMatch() { + new PgDB().withCloseable JsonFunctions.&byContainsMatch + } + + @Test + @DisplayName('byContains retrieves ordered matching documents') + void byContainsMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&byContainsMatchOrdered + } + + @Test + @DisplayName('byContains succeeds when no documents match') + void byContainsNoMatch() { + new PgDB().withCloseable JsonFunctions.&byContainsNoMatch + } + + @Test + @DisplayName('byJsonPath retrieves matching documents') + void byJsonPathMatch() { + new PgDB().withCloseable JsonFunctions.&byJsonPathMatch + } + + @Test + @DisplayName('byJsonPath retrieves ordered matching documents') + void byJsonPathMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&byJsonPathMatchOrdered + } + + @Test + @DisplayName('byJsonPath succeeds when no documents match') + void byJsonPathNoMatch() { + new PgDB().withCloseable JsonFunctions.&byJsonPathNoMatch + } + + @Test + @DisplayName('firstByFields retrieves a matching document') + void firstByFieldsMatchOne() { + new PgDB().withCloseable JsonFunctions.&firstByFieldsMatchOne + } + + @Test + @DisplayName('firstByFields retrieves a matching document among many') + void firstByFieldsMatchMany() { + new PgDB().withCloseable JsonFunctions.&firstByFieldsMatchMany + } + + @Test + @DisplayName('firstByFields retrieves a matching document among many (ordered)') + void firstByFieldsMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&firstByFieldsMatchOrdered + } + + @Test + @DisplayName('firstByFields returns null when no document matches') + void firstByFieldsNoMatch() { + new PgDB().withCloseable JsonFunctions.&firstByFieldsNoMatch + } + + @Test + @DisplayName('firstByContains retrieves a matching document') + void firstByContainsMatchOne() { + new PgDB().withCloseable JsonFunctions.&firstByContainsMatchOne + } + + @Test + @DisplayName('firstByContains retrieves a matching document among many') + void firstByContainsMatchMany() { + new PgDB().withCloseable JsonFunctions.&firstByContainsMatchMany + } + + @Test + @DisplayName('firstByContains retrieves a matching document among many (ordered)') + void firstByContainsMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&firstByContainsMatchOrdered + } + + @Test + @DisplayName('firstByContains returns null when no document matches') + void firstByContainsNoMatch() { + new PgDB().withCloseable JsonFunctions.&firstByContainsNoMatch + } + + @Test + @DisplayName('firstByJsonPath retrieves a matching document') + void firstByJsonPathMatchOne() { + new PgDB().withCloseable JsonFunctions.&firstByJsonPathMatchOne + } + + @Test + @DisplayName('firstByJsonPath retrieves a matching document among many') + void firstByJsonPathMatchMany() { + new PgDB().withCloseable JsonFunctions.&firstByJsonPathMatchMany + } + + @Test + @DisplayName('firstByJsonPath retrieves a matching document among many (ordered)') + void firstByJsonPathMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&firstByJsonPathMatchOrdered + } + + @Test + @DisplayName('firstByJsonPath returns null when no document matches') + void firstByJsonPathNoMatch() { + new PgDB().withCloseable JsonFunctions.&firstByJsonPathNoMatch + } + + @Test + @DisplayName('writeAll retrieves all documents') + void writeAllDefault() { + new PgDB().withCloseable JsonFunctions.&writeAllDefault + } + + @Test + @DisplayName('writeAll succeeds with an empty table') + void writeAllEmpty() { + new PgDB().withCloseable JsonFunctions.&writeAllEmpty + } + + @Test + @DisplayName('writeById retrieves a document via a string ID') + void writeByIdString() { + new PgDB().withCloseable JsonFunctions.&writeByIdString + } + + @Test + @DisplayName('writeById retrieves a document via a numeric ID') + void writeByIdNumber() { + new PgDB().withCloseable JsonFunctions.&writeByIdNumber + } + + @Test + @DisplayName('writeById returns null when a matching ID is not found') + void writeByIdNotFound() { + new PgDB().withCloseable JsonFunctions.&writeByIdNotFound + } + + @Test + @DisplayName('writeByFields retrieves matching documents') + void writeByFieldsMatch() { + new PgDB().withCloseable JsonFunctions.&writeByFieldsMatch + } + + @Test + @DisplayName('writeByFields retrieves ordered matching documents') + void writeByFieldsMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&writeByFieldsMatchOrdered + } + + @Test + @DisplayName('writeByFields retrieves matching documents with a numeric IN clause') + void writeByFieldsMatchNumIn() { + new PgDB().withCloseable JsonFunctions.&writeByFieldsMatchNumIn + } + + @Test + @DisplayName('writeByFields succeeds when no documents match') + void writeByFieldsNoMatch() { + new PgDB().withCloseable JsonFunctions.&writeByFieldsNoMatch + } + + @Test + @DisplayName('writeByFields retrieves matching documents with an IN_ARRAY comparison') + void writeByFieldsMatchInArray() { + new PgDB().withCloseable JsonFunctions.&writeByFieldsMatchInArray + } + + @Test + @DisplayName('writeByFields succeeds when no documents match an IN_ARRAY comparison') + void writeByFieldsNoMatchInArray() { + new PgDB().withCloseable JsonFunctions.&writeByFieldsNoMatchInArray + } + + @Test + @DisplayName('writeByContains retrieves matching documents') + void writeByContainsMatch() { + new PgDB().withCloseable JsonFunctions.&writeByContainsMatch + } + + @Test + @DisplayName('writeByContains retrieves ordered matching documents') + void writeByContainsMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&writeByContainsMatchOrdered + } + + @Test + @DisplayName('writeByContains succeeds when no documents match') + void writeByContainsNoMatch() { + new PgDB().withCloseable JsonFunctions.&writeByContainsNoMatch + } + + @Test + @DisplayName('writeByJsonPath retrieves matching documents') + void writeByJsonPathMatch() { + new PgDB().withCloseable JsonFunctions.&writeByJsonPathMatch + } + + @Test + @DisplayName('writeByJsonPath retrieves ordered matching documents') + void writeByJsonPathMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&writeByJsonPathMatchOrdered + } + + @Test + @DisplayName('writeByJsonPath succeeds when no documents match') + void writeByJsonPathNoMatch() { + new PgDB().withCloseable JsonFunctions.&writeByJsonPathNoMatch + } + + @Test + @DisplayName('writeFirstByFields retrieves a matching document') + void writeFirstByFieldsMatchOne() { + new PgDB().withCloseable JsonFunctions.&writeFirstByFieldsMatchOne + } + + @Test + @DisplayName('writeFirstByFields retrieves a matching document among many') + void writeFirstByFieldsMatchMany() { + new PgDB().withCloseable JsonFunctions.&writeFirstByFieldsMatchMany + } + + @Test + @DisplayName('writeFirstByFields retrieves a matching document among many (ordered)') + void writeFirstByFieldsMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&writeFirstByFieldsMatchOrdered + } + + @Test + @DisplayName('writeFirstByFields returns null when no document matches') + void writeFirstByFieldsNoMatch() { + new PgDB().withCloseable JsonFunctions.&writeFirstByFieldsNoMatch + } + + @Test + @DisplayName('writeFirstByContains retrieves a matching document') + void writeFirstByContainsMatchOne() { + new PgDB().withCloseable JsonFunctions.&writeFirstByContainsMatchOne + } + + @Test + @DisplayName('writeFirstByContains retrieves a matching document among many') + void writeFirstByContainsMatchMany() { + new PgDB().withCloseable JsonFunctions.&writeFirstByContainsMatchMany + } + + @Test + @DisplayName('writeFirstByContains retrieves a matching document among many (ordered)') + void writeFirstByContainsMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&writeFirstByContainsMatchOrdered + } + + @Test + @DisplayName('writeFirstByContains returns null when no document matches') + void writeFirstByContainsNoMatch() { + new PgDB().withCloseable JsonFunctions.&writeFirstByContainsNoMatch + } + + @Test + @DisplayName('writeFirstByJsonPath retrieves a matching document') + void writeFirstByJsonPathMatchOne() { + new PgDB().withCloseable JsonFunctions.&writeFirstByJsonPathMatchOne + } + + @Test + @DisplayName('writeFirstByJsonPath retrieves a matching document among many') + void writeFirstByJsonPathMatchMany() { + new PgDB().withCloseable JsonFunctions.&writeFirstByJsonPathMatchMany + } + + @Test + @DisplayName('writeFirstByJsonPath retrieves a matching document among many (ordered)') + void writeFirstByJsonPathMatchOrdered() { + new PgDB().withCloseable JsonFunctions.&writeFirstByJsonPathMatchOrdered + } + + @Test + @DisplayName('writeFirstByJsonPath returns null when no document matches') + void writeFirstByJsonPathNoMatch() { + new PgDB().withCloseable JsonFunctions.&writeFirstByJsonPathNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLPatchIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLPatchIT.groovy new file mode 100644 index 0000000..312e6dc --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLPatchIT.groovy @@ -0,0 +1,59 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: Patch') +final class PostgreSQLPatchIT { + + @Test + @DisplayName('byId patches an existing document') + void byIdMatch() { + new PgDB().withCloseable PatchFunctions.&byIdMatch + } + + @Test + @DisplayName('byId succeeds for a non-existent document') + void byIdNoMatch() { + new PgDB().withCloseable PatchFunctions.&byIdNoMatch + } + + @Test + @DisplayName('byFields patches matching document') + void byFieldsMatch() { + new PgDB().withCloseable PatchFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields succeeds when no documents match') + void byFieldsNoMatch() { + new PgDB().withCloseable PatchFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byContains patches matching document') + void byContainsMatch() { + new PgDB().withCloseable PatchFunctions.&byContainsMatch + } + + @Test + @DisplayName('byContains succeeds when no documents match') + void byContainsNoMatch() { + new PgDB().withCloseable PatchFunctions.&byContainsNoMatch + } + + @Test + @DisplayName('byJsonPath patches matching document') + void byJsonPathMatch() { + new PgDB().withCloseable PatchFunctions.&byJsonPathMatch + } + + @Test + @DisplayName('byJsonPath succeeds when no documents match') + void byJsonPathNoMatch() { + new PgDB().withCloseable PatchFunctions.&byJsonPathNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLRemoveFieldsIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLRemoveFieldsIT.groovy new file mode 100644 index 0000000..6a4dac3 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLRemoveFieldsIT.groovy @@ -0,0 +1,83 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName('Groovy | PostgreSQL: RemoveFields') +final class PostgreSQLRemoveFieldsIT { + + @Test + @DisplayName('byId removes fields from an existing document') + void byIdMatchFields() { + new PgDB().withCloseable RemoveFieldsFunctions.&byIdMatchFields + } + + @Test + @DisplayName('byId succeeds when fields do not exist on an existing document') + void byIdMatchNoFields() { + new PgDB().withCloseable RemoveFieldsFunctions.&byIdMatchNoFields + } + + @Test + @DisplayName('byId succeeds when no document exists') + void byIdNoMatch() { + new PgDB().withCloseable RemoveFieldsFunctions.&byIdNoMatch + } + + @Test + @DisplayName('byFields removes fields from matching documents') + void byFieldsMatchFields() { + new PgDB().withCloseable RemoveFieldsFunctions.&byFieldsMatchFields + } + + @Test + @DisplayName('byFields succeeds when fields do not exist on matching documents') + void byFieldsMatchNoFields() { + new PgDB().withCloseable RemoveFieldsFunctions.&byFieldsMatchNoFields + } + + @Test + @DisplayName('byFields succeeds when no matching documents exist') + void byFieldsNoMatch() { + new PgDB().withCloseable RemoveFieldsFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byContains removes fields from matching documents') + void byContainsMatchFields() { + new PgDB().withCloseable RemoveFieldsFunctions.&byContainsMatchFields + } + + @Test + @DisplayName('byContains succeeds when fields do not exist on matching documents') + void byContainsMatchNoFields() { + new PgDB().withCloseable RemoveFieldsFunctions.&byContainsMatchNoFields + } + + @Test + @DisplayName('byContains succeeds when no matching documents exist') + void byContainsNoMatch() { + new PgDB().withCloseable RemoveFieldsFunctions.&byContainsNoMatch + } + + @Test + @DisplayName('byJsonPath removes fields from matching documents') + void byJsonPathMatchFields() { + new PgDB().withCloseable RemoveFieldsFunctions.&byJsonPathMatchFields + } + + @Test + @DisplayName('byJsonPath succeeds when fields do not exist on matching documents') + void byJsonPathMatchNoFields() { + new PgDB().withCloseable RemoveFieldsFunctions.&byJsonPathMatchNoFields + } + + @Test + @DisplayName('byJsonPath succeeds when no matching documents exist') + void byJsonPathNoMatch() { + new PgDB().withCloseable RemoveFieldsFunctions.&byJsonPathNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/RemoveFieldsFunctions.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/RemoveFieldsFunctions.groovy new file mode 100644 index 0000000..8182d62 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/RemoveFieldsFunctions.groovy @@ -0,0 +1,104 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Field + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +final class RemoveFieldsFunctions { + + static void byIdMatchFields(ThrowawayDatabase db) { + JsonDocument.load db + db.conn.removeFieldsById TEST_TABLE, 'two', List.of('sub', 'value') + def doc = db.conn.findById TEST_TABLE, 'two', JsonDocument + assertTrue doc.isPresent(), 'There should have been a document returned' + assertEquals '', doc.get().value, 'The value should have been empty' + assertNull doc.get().sub, 'The sub-document should have been removed' + } + + static void byIdMatchNoFields(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse db.conn.existsByFields(TEST_TABLE, List.of(Field.exists('a_field_that_does_not_exist'))) + db.conn.removeFieldsById TEST_TABLE, 'one', List.of('a_field_that_does_not_exist') // no exception = pass + } + + static void byIdNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse db.conn.existsById(TEST_TABLE, 'fifty') + db.conn.removeFieldsById TEST_TABLE, 'fifty', List.of('sub') // no exception = pass + } + + static void byFieldsMatchFields(ThrowawayDatabase db) { + JsonDocument.load db + def fields = List.of Field.equal('numValue', 17) + db.conn.removeFieldsByFields TEST_TABLE, fields, List.of('sub') + def doc = db.conn.findFirstByFields TEST_TABLE, fields, JsonDocument + assertTrue doc.isPresent(), 'The document should have been returned' + assertEquals 'four', doc.get().id, 'An incorrect document was returned' + assertNull doc.get().sub, 'The sub-document should have been removed' + } + + static void byFieldsMatchNoFields(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse db.conn.existsByFields(TEST_TABLE, List.of(Field.exists('nada'))) + db.conn.removeFieldsByFields TEST_TABLE, List.of(Field.equal('numValue', 17)), List.of('nada') // no exn = pass + } + + static void byFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + def fields = List.of Field.notEqual('missing', 'nope') + assertFalse db.conn.existsByFields(TEST_TABLE, fields) + db.conn.removeFieldsByFields TEST_TABLE, fields, List.of('value') // no exception = pass + } + + static void byContainsMatchFields(ThrowawayDatabase db) { + JsonDocument.load db + def criteria = Map.of('sub', Map.of('foo', 'green')) + db.conn.removeFieldsByContains TEST_TABLE, criteria, List.of('value') + def docs = db.conn.findByContains TEST_TABLE, criteria, JsonDocument + assertEquals 2, docs.size(), 'There should have been 2 documents returned' + docs.forEach { + assertTrue List.of('two', 'four').contains(it.id), "An incorrect document was returned (${it.id})" + assertEquals '', it.value, 'The value should have been empty' + } + } + + static void byContainsMatchNoFields(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse db.conn.existsByFields(TEST_TABLE, List.of(Field.exists('invalid_field'))) + db.conn.removeFieldsByContains TEST_TABLE, Map.of('sub', Map.of('foo', 'green')), List.of('invalid_field') + // no exception = pass + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + def contains = Map.of 'value', 'substantial' + assertFalse db.conn.existsByContains(TEST_TABLE, contains) + db.conn.removeFieldsByContains TEST_TABLE, contains, List.of('numValue') + } + + static void byJsonPathMatchFields(ThrowawayDatabase db) { + JsonDocument.load db + def path = '$.value ? (@ == "purple")' + db.conn.removeFieldsByJsonPath TEST_TABLE, path, List.of('sub') + def docs = db.conn.findByJsonPath TEST_TABLE, path, JsonDocument + assertEquals 2, docs.size(), 'There should have been 2 documents returned' + docs.forEach { + assertTrue List.of('four', 'five').contains(it.id), "An incorrect document was returned (${it.id})" + assertNull it.sub, 'The sub-document should have been removed' + } + } + + static void byJsonPathMatchNoFields(ThrowawayDatabase db) { + JsonDocument.load db + assertFalse db.conn.existsByFields(TEST_TABLE, List.of(Field.exists('submarine'))) + db.conn.removeFieldsByJsonPath TEST_TABLE, '$.value ? (@ == "purple")', List.of('submarine') // no exn = pass + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load db + def path = '$.value ? (@ == "mauve")' + assertFalse db.conn.existsByJsonPath(TEST_TABLE, path) + db.conn.removeFieldsByJsonPath TEST_TABLE, path, List.of('value') // no exception = pass + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCountIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCountIT.groovy new file mode 100644 index 0000000..8afab08 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCountIT.groovy @@ -0,0 +1,48 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("Groovy | SQLite: Count") +final class SQLiteCountIT { + + @Test + @DisplayName("all counts all documents") + void all() { + new SQLiteDB().withCloseable CountFunctions.&all + } + + @Test + @DisplayName("byFields counts documents by a numeric value") + void byFieldsNumeric() { + new SQLiteDB().withCloseable CountFunctions.&byFieldsNumeric + } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + void byFieldsAlpha() { + new SQLiteDB().withCloseable CountFunctions.&byFieldsAlpha + } + + @Test + @DisplayName("byContains fails") + void byContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { CountFunctions.byContainsMatch db } + } + } + + @Test + @DisplayName("byJsonPath fails") + void byJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { CountFunctions.byJsonPathMatch db } + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCustomIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCustomIT.groovy new file mode 100644 index 0000000..517e011 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCustomIT.groovy @@ -0,0 +1,89 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * SQLite integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName('Groovy | SQLite: Custom') +final class SQLiteCustomIT { + + @Test + @DisplayName('list succeeds with empty list') + void listEmpty() { + new SQLiteDB().withCloseable CustomFunctions.&listEmpty + } + + @Test + @DisplayName('list succeeds with a non-empty list') + void listAll() { + new SQLiteDB().withCloseable CustomFunctions.&listAll + } + + @Test + @DisplayName('jsonArray succeeds with empty array') + void jsonArrayEmpty() { + new SQLiteDB().withCloseable CustomFunctions.&jsonArrayEmpty + } + + @Test + @DisplayName('jsonArray succeeds with a single-item array') + void jsonArraySingle() { + new SQLiteDB().withCloseable CustomFunctions.&jsonArraySingle + } + + @Test + @DisplayName('jsonArray succeeds with a multi-item array') + void jsonArrayMany() { + new SQLiteDB().withCloseable CustomFunctions.&jsonArrayMany + } + + @Test + @DisplayName('writeJsonArray succeeds with empty array') + void writeJsonArrayEmpty() { + new SQLiteDB().withCloseable CustomFunctions.&writeJsonArrayEmpty + } + + @Test + @DisplayName('writeJsonArray succeeds with a single-item array') + void writeJsonArraySingle() { + new SQLiteDB().withCloseable CustomFunctions.&writeJsonArraySingle + } + + @Test + @DisplayName('writeJsonArray succeeds with a multi-item array') + void writeJsonArrayMany() { + new SQLiteDB().withCloseable CustomFunctions.&writeJsonArrayMany + } + + @Test + @DisplayName('single succeeds when document not found') + void singleNone() { + new SQLiteDB().withCloseable CustomFunctions.&singleNone + } + + @Test + @DisplayName('single succeeds when a document is found') + void singleOne() { + new SQLiteDB().withCloseable CustomFunctions.&singleOne + } + + @Test + @DisplayName('nonQuery makes changes') + void nonQueryChanges() { + new SQLiteDB().withCloseable CustomFunctions.&nonQueryChanges + } + + @Test + @DisplayName('nonQuery makes no changes when where clause matches nothing') + void nonQueryNoChanges() { + new SQLiteDB().withCloseable CustomFunctions.&nonQueryNoChanges + } + + @Test + @DisplayName('scalar succeeds') + void scalar() { + new SQLiteDB().withCloseable CustomFunctions.&scalar + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDB.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDB.groovy new file mode 100644 index 0000000..580f3fc --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDB.groovy @@ -0,0 +1,35 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.java.DocumentConfig +import solutions.bitbadger.documents.java.Results + +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE + +/** + * A wrapper for a throwaway SQLite database + */ +class SQLiteDB implements ThrowawayDatabase { + + SQLiteDB() { + Configuration.setConnectionString "jdbc:sqlite:${dbName}.db" + conn = Configuration.dbConn() + conn.ensureTable TEST_TABLE + + // Use a Jackson-based document serializer for testing + DocumentConfig.serializer = new JacksonDocumentSerializer() + } + + void close() { + conn.close() + new File("${dbName}.db").delete() + Configuration.setConnectionString null + } + + boolean dbObjectExists(String name) { + conn.customScalar("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = :name) AS it", + List.of(new Parameter(":name", ParameterType.STRING, name)), Boolean, Results.&toExists) + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDefinitionIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDefinitionIT.groovy new file mode 100644 index 0000000..1e03d79 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDefinitionIT.groovy @@ -0,0 +1,42 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName('Groovy | SQLite: Definition') +final class SQLiteDefinitionIT { + + @Test + @DisplayName('ensureTable creates table and index') + void ensureTable() { + new SQLiteDB().withCloseable DefinitionFunctions.&ensureTable + } + + @Test + @DisplayName('ensureFieldIndex creates an index') + void ensureFieldIndex() { + new SQLiteDB().withCloseable DefinitionFunctions.&ensureFieldIndex + } + + @Test + @DisplayName('ensureDocumentIndex fails for full index') + void ensureDocumentIndexFull() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { DefinitionFunctions::ensureDocumentIndexFull db } + } + } + + @Test + @DisplayName('ensureDocumentIndex fails for optimized index') + void ensureDocumentIndexOptimized() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { DefinitionFunctions::ensureDocumentIndexOptimized db } + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDeleteIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDeleteIT.groovy new file mode 100644 index 0000000..eb08f3a --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDeleteIT.groovy @@ -0,0 +1,54 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName('Groovy | SQLite: Delete') +final class SQLiteDeleteIT { + + @Test + @DisplayName('byId deletes a matching ID') + void byIdMatch() { + new SQLiteDB().withCloseable DeleteFunctions.&byIdMatch + } + + @Test + @DisplayName('byId succeeds when no ID matches') + void byIdNoMatch() { + new SQLiteDB().withCloseable DeleteFunctions.&byIdNoMatch + } + + @Test + @DisplayName('byFields deletes matching documents') + void byFieldsMatch() { + new SQLiteDB().withCloseable DeleteFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields succeeds when no documents match') + void byFieldsNoMatch() { + new SQLiteDB().withCloseable DeleteFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byContains fails') + void byContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { DeleteFunctions.byContainsMatch db } + } + } + + @Test + @DisplayName('byJsonPath fails') + void byJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { DeleteFunctions.byJsonPathMatch db } + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDocumentIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDocumentIT.groovy new file mode 100644 index 0000000..2d919c3 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteDocumentIT.groovy @@ -0,0 +1,65 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +/** + * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName('Groovy | SQLite: Document') +final class SQLiteDocumentIT { + + @Test + @DisplayName('insert works with default values') + void insertDefault() { + new SQLiteDB().withCloseable DocumentFunctions.&insertDefault + } + + @Test + @DisplayName('insert fails with duplicate key') + void insertDupe() { + new SQLiteDB().withCloseable DocumentFunctions.&insertDupe + } + + @Test + @DisplayName('insert succeeds with numeric auto IDs') + void insertNumAutoId() { + new SQLiteDB().withCloseable DocumentFunctions.&insertNumAutoId + } + + @Test + @DisplayName('insert succeeds with UUID auto ID') + void insertUUIDAutoId() { + new SQLiteDB().withCloseable DocumentFunctions.&insertUUIDAutoId + } + + @Test + @DisplayName('insert succeeds with random string auto ID') + void insertStringAutoId() { + new SQLiteDB().withCloseable DocumentFunctions.&insertStringAutoId + } + + @Test + @DisplayName('save updates an existing document') + void saveMatch() { + new SQLiteDB().withCloseable DocumentFunctions.&saveMatch + } + + @Test + @DisplayName('save inserts a new document') + void saveNoMatch() { + new SQLiteDB().withCloseable DocumentFunctions.&saveNoMatch + } + + @Test + @DisplayName('update replaces an existing document') + void updateMatch() { + new SQLiteDB().withCloseable DocumentFunctions.&updateMatch + } + + @Test + @DisplayName('update succeeds when no document exists') + void updateNoMatch() { + new SQLiteDB().withCloseable DocumentFunctions.&updateNoMatch + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteExistsIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteExistsIT.groovy new file mode 100644 index 0000000..9915afb --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteExistsIT.groovy @@ -0,0 +1,54 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName('Groovy | SQLite: Exists') +final class SQLiteExistsIT { + + @Test + @DisplayName('byId returns true when a document matches the ID') + void byIdMatch() { + new SQLiteDB().withCloseable ExistsFunctions.&byIdMatch + } + + @Test + @DisplayName('byId returns false when no document matches the ID') + void byIdNoMatch() { + new SQLiteDB().withCloseable ExistsFunctions.&byIdNoMatch + } + + @Test + @DisplayName('byFields returns true when documents match') + void byFieldsMatch() { + new SQLiteDB().withCloseable ExistsFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields returns false when no documents match') + void byFieldsNoMatch() { + new SQLiteDB().withCloseable ExistsFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byContains fails') + void byContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { ExistsFunctions.byContainsMatch db } + } + } + + @Test + @DisplayName('byJsonPath fails') + void byJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { ExistsFunctions.byJsonPathMatch db } + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteFindIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteFindIT.groovy new file mode 100644 index 0000000..1c4da1c --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteFindIT.groovy @@ -0,0 +1,154 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName('Groovy | SQLite: Find') +final class SQLiteFindIT { + + @Test + @DisplayName('all retrieves all documents') + void allDefault() { + new SQLiteDB().withCloseable FindFunctions.&allDefault + } + + @Test + @DisplayName('all sorts data ascending') + void allAscending() { + new SQLiteDB().withCloseable FindFunctions.&allAscending + } + + @Test + @DisplayName('all sorts data descending') + void allDescending() { + new SQLiteDB().withCloseable FindFunctions.&allDescending + } + + @Test + @DisplayName('all sorts data numerically') + void allNumOrder() { + new SQLiteDB().withCloseable FindFunctions.&allNumOrder + } + + @Test + @DisplayName('all succeeds with an empty table') + void allEmpty() { + new SQLiteDB().withCloseable FindFunctions.&allEmpty + } + + @Test + @DisplayName('byId retrieves a document via a string ID') + void byIdString() { + new SQLiteDB().withCloseable FindFunctions.&byIdString + } + + @Test + @DisplayName('byId retrieves a document via a numeric ID') + void byIdNumber() { + new SQLiteDB().withCloseable FindFunctions.&byIdNumber + } + + @Test + @DisplayName('byId returns null when a matching ID is not found') + void byIdNotFound() { + new SQLiteDB().withCloseable FindFunctions.&byIdNotFound + } + + @Test + @DisplayName('byFields retrieves matching documents') + void byFieldsMatch() { + new SQLiteDB().withCloseable FindFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields retrieves ordered matching documents') + void byFieldsMatchOrdered() { + new SQLiteDB().withCloseable FindFunctions.&byFieldsMatchOrdered + } + + @Test + @DisplayName('byFields retrieves matching documents with a numeric IN clause') + void byFieldsMatchNumIn() { + new SQLiteDB().withCloseable FindFunctions.&byFieldsMatchNumIn + } + + @Test + @DisplayName('byFields succeeds when no documents match') + void byFieldsNoMatch() { + new SQLiteDB().withCloseable FindFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byFields retrieves matching documents with an IN_ARRAY comparison') + void byFieldsMatchInArray() { + new SQLiteDB().withCloseable FindFunctions.&byFieldsMatchInArray + } + + @Test + @DisplayName('byFields succeeds when no documents match an IN_ARRAY comparison') + void byFieldsNoMatchInArray() { + new SQLiteDB().withCloseable FindFunctions.&byFieldsNoMatchInArray + } + + @Test + @DisplayName('byContains fails') + void byContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { FindFunctions.byContainsMatch db } + } + } + + @Test + @DisplayName('byJsonPath fails') + void byJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { FindFunctions.byJsonPathMatch db } + } + } + + @Test + @DisplayName('firstByFields retrieves a matching document') + void firstByFieldsMatchOne() { + new SQLiteDB().withCloseable FindFunctions.&firstByFieldsMatchOne + } + + @Test + @DisplayName('firstByFields retrieves a matching document among many') + void firstByFieldsMatchMany() { + new SQLiteDB().withCloseable FindFunctions.&firstByFieldsMatchMany + } + + @Test + @DisplayName('firstByFields retrieves a matching document among many (ordered)') + void firstByFieldsMatchOrdered() { + new SQLiteDB().withCloseable FindFunctions.&firstByFieldsMatchOrdered + } + + @Test + @DisplayName('firstByFields returns null when no document matches') + void firstByFieldsNoMatch() { + new SQLiteDB().withCloseable FindFunctions.&firstByFieldsNoMatch + } + + @Test + @DisplayName('firstByContains fails') + void firstByContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { FindFunctions.firstByContainsMatchOne db } + } + } + + @Test + @DisplayName('firstByJsonPath fails') + void firstByJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { FindFunctions.firstByJsonPathMatchOne db } + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteJsonIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteJsonIT.groovy new file mode 100644 index 0000000..3e81c59 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteJsonIT.groovy @@ -0,0 +1,258 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName('Groovy | SQLite: Json') +final class SQLiteJsonIT { + + @Test + @DisplayName('all retrieves all documents') + void allDefault() { + new SQLiteDB().withCloseable JsonFunctions.&allDefault + } + + @Test + @DisplayName('all succeeds with an empty table') + void allEmpty() { + new SQLiteDB().withCloseable JsonFunctions.&allEmpty + } + + @Test + @DisplayName('byId retrieves a document via a string ID') + void byIdString() { + new SQLiteDB().withCloseable JsonFunctions.&byIdString + } + + @Test + @DisplayName('byId retrieves a document via a numeric ID') + void byIdNumber() { + new SQLiteDB().withCloseable JsonFunctions.&byIdNumber + } + + @Test + @DisplayName('byId returns null when a matching ID is not found') + void byIdNotFound() { + new SQLiteDB().withCloseable JsonFunctions.&byIdNotFound + } + + @Test + @DisplayName('byFields retrieves matching documents') + void byFieldsMatch() { + new SQLiteDB().withCloseable JsonFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields retrieves ordered matching documents') + void byFieldsMatchOrdered() { + new SQLiteDB().withCloseable JsonFunctions.&byFieldsMatchOrdered + } + + @Test + @DisplayName('byFields retrieves matching documents with a numeric IN clause') + void byFieldsMatchNumIn() { + new SQLiteDB().withCloseable JsonFunctions.&byFieldsMatchNumIn + } + + @Test + @DisplayName('byFields succeeds when no documents match') + void byFieldsNoMatch() { + new SQLiteDB().withCloseable JsonFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byFields retrieves matching documents with an IN_ARRAY comparison') + void byFieldsMatchInArray() { + new SQLiteDB().withCloseable JsonFunctions.&byFieldsMatchInArray + } + + @Test + @DisplayName('byFields succeeds when no documents match an IN_ARRAY comparison') + void byFieldsNoMatchInArray() { + new SQLiteDB().withCloseable JsonFunctions.&byFieldsNoMatchInArray + } + + @Test + @DisplayName('byContains fails') + void byContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { JsonFunctions.byContainsMatch db } + } + } + + @Test + @DisplayName('byJsonPath fails') + void byJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { JsonFunctions.byJsonPathMatch db } + } + } + + @Test + @DisplayName('firstByFields retrieves a matching document') + void firstByFieldsMatchOne() { + new SQLiteDB().withCloseable JsonFunctions.&firstByFieldsMatchOne + } + + @Test + @DisplayName('firstByFields retrieves a matching document among many') + void firstByFieldsMatchMany() { + new SQLiteDB().withCloseable JsonFunctions.&firstByFieldsMatchMany + } + + @Test + @DisplayName('firstByFields retrieves a matching document among many (ordered)') + void firstByFieldsMatchOrdered() { + new SQLiteDB().withCloseable JsonFunctions.&firstByFieldsMatchOrdered + } + + @Test + @DisplayName('firstByFields returns null when no document matches') + void firstByFieldsNoMatch() { + new SQLiteDB().withCloseable JsonFunctions.&firstByFieldsNoMatch + } + + @Test + @DisplayName('firstByContains fails') + void firstByContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { JsonFunctions.firstByContainsMatchOne db } + } + } + + @Test + @DisplayName('firstByJsonPath fails') + void firstByJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { JsonFunctions.firstByJsonPathMatchOne db } + } + } + + @Test + @DisplayName('writeAll retrieves all documents') + void writeAllDefault() { + new SQLiteDB().withCloseable JsonFunctions.&writeAllDefault + } + + @Test + @DisplayName('writeAll succeeds with an empty table') + void writeAllEmpty() { + new SQLiteDB().withCloseable JsonFunctions.&writeAllEmpty + } + + @Test + @DisplayName('writeById retrieves a document via a string ID') + void writeByIdString() { + new SQLiteDB().withCloseable JsonFunctions.&writeByIdString + } + + @Test + @DisplayName('writeById retrieves a document via a numeric ID') + void writeByIdNumber() { + new SQLiteDB().withCloseable JsonFunctions.&writeByIdNumber + } + + @Test + @DisplayName('writeById returns null when a matching ID is not found') + void writeByIdNotFound() { + new SQLiteDB().withCloseable JsonFunctions.&writeByIdNotFound + } + + @Test + @DisplayName('writeByFields retrieves matching documents') + void writeByFieldsMatch() { + new SQLiteDB().withCloseable JsonFunctions.&writeByFieldsMatch + } + + @Test + @DisplayName('writeByFields retrieves ordered matching documents') + void writeByFieldsMatchOrdered() { + new SQLiteDB().withCloseable JsonFunctions.&writeByFieldsMatchOrdered + } + + @Test + @DisplayName('writeByFields retrieves matching documents with a numeric IN clause') + void writeByFieldsMatchNumIn() { + new SQLiteDB().withCloseable JsonFunctions.&writeByFieldsMatchNumIn + } + + @Test + @DisplayName('writeByFields succeeds when no documents match') + void writeByFieldsNoMatch() { + new SQLiteDB().withCloseable JsonFunctions.&writeByFieldsNoMatch + } + + @Test + @DisplayName('writeByFields retrieves matching documents with an IN_ARRAY comparison') + void writeByFieldsMatchInArray() { + new SQLiteDB().withCloseable JsonFunctions.&writeByFieldsMatchInArray + } + + @Test + @DisplayName('writeByFields succeeds when no documents match an IN_ARRAY comparison') + void writeByFieldsNoMatchInArray() { + new SQLiteDB().withCloseable JsonFunctions.&writeByFieldsNoMatchInArray + } + + @Test + @DisplayName('writeByContains fails') + void writeByContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { JsonFunctions.writeByContainsMatch db } + } + } + + @Test + @DisplayName('writeByJsonPath fails') + void writeByJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { JsonFunctions.writeByJsonPathMatch db } + } + } + + @Test + @DisplayName('writeFirstByFields retrieves a matching document') + void writeFirstByFieldsMatchOne() { + new SQLiteDB().withCloseable JsonFunctions.&writeFirstByFieldsMatchOne + } + + @Test + @DisplayName('writeFirstByFields retrieves a matching document among many') + void writeFirstByFieldsMatchMany() { + new SQLiteDB().withCloseable JsonFunctions.&writeFirstByFieldsMatchMany + } + + @Test + @DisplayName('writeFirstByFields retrieves a matching document among many (ordered)') + void writeFirstByFieldsMatchOrdered() { + new SQLiteDB().withCloseable JsonFunctions.&writeFirstByFieldsMatchOrdered + } + + @Test + @DisplayName('writeFirstByFields returns null when no document matches') + void writeFirstByFieldsNoMatch() { + new SQLiteDB().withCloseable JsonFunctions.&writeFirstByFieldsNoMatch + } + + @Test + @DisplayName('writeFirstByContains fails') + void writeFirstByContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { JsonFunctions.writeFirstByContainsMatchOne db } + } + } + + @Test + @DisplayName('writeFirstByJsonPath fails') + void writeFirstByJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { JsonFunctions.writeFirstByJsonPathMatchOne db } + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLitePatchIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLitePatchIT.groovy new file mode 100644 index 0000000..9638cc3 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLitePatchIT.groovy @@ -0,0 +1,54 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName('Groovy | SQLite: Patch') +final class SQLitePatchIT { + + @Test + @DisplayName('byId patches an existing document') + void byIdMatch() { + new SQLiteDB().withCloseable PatchFunctions.&byIdMatch + } + + @Test + @DisplayName('byId succeeds for a non-existent document') + void byIdNoMatch() { + new SQLiteDB().withCloseable PatchFunctions.&byIdNoMatch + } + + @Test + @DisplayName('byFields patches matching document') + void byFieldsMatch() { + new SQLiteDB().withCloseable PatchFunctions.&byFieldsMatch + } + + @Test + @DisplayName('byFields succeeds when no documents match') + void byFieldsNoMatch() { + new SQLiteDB().withCloseable PatchFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byContains fails') + void byContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { PatchFunctions.byContainsMatch db } + } + } + + @Test + @DisplayName('byJsonPath fails') + void byJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { PatchFunctions.byJsonPathMatch db } + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteRemoveFieldsIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteRemoveFieldsIT.groovy new file mode 100644 index 0000000..2f0fa65 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteRemoveFieldsIT.groovy @@ -0,0 +1,66 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName('Groovy | SQLite: RemoveFields') +final class SQLiteRemoveFieldsIT { + + @Test + @DisplayName('byId removes fields from an existing document') + void byIdMatchFields() { + new SQLiteDB().withCloseable RemoveFieldsFunctions.&byIdMatchFields + } + + @Test + @DisplayName('byId succeeds when fields do not exist on an existing document') + void byIdMatchNoFields() { + new SQLiteDB().withCloseable RemoveFieldsFunctions.&byIdMatchNoFields + } + + @Test + @DisplayName('byId succeeds when no document exists') + void byIdNoMatch() { + new SQLiteDB().withCloseable RemoveFieldsFunctions.&byIdNoMatch + } + + @Test + @DisplayName('byFields removes fields from matching documents') + void byFieldsMatchFields() { + new SQLiteDB().withCloseable RemoveFieldsFunctions.&byFieldsMatchFields + } + + @Test + @DisplayName('byFields succeeds when fields do not exist on matching documents') + void byFieldsMatchNoFields() { + new SQLiteDB().withCloseable RemoveFieldsFunctions.&byFieldsMatchNoFields + } + + @Test + @DisplayName('byFields succeeds when no matching documents exist') + void byFieldsNoMatch() { + new SQLiteDB().withCloseable RemoveFieldsFunctions.&byFieldsNoMatch + } + + @Test + @DisplayName('byContains fails') + void byContainsFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { RemoveFieldsFunctions.byContainsMatchFields db } + } + } + + @Test + @DisplayName('byJsonPath fails') + void byJsonPathFails() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { RemoveFieldsFunctions.byJsonPathMatchFields db } + } + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SubDocument.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SubDocument.groovy new file mode 100644 index 0000000..a35722c --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SubDocument.groovy @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +class SubDocument { + String foo + String bar + + SubDocument(String foo = "", String bar = "") { + this.foo = foo + this.bar = bar + } +} diff --git a/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ThrowawayDatabase.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ThrowawayDatabase.groovy new file mode 100644 index 0000000..0aaf20b --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/ThrowawayDatabase.groovy @@ -0,0 +1,24 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.AutoId +import java.sql.Connection + +/** + * Common trait for PostgreSQL and SQLite throwaway databases + */ +trait ThrowawayDatabase implements AutoCloseable { + + /** The database connection for the throwaway database */ + Connection conn + + /** + * Determine if a database object exists + * + * @param name The name of the object whose existence should be checked + * @return True if the object exists, false if not + */ + abstract boolean dbObjectExists(String name) + + /** The name for the throwaway database */ + String dbName = "throwaway_${AutoId.generateRandomString(8)}" +} diff --git a/src/groovy/src/test/java/module-info.java b/src/groovy/src/test/java/module-info.java new file mode 100644 index 0000000..ec115cb --- /dev/null +++ b/src/groovy/src/test/java/module-info.java @@ -0,0 +1,15 @@ +module solutions.bitbadger.documents.groovy.tests { + requires solutions.bitbadger.documents.core; + requires solutions.bitbadger.documents.groovy; + requires com.fasterxml.jackson.databind; + requires java.desktop; + requires java.sql; + requires org.apache.groovy; + requires org.junit.jupiter.api; + + exports solutions.bitbadger.documents.groovy.tests; + exports solutions.bitbadger.documents.groovy.tests.integration; + + opens solutions.bitbadger.documents.groovy.tests; + opens solutions.bitbadger.documents.groovy.tests.integration; +} diff --git a/src/kotlinx/pom.xml b/src/kotlinx/pom.xml new file mode 100644 index 0000000..39164b8 --- /dev/null +++ b/src/kotlinx/pom.xml @@ -0,0 +1,186 @@ + + + 4.0.0 + + solutions.bitbadger + documents + 1.0.0-RC1 + ../../pom.xml + + + solutions.bitbadger.documents + kotlinx + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (KotlinX Serialization Library) + https://relationaldocs.bitbadger.solutions/jvm/ + + + + MIT License + https://www.opensource.org/licenses/mit-license.php + + + + + + Daniel J. Summers + daniel@bitbadger.solutions + Bit Badger Solutions + https://bitbadger.solutions + + + + + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents + + + + solutions.bitbadger.documents + core + ${project.version} + + + org.jetbrains.kotlinx + kotlinx-serialization-json-jvm + ${serialization.version} + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + process-sources + + compile + + + + ${project.basedir}/src/main/java + ${project.basedir}/src/main/kotlin + + + + + test-compile + process-test-sources + + test-compile + + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + + + + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + + + maven-surefire-plugin + ${surefire.version} + + + maven-failsafe-plugin + ${failsafe.version} + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + org.codehaus.mojo + build-helper-maven-plugin + ${buildHelperPlugin.version} + + + generate-sources + + add-source + + + + src/main/kotlin + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${sourcePlugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.jetbrains.dokka + dokka-maven-plugin + 2.0.0 + + true + ${project.basedir}/src/main/module-info.md + + + + package + + javadocJar + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + Deployment-kotlinx-${project.version} + central + + + + + diff --git a/src/kotlinx/src/main/java/module-info.java b/src/kotlinx/src/main/java/module-info.java new file mode 100644 index 0000000..220592d --- /dev/null +++ b/src/kotlinx/src/main/java/module-info.java @@ -0,0 +1,10 @@ +module solutions.bitbadger.documents.kotlinx { + requires solutions.bitbadger.documents.core; + requires kotlin.stdlib; + requires kotlin.reflect; + requires kotlinx.serialization.json; + requires java.sql; + + exports solutions.bitbadger.documents.kotlinx; + exports solutions.bitbadger.documents.kotlinx.extensions; +} diff --git a/src/kotlinx/src/main/kotlin/Count.kt b/src/kotlinx/src/main/kotlin/Count.kt new file mode 100644 index 0000000..0ebcfca --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Count.kt @@ -0,0 +1,120 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.query.CountQuery + +import java.sql.Connection + +/** + * Functions to count documents + */ +object Count { + + /** + * Count all documents in the table + * + * @param tableName The name of the table in which documents should be counted + * @param conn The connection over which documents should be counted + * @return A count of the documents in the table + */ + fun all(tableName: String, conn: Connection) = + conn.customScalar(CountQuery.all(tableName), mapFunc = Results::toCount) + + /** + * Count all documents in the table + * + * @param tableName The name of the table in which documents should be counted + * @return A count of the documents in the table + */ + fun all(tableName: String) = + Configuration.dbConn().use { all(tableName, it) } + + /** + * Count documents using a field comparison + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the deletion should be executed + * @return A count of the matching documents in the table + */ + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ): Long { + val named = Parameters.nameFields(fields) + return conn.customScalar( + CountQuery.byFields(tableName, named, howMatched), + Parameters.addFields(named), + Results::toCount + ) + } + + /** + * Count documents using a field comparison + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return A count of the matching documents in the table + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } + + /** + * Count documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the count should be executed + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = + conn.customScalar( + CountQuery.byContains(tableName), + listOf(Parameters.json(":criteria", criteria)), + Results::toCount + ) + + /** + * Count documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Count documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @param conn The connection on which the count should be executed + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, conn: Connection) = + conn.customScalar( + CountQuery.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path)), + Results::toCount + ) + + /** + * Count documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +} diff --git a/src/kotlinx/src/main/kotlin/Custom.kt b/src/kotlinx/src/main/kotlin/Custom.kt new file mode 100644 index 0000000..3b15c0e --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Custom.kt @@ -0,0 +1,218 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.Configuration +import java.io.PrintWriter +import solutions.bitbadger.documents.java.Custom as CoreCustom +import java.sql.Connection +import java.sql.ResultSet + +/** + * Custom query execution functions + */ +object Custom { + + /** + * Execute a query that returns a list of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + */ + inline fun list( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + mapFunc: (ResultSet) -> TDoc + ) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, mapFunc) } + + /** + * Execute a query that returns a list of results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + */ + inline fun list( + query: String, + parameters: Collection> = listOf(), + mapFunc: (ResultSet) -> TDoc + ) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) } + + /** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + fun jsonArray( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + mapFunc: (ResultSet) -> String + ) = CoreCustom.jsonArray(query, parameters, conn, mapFunc) + + /** + * Execute a query that returns a JSON array of results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + fun jsonArray(query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String) = + CoreCustom.jsonArray(query, parameters, mapFunc) + + /** + * Execute a query, writing its JSON array of results to the given `PrintWriter` + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @throws DocumentException If parameters are invalid + */ + fun writeJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + conn: Connection, + mapFunc: (ResultSet) -> String + ) = CoreCustom.writeJsonArray(query, parameters, writer, conn, mapFunc) + + /** + * Execute a query, writing its JSON array of results to the given `PrintWriter` (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @throws DocumentException If parameters are invalid + */ + fun writeJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + mapFunc: (ResultSet) -> String + ) = CoreCustom.writeJsonArray(query, parameters, writer, mapFunc) + + /** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The document if one matches the query, `null` otherwise + */ + inline fun single( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + mapFunc: (ResultSet) -> TDoc + ) = list("$query LIMIT 1", parameters, conn, mapFunc).singleOrNull() + + /** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The document if one matches the query, `null` otherwise + */ + inline fun single( + query: String, + parameters: Collection> = listOf(), + noinline mapFunc: (ResultSet) -> TDoc + ) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) } + + /** + * Execute a query that returns JSON for one or no documents + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + fun jsonSingle( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + mapFunc: (ResultSet) -> String + ) = CoreCustom.jsonSingle(query, parameters, conn, mapFunc) + + /** + * Execute a query that returns JSON for one or no documents (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + fun jsonSingle(query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String) = + CoreCustom.jsonSingle(query, parameters, mapFunc) + + /** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param conn The connection over which the query should be executed + * @param parameters Parameters to use for the query + */ + fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) = + CoreCustom.nonQuery(query, parameters, conn) + + /** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + */ + fun nonQuery(query: String, parameters: Collection> = listOf()) = + Configuration.dbConn().use { nonQuery(query, parameters, it) } + + /** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + */ + inline fun scalar( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + mapFunc: (ResultSet) -> T + ) = Parameters.apply(conn, query, parameters).use { stmt -> + stmt.executeQuery().use { rs -> + rs.next() + mapFunc(rs) + } + } + + /** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + */ + inline fun scalar( + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> T + ) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) } +} diff --git a/src/kotlinx/src/main/kotlin/Definition.kt b/src/kotlinx/src/main/kotlin/Definition.kt new file mode 100644 index 0000000..508b011 --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Definition.kt @@ -0,0 +1,71 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.java.Definition as JvmDefinition +import java.sql.Connection + +/** + * Functions to define tables and indexes + */ +object Definition { + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + * @param conn The connection on which the query should be executed + */ + fun ensureTable(tableName: String, conn: Connection) = + JvmDefinition.ensureTable(tableName, conn) + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + */ + fun ensureTable(tableName: String) = + JvmDefinition.ensureTable(tableName) + + /** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed< + * @param conn The connection on which the query should be executed + */ + fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection, conn: Connection) = + JvmDefinition.ensureFieldIndex(tableName, indexName, fields, conn) + + /** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed< + */ + fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection) = + JvmDefinition.ensureFieldIndex(tableName, indexName, fields) + + /** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @param conn The connection on which the query should be executed + * @throws DocumentException If called on a SQLite connection + */ + fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) = + JvmDefinition.ensureDocumentIndex(tableName, indexType, conn) + + /** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @throws DocumentException If called on a SQLite connection + */ + fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex) = + JvmDefinition.ensureDocumentIndex(tableName, indexType) +} diff --git a/src/kotlinx/src/main/kotlin/Delete.kt b/src/kotlinx/src/main/kotlin/Delete.kt new file mode 100644 index 0000000..159a1a7 --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Delete.kt @@ -0,0 +1,95 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.java.Delete as JvmDelete +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.query.DeleteQuery +import java.sql.Connection + +/** + * Functions to delete documents + */ +object Delete { + + /** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + * @param conn The connection on which the deletion should be executed + */ + fun byId(tableName: String, docId: TKey, conn: Connection) = + JvmDelete.byId(tableName, docId, conn) + + /** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + */ + fun byId(tableName: String, docId: TKey) = + JvmDelete.byId(tableName, docId) + + /** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the deletion should be executed + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) = + JvmDelete.byFields(tableName, fields, howMatched, conn) + + /** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + JvmDelete.byFields(tableName, fields, howMatched) + + /** + * Delete documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = + conn.customNonQuery(DeleteQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) + + /** + * Delete documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Delete documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, conn: Connection) = + JvmDelete.byJsonPath(tableName, path, conn) + + /** + * Delete documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String) = + JvmDelete.byJsonPath(tableName, path) +} diff --git a/src/kotlinx/src/main/kotlin/Document.kt b/src/kotlinx/src/main/kotlin/Document.kt new file mode 100644 index 0000000..1667a49 --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Document.kt @@ -0,0 +1,114 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.kotlinx.extensions.customNonQuery +import solutions.bitbadger.documents.query.DocumentQuery +import solutions.bitbadger.documents.query.Where +import solutions.bitbadger.documents.query.statementWhere +import java.sql.Connection + +/** + * Functions for manipulating documents + */ +object Document { + + /** + * Insert a new document + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + * @param conn The connection on which the query should be executed + */ + inline fun insert(tableName: String, document: TDoc, conn: Connection) { + val strategy = Configuration.autoIdStrategy + val query = if (strategy == AutoId.DISABLED) { + DocumentQuery.insert(tableName) + } else { + val idField = Configuration.idField + val dialect = Configuration.dialect("Create auto-ID insert query") + val dataParam = if (AutoId.needsAutoId(strategy, document, idField)) { + when (dialect) { + Dialect.POSTGRESQL -> + when (strategy) { + AutoId.NUMBER -> "' || (SELECT coalesce(max(data->>'$idField')::numeric, 0) + 1 " + + "FROM $tableName) || '" + AutoId.UUID -> "\"${AutoId.generateUUID()}\"" + AutoId.RANDOM_STRING -> "\"${AutoId.generateRandomString()}\"" + else -> "\"' || (:data)->>'$idField' || '\"" + }.let { ":data::jsonb || ('{\"$idField\":$it}')::jsonb" } + + Dialect.SQLITE -> + when (strategy) { + AutoId.NUMBER -> "(SELECT coalesce(max(data->>'$idField'), 0) + 1 FROM $tableName)" + AutoId.UUID -> "'${AutoId.generateUUID()}'" + AutoId.RANDOM_STRING -> "'${AutoId.generateRandomString()}'" + else -> "(:data)->>'$idField'" + }.let { "json_set(:data, '$.$idField', $it)" } + } + } else { + ":data" + } + + DocumentQuery.insert(tableName).replace(":data", dataParam) + } + conn.customNonQuery(query, listOf(Parameters.json(":data", document))) + } + + /** + * Insert a new document + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + */ + inline fun insert(tableName: String, document: TDoc) = + Configuration.dbConn().use { insert(tableName, document, it) } + + /** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + * @param conn The connection on which the query should be executed + */ + inline fun save(tableName: String, document: TDoc, conn: Connection) = + conn.customNonQuery(DocumentQuery.save(tableName), listOf(Parameters.json(":data", document))) + + /** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + */ + inline fun save(tableName: String, document: TDoc) = + Configuration.dbConn().use { save(tableName, document, it) } + + /** + * Update (replace) a document by its ID + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + * @param conn The connection on which the query should be executed + */ + inline fun update(tableName: String, docId: TKey, document: TDoc, conn: Connection) = + conn.customNonQuery( + statementWhere(DocumentQuery.update(tableName), Where.byId(":id", docId)), + Parameters.addFields( + listOf(Field.equal(Configuration.idField, docId, ":id")), + mutableListOf(Parameters.json(":data", document)) + ) + ) + + /** + * Update (replace) a document by its ID + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + */ + inline fun update(tableName: String, docId: TKey, document: TDoc) = + Configuration.dbConn().use { update(tableName, docId, document, it) } +} diff --git a/src/kotlinx/src/main/kotlin/DocumentConfig.kt b/src/kotlinx/src/main/kotlin/DocumentConfig.kt new file mode 100644 index 0000000..f1b4de9 --- /dev/null +++ b/src/kotlinx/src/main/kotlin/DocumentConfig.kt @@ -0,0 +1,33 @@ +package solutions.bitbadger.documents.kotlinx + +import kotlinx.serialization.json.Json + +/** + * Configuration for document serialization + */ +object DocumentConfig { + + val options = Json { + coerceInputValues = true + encodeDefaults = true + explicitNulls = false + } + + /** + * Serialize a document to JSON + * + * @param document The document to be serialized + * @return The JSON string with the serialized document + */ + inline fun serialize(document: TDoc) = + options.encodeToString(document) + + /** + * Deserialize a document from JSON + * + * @param json The JSON string with the serialized document + * @return The document created from the given JSON + */ + inline fun deserialize(json: String) = + options.decodeFromString(json) +} diff --git a/src/kotlinx/src/main/kotlin/Exists.kt b/src/kotlinx/src/main/kotlin/Exists.kt new file mode 100644 index 0000000..2bb12dc --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Exists.kt @@ -0,0 +1,107 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.java.Exists as JvmExists +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.query.ExistsQuery +import java.sql.Connection + +/** + * Functions to determine whether documents exist + */ +object Exists { + + /** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @param conn The connection on which the existence check should be executed + * @return True if the document exists, false if not + */ + fun byId(tableName: String, docId: TKey, conn: Connection) = + JvmExists.byId(tableName, docId, conn) + + /** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @return True if the document exists, false if not + */ + fun byId(tableName: String, docId: TKey) = + JvmExists.byId(tableName, docId) + + /** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) = + JvmExists.byFields(tableName, fields, howMatched, conn) + + /** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return True if any matching documents exist, false if not + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + JvmExists.byFields(tableName, fields, howMatched) + + /** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = + conn.customScalar( + ExistsQuery.byContains(tableName), + listOf(Parameters.json(":criteria", criteria)), + Results::toExists + ) + + /** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, conn: Connection) = + JvmExists.byJsonPath(tableName, path, conn) + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String) = + JvmExists.byJsonPath(tableName, path) +} diff --git a/src/kotlinx/src/main/kotlin/Find.kt b/src/kotlinx/src/main/kotlin/Find.kt new file mode 100644 index 0000000..3e90428 --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Find.kt @@ -0,0 +1,417 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.query.orderBy +import java.sql.Connection + +/** + * Functions to find and retrieve documents + */ +object Find { + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents from the given table + */ + inline fun all(tableName: String, orderBy: Collection>? = null, conn: Connection) = + conn.customList(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), mapFunc = Results::fromData) + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents from the given table + */ + inline fun all(tableName: String, orderBy: Collection>? = null) = + Configuration.dbConn().use { all(tableName, orderBy, it) } + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param conn The connection over which documents should be retrieved + * @return A list of documents from the given table + */ + inline fun all(tableName: String, conn: Connection) = + all(tableName, null, conn) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @param conn The connection over which documents should be retrieved + * @return The document if it is found, `null` otherwise + */ + inline fun byId(tableName: String, docId: TKey, conn: Connection) = + conn.customSingle( + FindQuery.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + Results::fromData + ) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return The document if it is found, `null` otherwise + */ + inline fun byId(tableName: String, docId: TKey) = + Configuration.dbConn().use { byId(tableName, docId, it) } + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the field comparison + */ + inline fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ): List { + val named = Parameters.nameFields(fields) + return conn.customList( + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + Parameters.addFields(named), + Results::fromData + ) + } + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the field comparison + */ + inline fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { byFields(tableName, fields, howMatched, orderBy, it) } + + /** + * Retrieve documents using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the field comparison + */ + inline fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ) = + byFields(tableName, fields, howMatched, null, conn) + + /** + * Retrieve documents using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return A list of documents matching the field comparison + */ + inline fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null + ) = + Configuration.dbConn().use { byFields(tableName, fields, howMatched, null, it) } + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = + conn.customList( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + Results::fromData + ) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { byContains(tableName, criteria, orderBy, it) } + + /** + * Retrieve documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains( + tableName: String, + criteria: TContains, + conn: Connection + ) = + byContains(tableName, criteria, null, conn) + + /** + * Retrieve documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byJsonPath( + tableName: String, + path: String, + orderBy: Collection>? = null, + conn: Connection + ) = + conn.customList( + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameter(":path", ParameterType.STRING, path)), + Results::fromData + ) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + Configuration.dbConn().use { byJsonPath(tableName, path, orderBy, it) } + + /** + * Retrieve documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byJsonPath(tableName: String, path: String, conn: Connection) = + byJsonPath(tableName, path, null, conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first document matching the field comparison, or `null` if no matches are found + */ + inline fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ): TDoc? { + val named = Parameters.nameFields(fields) + return conn.customSingle( + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + Parameters.addFields(named), + Results::fromData + ) + } + + /** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the field comparison, or `null` if no matches are found + */ + inline fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { firstByFields(tableName, fields, howMatched, orderBy, it) } + + /** + * Retrieve the first document using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @return The first document matching the field comparison, or `null` if no matches are found + */ + inline fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ) = + firstByFields(tableName, fields, howMatched, null, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first document matching the JSON containment query, or `null` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ + inline fun firstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = + conn.customSingle( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + Results::fromData + ) + + /** + * Retrieve the first document using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return The first document matching the JSON containment query, or `null` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ + inline fun firstByContains( + tableName: String, + criteria: TContains, + conn: Connection + ) = + firstByContains(tableName, criteria, null, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the JSON containment query, or `null` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ + inline fun firstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { firstByContains(tableName, criteria, orderBy, it) } + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first document matching the JSON Path match query, or `null` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ + inline fun firstByJsonPath( + tableName: String, + path: String, + orderBy: Collection>? = null, + conn: Connection + ) = + conn.customSingle( + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameter(":path", ParameterType.STRING, path)), + Results::fromData + ) + + /** + * Retrieve the first document using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return The first document matching the JSON Path match query, or `null` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ + inline fun firstByJsonPath(tableName: String, path: String, conn: Connection) = + firstByJsonPath(tableName, path, null, conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the JSON Path match query, or `null` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ + inline fun firstByJsonPath( + tableName: String, + path: String, + orderBy: Collection>? = null + ) = + Configuration.dbConn().use { firstByJsonPath(tableName, path, orderBy, it) } +} diff --git a/src/kotlinx/src/main/kotlin/Json.kt b/src/kotlinx/src/main/kotlin/Json.kt new file mode 100644 index 0000000..75a6de7 --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Json.kt @@ -0,0 +1,731 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.query.orderBy +import java.io.PrintWriter +import solutions.bitbadger.documents.java.Json as CoreJson +import java.sql.Connection + +/** + * Functions to find and retrieve documents, returning them as JSON strings + */ +object Json { + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents from the given table + * @throws DocumentException If query execution fails + */ + fun all(tableName: String, orderBy: Collection>? = null, conn: Connection) = + CoreJson.all(tableName, orderBy, conn) + + /** + * Retrieve all documents in the given table (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents from the given table + * @throws DocumentException If no connection string has been set, or if query execution fails + */ + fun all(tableName: String, orderBy: Collection>? = null) = + CoreJson.all(tableName, orderBy) + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents from the given table + * @throws DocumentException If query execution fails + */ + fun all(tableName: String, conn: Connection) = + CoreJson.all(tableName, conn) + + /** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If query execution fails + */ + fun writeAll(tableName: String, writer: PrintWriter, orderBy: Collection>? = null, conn: Connection) = + CoreJson.writeAll(tableName, writer, orderBy, conn) + + /** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If query execution fails + */ + fun writeAll(tableName: String, writer: PrintWriter, orderBy: Collection>? = null) = + CoreJson.writeAll(tableName, writer, orderBy) + + /** + * Write all documents in the given table to the given `PrintWriter` + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If query execution fails + */ + fun writeAll(tableName: String, writer: PrintWriter, conn: Connection) = + CoreJson.writeAll(tableName, writer, conn) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @param conn The connection over which documents should be retrieved + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no dialect has been configured + */ + fun byId(tableName: String, docId: TKey, conn: Connection) = + CoreJson.byId(tableName, docId, conn) + + /** + * Retrieve a document by its ID (creates connection) + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no connection string has been set + */ + fun byId(tableName: String, docId: TKey) = + CoreJson.byId(tableName, docId) + + /** + * Write a document to the given `PrintWriter` by its ID + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured + */ + fun writeById(tableName: String, writer: PrintWriter, docId: TKey, conn: Connection) = + CoreJson.writeById(tableName, writer, docId, conn) + + /** + * Write a document to the given `PrintWriter` by its ID (creates connection) + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @throws DocumentException If no dialect has been configured + */ + fun writeById(tableName: String, writer: PrintWriter, docId: TKey) = + CoreJson.writeById(tableName, writer, docId) + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ) = CoreJson.byFields(tableName, fields, howMatched, orderBy, conn) + + /** + * Retrieve documents using a field comparison, ordering results by the given fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = CoreJson.byFields(tableName, fields, howMatched, orderBy) + + /** + * Retrieve documents using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) = + CoreJson.byFields(tableName, fields, howMatched, conn) + + /** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun writeByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ) = CoreJson.writeByFields(tableName, writer, fields, howMatched, orderBy, conn) + + /** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields + * (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun writeByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = CoreJson.writeByFields(tableName, writer, fields, howMatched, orderBy) + + /** + * Write documents to the given `PrintWriter` using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun writeByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ) = CoreJson.writeByFields(tableName, writer, fields, howMatched, conn) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = Custom.jsonArray( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + conn, + Results::jsonFromData + ) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + inline fun byContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { byContains(tableName, criteria, orderBy, it) } + + /** + * Retrieve documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = + byContains(tableName, criteria, null, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + inline fun writeByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = Custom.writeJsonArray( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + writer, + conn, + Results::jsonFromData + ) + + /** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ + inline fun writeByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { writeByContains(tableName, writer, criteria, orderBy, it) } + + /** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + inline fun writeByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + conn: Connection + ) = writeByContains(tableName, writer, criteria, null, conn) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, orderBy: Collection>? = null, conn: Connection) = + CoreJson.byJsonPath(tableName, path, orderBy, conn) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + CoreJson.byJsonPath(tableName, path, orderBy) + + /** + * Retrieve documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, conn: Connection) = + CoreJson.byJsonPath(tableName, path, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + fun writeByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null, + conn: Connection + ) = CoreJson.writeByJsonPath(tableName, writer, path, orderBy, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ + fun writeByJsonPath(tableName: String, writer: PrintWriter, path: String, orderBy: Collection>? = null) = + CoreJson.writeByJsonPath(tableName, writer, path, orderBy) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + fun writeByJsonPath(tableName: String, writer: PrintWriter, path: String, conn: Connection) = + CoreJson.writeByJsonPath(tableName, writer, path, conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ) = CoreJson.firstByFields(tableName, fields, howMatched, orderBy, conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = CoreJson.firstByFields(tableName, fields, howMatched, orderBy) + + /** + * Retrieve the first document using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun firstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ) = CoreJson.firstByFields(tableName, fields, howMatched, conn) + + /** + * Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun writeFirstByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ) = CoreJson.writeFirstByFields(tableName, writer, fields, howMatched, orderBy, conn) + + /** + * Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields + * (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun writeFirstByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null + ) = CoreJson.writeFirstByFields(tableName, writer, fields, howMatched, orderBy) + + /** + * Write the first document to the given `PrintWriter` using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + fun writeFirstByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ) = CoreJson.writeFirstByFields(tableName, writer, fields, howMatched, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + inline fun firstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = Custom.jsonSingle( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + conn, + Results::jsonFromData + ) + + /** + * Retrieve the first document using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + inline fun firstByContains(tableName: String, criteria: TContains, conn: Connection) = + firstByContains(tableName, criteria, null, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + inline fun firstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { firstByContains(tableName, criteria, orderBy, it) } + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + inline fun writeFirstByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null, + conn: Connection + ) = writer.write( + Custom.jsonSingle( + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(Parameters.json(":criteria", criteria)), + conn, + Results::jsonFromData + ) + ) + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ + inline fun writeFirstByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { writeFirstByContains(tableName, writer, criteria, orderBy, it) } + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + inline fun writeFirstByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + conn: Connection + ) = writeFirstByContains(tableName, writer, criteria, null, conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + fun firstByJsonPath(tableName: String, path: String, orderBy: Collection>? = null, conn: Connection) = + CoreJson.firstByJsonPath(tableName, path, orderBy, conn) + + /** + * Retrieve the first document using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + fun firstByJsonPath(tableName: String, path: String, conn: Connection) = + CoreJson.firstByJsonPath(tableName, path, conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + fun firstByJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + CoreJson.firstByJsonPath(tableName, path, orderBy) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + fun writeFirstByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null, + conn: Connection + ) = CoreJson.writeFirstByJsonPath(tableName, writer, path, orderBy, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ + fun writeFirstByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null + ) = CoreJson.writeFirstByJsonPath(tableName, writer, path, orderBy) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + fun writeFirstByJsonPath(tableName: String, writer: PrintWriter, path: String, conn: Connection) = + CoreJson.writeFirstByJsonPath(tableName, writer, path, conn) +} diff --git a/src/kotlinx/src/main/kotlin/Parameters.kt b/src/kotlinx/src/main/kotlin/Parameters.kt new file mode 100644 index 0000000..4e71c9d --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Parameters.kt @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.java.Parameters as JvmParameters +import java.sql.Connection + +/** + * Functions to assist with the creation and implementation of parameters for SQL queries + * + * @author Daniel J. Summers + */ +object Parameters { + + /** + * Assign parameter names to any fields that do not have them assigned + * + * @param fields The collection of fields to be named + * @return The collection of fields with parameter names assigned + */ + fun nameFields(fields: Collection>): Collection> = + JvmParameters.nameFields(fields) + + /** + * Create a parameter by encoding a JSON object + * + * @param name The parameter name + * @param value The object to be encoded as JSON + * @return A parameter with the value encoded + */ + inline fun json(name: String, value: T) = + Parameter(name, ParameterType.JSON, DocumentConfig.serialize(value)) + + /** + * Add field parameters to the given set of parameters + * + * @param fields The fields being compared in the query + * @param existing Any existing parameters for the query (optional, defaults to empty collection) + * @return A collection of parameters for the query + */ + fun addFields(fields: Collection>, existing: MutableCollection> = mutableListOf()) = + JvmParameters.addFields(fields, existing) + + /** + * Replace the parameter names in the query with question marks + * + * @param query The query with named placeholders + * @param parameters The parameters for the query + * @return The query, with name parameters changed to `?`s + */ + fun replaceNamesInQuery(query: String, parameters: Collection>) = + JvmParameters.replaceNamesInQuery(query, parameters) + + /** + * Apply the given parameters to the given query, returning a prepared statement + * + * @param conn The active JDBC connection + * @param query The query + * @param parameters The parameters for the query + * @return A `PreparedStatement` with the parameter names replaced with `?` and parameter values bound + * @throws DocumentException If parameter names are invalid or number value types are invalid + */ + fun apply(conn: Connection, query: String, parameters: Collection>) = + JvmParameters.apply(conn, query, parameters) + + /** + * Create parameters for field names to be removed from a document + * + * @param names The names of the fields to be removed + * @param parameterName The parameter name to use for the query + * @return A list of parameters to use for building the query + * @throws DocumentException If the dialect has not been set + */ + fun fieldNames(names: Collection, parameterName: String = ":name") = + JvmParameters.fieldNames(names, parameterName) +} diff --git a/src/kotlinx/src/main/kotlin/Patch.kt b/src/kotlinx/src/main/kotlin/Patch.kt new file mode 100644 index 0000000..730d069 --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Patch.kt @@ -0,0 +1,137 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.query.PatchQuery +import java.sql.Connection + +/** + * Functions to patch (partially update) documents + */ +object Patch { + + /** + * Patch a document by its ID + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + */ + inline fun byId(tableName: String, docId: TKey, patch: TPatch, conn: Connection) = + conn.customNonQuery( + PatchQuery.byId(tableName, docId), + Parameters.addFields( + listOf(Field.equal(Configuration.idField, docId, ":id")), + mutableListOf(Parameters.json(":data", patch)) + ) + ) + + /** + * Patch a document by its ID + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + */ + inline fun byId(tableName: String, docId: TKey, patch: TPatch) = + Configuration.dbConn().use { byId(tableName, docId, patch, it) } + + /** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + * @param conn The connection on which the update should be executed + */ + inline fun byFields( + tableName: String, + fields: Collection>, + patch: TPatch, + howMatched: FieldMatch? = null, + conn: Connection + ) { + val named = Parameters.nameFields(fields) + conn.customNonQuery( + PatchQuery.byFields(tableName, named, howMatched), Parameters.addFields( + named, + mutableListOf(Parameters.json(":data", patch)) + ) + ) + } + + /** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + */ + inline fun byFields( + tableName: String, + fields: Collection>, + patch: TPatch, + howMatched: FieldMatch? = null + ) = + Configuration.dbConn().use { byFields(tableName, fields, patch, howMatched, it) } + + /** + * Patch documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains( + tableName: String, + criteria: TContains, + patch: TPatch, + conn: Connection + ) = + conn.customNonQuery( + PatchQuery.byContains(tableName), + listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch)) + ) + + /** + * Patch documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains, patch: TPatch) = + Configuration.dbConn().use { byContains(tableName, criteria, patch, it) } + + /** + * Patch documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + inline fun byJsonPath(tableName: String, path: String, patch: TPatch, conn: Connection) = + conn.customNonQuery( + PatchQuery.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch)) + ) + + /** + * Patch documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If called on a SQLite connection + */ + inline fun byJsonPath(tableName: String, path: String, patch: TPatch) = + Configuration.dbConn().use { byJsonPath(tableName, path, patch, it) } +} diff --git a/src/kotlinx/src/main/kotlin/RemoveFields.kt b/src/kotlinx/src/main/kotlin/RemoveFields.kt new file mode 100644 index 0000000..b5f8b0f --- /dev/null +++ b/src/kotlinx/src/main/kotlin/RemoveFields.kt @@ -0,0 +1,124 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.java.RemoveFields as JvmRemoveFields +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.query.RemoveFieldsQuery +import java.sql.Connection + +/** + * Functions to remove fields from documents + */ +object RemoveFields { + + /** + * Remove fields from a document by its ID + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + */ + fun byId(tableName: String, docId: TKey, toRemove: Collection, conn: Connection) = + JvmRemoveFields.byId(tableName, docId, toRemove, conn) + + /** + * Remove fields from a document by its ID + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + */ + fun byId(tableName: String, docId: TKey, toRemove: Collection) = + JvmRemoveFields.byId(tableName, docId, toRemove) + + /** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + * @param conn The connection on which the update should be executed + */ + fun byFields( + tableName: String, + fields: Collection>, + toRemove: Collection, + howMatched: FieldMatch? = null, + conn: Connection + ) = + JvmRemoveFields.byFields(tableName, fields, toRemove, howMatched, conn) + + /** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + */ + fun byFields( + tableName: String, + fields: Collection>, + toRemove: Collection, + howMatched: FieldMatch? = null + ) = + JvmRemoveFields.byFields(tableName, fields, toRemove, howMatched) + + /** + * Remove fields from documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains( + tableName: String, + criteria: TContains, + toRemove: Collection, + conn: Connection + ) { + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + RemoveFieldsQuery.byContains(tableName, nameParams), + listOf(Parameters.json(":criteria", criteria), *nameParams.toTypedArray()) + ) + } + + /** + * Remove fields from documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @throws DocumentException If called on a SQLite connection + */ + inline fun byContains(tableName: String, criteria: TContains, toRemove: Collection) = + Configuration.dbConn().use { byContains(tableName, criteria, toRemove, it) } + + /** + * Remove fields from documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, toRemove: Collection, conn: Connection) = + JvmRemoveFields.byJsonPath(tableName, path, toRemove, conn) + + /** + * Remove fields from documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @throws DocumentException If called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String, toRemove: Collection) = + JvmRemoveFields.byJsonPath(tableName, path, toRemove) +} diff --git a/src/kotlinx/src/main/kotlin/Results.kt b/src/kotlinx/src/main/kotlin/Results.kt new file mode 100644 index 0000000..e4b04ae --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Results.kt @@ -0,0 +1,107 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.java.Results as CoreResults +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.SQLException + +/** + * Helper functions for handling results + */ +object Results { + + /** + * Create a domain item from a document, specifying the field in which the document is found + * + * @param field The field name containing the JSON document + * @return A function to create the constructed domain item + */ + inline fun fromDocument(field: String): (ResultSet) -> TDoc = + { rs -> DocumentConfig.deserialize(rs.getString(field)) } + + /** + * Create a domain item from a document + * + * @return The constructed domain item + */ + inline fun fromData(rs: ResultSet) = + fromDocument("data")(rs) + + /** + * Create a list of items for the results of the given command, using the specified mapping function + * + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to domain class instance + * @return A list of items from the query's result + * @throws DocumentException If there is a problem executing the query + */ + inline fun toCustomList(stmt: PreparedStatement, mapFunc: (ResultSet) -> TDoc) = + try { + stmt.executeQuery().use { + val results = mutableListOf() + while (it.next()) { + results.add(mapFunc(it)) + } + results.toList() + } + } catch (ex: SQLException) { + throw DocumentException("Error retrieving documents from query: ${ex.message}", ex) + } + + /** + * Extract a count from the first column + * + * @param rs A `ResultSet` set to the row with the count to retrieve + * @return The count from the row + */ + fun toCount(rs: ResultSet) = + when (Configuration.dialect()) { + Dialect.POSTGRESQL -> rs.getInt("it").toLong() + Dialect.SQLITE -> rs.getLong("it") + } + + /** + * Extract a true/false value from the first column + * + * @param rs A `ResultSet` set to the row with the true/false value to retrieve + * @return The true/false value from the row + */ + fun toExists(rs: ResultSet) = + when (Configuration.dialect()) { + Dialect.POSTGRESQL -> rs.getBoolean("it") + Dialect.SQLITE -> toCount(rs) > 0L + } + + /** + * Retrieve the JSON text of a document, specifying the field in which the document is found + * + * @param field The field name containing the JSON document + * @param rs A `ResultSet` set to the row with the document to be constructed + * @return The JSON text of the document + */ + fun jsonFromDocument(field: String, rs: ResultSet) = + CoreResults.jsonFromDocument(field, rs) + + /** + * Retrieve the JSON text of a document, specifying the field in which the document is found + * + * @param rs A `ResultSet` set to the row with the document to be constructed + * @return The JSON text of the document + */ + fun jsonFromData(rs: ResultSet) = + CoreResults.jsonFromData(rs) + + /** + * Create a JSON array of items for the results of the given command, using the specified mapping function + * + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to JSON text + * @return A string with a JSON array of documents from the query's result + * @throws DocumentException If there is a problem executing the query (unchecked) + */ + fun toJsonArray(stmt: PreparedStatement, mapFunc: (ResultSet) -> String) = + CoreResults.toJsonArray(stmt, mapFunc) +} diff --git a/src/kotlinx/src/main/kotlin/extensions/Connection.kt b/src/kotlinx/src/main/kotlin/extensions/Connection.kt new file mode 100644 index 0000000..e437e21 --- /dev/null +++ b/src/kotlinx/src/main/kotlin/extensions/Connection.kt @@ -0,0 +1,750 @@ +package solutions.bitbadger.documents.kotlinx.extensions + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.* +import java.io.PrintWriter +import java.sql.Connection +import java.sql.ResultSet + +// ~~~ CUSTOM QUERIES ~~~ + +/** + * Execute a query that returns a list of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + */ +inline fun Connection.customList( + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc +) = Custom.list(query, parameters, this, mapFunc) + +/** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ +fun Connection.customJsonArray( + query: String, + parameters: Collection> = listOf(), + mapFunc: (ResultSet) -> String +) = Custom.jsonArray(query, parameters, this, mapFunc) + +/** + * Execute a query, writing its JSON array of results to the given `PrintWriter` (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @throws DocumentException If parameters are invalid + */ +fun Connection.writeCustomJsonArray( + query: String, + parameters: Collection> = listOf(), + writer: PrintWriter, + mapFunc: (ResultSet) -> String +) = Custom.writeJsonArray(query, parameters, writer, this, mapFunc) + +/** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The document if one matches the query, `null` otherwise + */ +inline fun Connection.customSingle( + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc +) = Custom.single(query, parameters, this, mapFunc) + +/** + * Execute a query that returns JSON for one or no documents (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ +fun Connection.customJsonSingle( + query: String, + parameters: Collection> = listOf(), + mapFunc: (ResultSet) -> String +) = Custom.jsonSingle(query, parameters, this, mapFunc) + +/** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + */ +fun Connection.customNonQuery(query: String, parameters: Collection> = listOf()) = + Custom.nonQuery(query, parameters, this) + +/** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + */ +inline fun Connection.customScalar( + query: String, + parameters: Collection> = listOf(), + mapFunc: (ResultSet) -> T +) = Custom.scalar(query, parameters, this, mapFunc) + +// ~~~ DEFINITION QUERIES ~~~ + +/** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + */ +fun Connection.ensureTable(tableName: String) = + Definition.ensureTable(tableName, this) + +/** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed< + */ +fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection) = + Definition.ensureFieldIndex(tableName, indexName, fields, this) + +/** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @throws DocumentException If called on a SQLite connection + */ +fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) = + Definition.ensureDocumentIndex(tableName, indexType, this) + +// ~~~ DOCUMENT MANIPULATION QUERIES ~~~ + +/** + * Insert a new document + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + */ +inline fun Connection.insert(tableName: String, document: TDoc) = + Document.insert(tableName, document, this) + +/** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + */ +inline fun Connection.save(tableName: String, document: TDoc) = + Document.save(tableName, document, this) + +/** + * Update (replace) a document by its ID + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + */ +inline fun Connection.update(tableName: String, docId: TKey, document: TDoc) = + Document.update(tableName, docId, document, this) + +// ~~~ DOCUMENT COUNT QUERIES ~~~ + +/** + * Count all documents in the table + * + * @param tableName The name of the table in which documents should be counted + * @return A count of the documents in the table + */ +fun Connection.countAll(tableName: String) = + Count.all(tableName, this) + +/** + * Count documents using a field comparison + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return A count of the matching documents in the table + */ +fun Connection.countByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Count.byFields(tableName, fields, howMatched, this) + +/** + * Count documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.countByContains(tableName: String, criteria: TContains) = + Count.byContains(tableName, criteria, this) + +/** + * Count documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ +fun Connection.countByJsonPath(tableName: String, path: String) = + Count.byJsonPath(tableName, path, this) + +// ~~~ DOCUMENT EXISTENCE QUERIES ~~~ + +/** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @return True if the document exists, false if not + */ +fun Connection.existsById(tableName: String, docId: TKey) = + Exists.byId(tableName, docId, this) + +/** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return True if any matching documents exist, false if not + */ +fun Connection.existsByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Exists.byFields(tableName, fields, howMatched, this) + +/** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.existsByContains(tableName: String, criteria: TContains) = + Exists.byContains(tableName, criteria, this) + +/** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ +fun Connection.existsByJsonPath(tableName: String, path: String) = + Exists.byJsonPath(tableName, path, this) + +// ~~~ DOCUMENT RETRIEVAL QUERIES (Domain Objects) ~~~ + +/** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents from the given table + */ +inline fun Connection.findAll(tableName: String, orderBy: Collection>? = null) = + Find.all(tableName, orderBy, this) + +/** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return The document if it is found, `null` otherwise + */ +inline fun Connection.findById(tableName: String, docId: TKey) = + Find.byId(tableName, docId, this) + +/** + * Retrieve documents using a field comparison, ordering results by the optional given fields + * + * @param tableName The table from which the document should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the field comparison + */ +inline fun Connection.findByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Find.byFields(tableName, fields, howMatched, orderBy, this) + +/** + * Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.findByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null +) = Find.byContains(tableName, criteria, orderBy, this) + +/** + * Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.findByJsonPath( + tableName: String, + path: String, + orderBy: Collection>? = null +) = Find.byJsonPath(tableName, path, orderBy, this) + +/** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the field comparison, or `null` if no matches are found + */ +inline fun Connection.findFirstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Find.firstByFields(tableName, fields, howMatched, orderBy, this) + +/** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the JSON containment query, or `null` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.findFirstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null +) = Find.firstByContains(tableName, criteria, orderBy, this) + +/** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the JSON Path match query, or `null` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.findFirstByJsonPath( + tableName: String, + path: String, + orderBy: Collection>? = null +) = Find.firstByJsonPath(tableName, path, orderBy, this) + +// ~~~ DOCUMENT RETRIEVAL QUERIES (Raw JSON) ~~~ + +/** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents from the given table + * @throws DocumentException If no connection string has been set, or if query execution fails + */ +fun Connection.jsonAll(tableName: String, orderBy: Collection>? = null) = + Json.all(tableName, orderBy, this) + +/** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no connection string has been set + */ +fun Connection.jsonById(tableName: String, docId: TKey) = + Json.byId(tableName, docId, this) + +/** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ +fun Connection.jsonByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Json.byFields(tableName, fields, howMatched, orderBy, this) + +/** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ +inline fun Connection.jsonByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null +) = Json.byContains(tableName, criteria, orderBy, this) + +/** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ +fun Connection.jsonByJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + Json.byJsonPath(tableName, path, orderBy, this) + +/** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ +fun Connection.jsonFirstByFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Json.firstByFields(tableName, fields, howMatched, orderBy, this) + +/** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ +inline fun Connection.jsonFirstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null +) = Json.firstByContains(tableName, criteria, orderBy, this) + +/** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ +fun Connection.jsonFirstByJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + Json.firstByJsonPath(tableName, path, orderBy, this) + +// ~~~ DOCUMENT RETRIEVAL QUERIES (Write raw JSON to PrintWriter) ~~~ + +/** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if query execution fails + */ +fun Connection.writeJsonAll(tableName: String, writer: PrintWriter, orderBy: Collection>? = null) = + Json.writeAll(tableName, writer, orderBy, this) + +/** + * Write a document to the given `PrintWriter` by its ID + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @throws DocumentException If no connection string has been set + */ +fun Connection.writeJsonById(tableName: String, writer: PrintWriter, docId: TKey) = + Json.writeById(tableName, writer, docId, this) + +/** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ +fun Connection.writeJsonByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Json.writeByFields(tableName, writer, fields, howMatched, orderBy, this) + +/** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ +inline fun Connection.writeJsonByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null +) = Json.writeByContains(tableName, writer, criteria, orderBy, this) + +/** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ +fun Connection.writeJsonByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null +) = Json.writeByJsonPath(tableName, writer, path, orderBy, this) + +/** + * Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ +fun Connection.writeJsonFirstByFields( + tableName: String, + writer: PrintWriter, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null +) = Json.writeFirstByFields(tableName, writer, fields, howMatched, orderBy, this) + +/** + * Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ +inline fun Connection.writeJsonFirstByContains( + tableName: String, + writer: PrintWriter, + criteria: TContains, + orderBy: Collection>? = null +) = Json.writeFirstByContains(tableName, writer, criteria, orderBy, this) + +/** + * Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ +fun Connection.writeJsonFirstByJsonPath( + tableName: String, + writer: PrintWriter, + path: String, + orderBy: Collection>? = null +) = Json.writeFirstByJsonPath(tableName, writer, path, orderBy, this) + +// ~~~ DOCUMENT PATCH (PARTIAL UPDATE) QUERIES ~~~ + +/** + * Patch a document by its ID + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + */ +inline fun Connection.patchById(tableName: String, docId: TKey, patch: TPatch) = + Patch.byId(tableName, docId, patch, this) + +/** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + */ +inline fun Connection.patchByFields( + tableName: String, + fields: Collection>, + patch: TPatch, + howMatched: FieldMatch? = null +) = Patch.byFields(tableName, fields, patch, howMatched, this) + +/** + * Patch documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.patchByContains( + tableName: String, + criteria: TContains, + patch: TPatch +) = Patch.byContains(tableName, criteria, patch, this) + +/** + * Patch documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.patchByJsonPath(tableName: String, path: String, patch: TPatch) = + Patch.byJsonPath(tableName, path, patch, this) + +// ~~~ DOCUMENT FIELD REMOVAL QUERIES ~~~ + +/** + * Remove fields from a document by its ID + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + */ +fun Connection.removeFieldsById(tableName: String, docId: TKey, toRemove: Collection) = + RemoveFields.byId(tableName, docId, toRemove, this) + +/** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + */ +fun Connection.removeFieldsByFields( + tableName: String, + fields: Collection>, + toRemove: Collection, + howMatched: FieldMatch? = null +) = RemoveFields.byFields(tableName, fields, toRemove, howMatched, this) + +/** + * Remove fields from documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.removeFieldsByContains( + tableName: String, + criteria: TContains, + toRemove: Collection +) = RemoveFields.byContains(tableName, criteria, toRemove, this) + +/** + * Remove fields from documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @throws DocumentException If called on a SQLite connection + */ +fun Connection.removeFieldsByJsonPath(tableName: String, path: String, toRemove: Collection) = + RemoveFields.byJsonPath(tableName, path, toRemove, this) + +// ~~~ DOCUMENT DELETION QUERIES ~~~ + +/** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + */ +fun Connection.deleteById(tableName: String, docId: TKey) = + Delete.byId(tableName, docId, this) + +/** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + */ +fun Connection.deleteByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Delete.byFields(tableName, fields, howMatched, this) + +/** + * Delete documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @throws DocumentException If called on a SQLite connection + */ +inline fun Connection.deleteByContains(tableName: String, criteria: TContains) = + Delete.byContains(tableName, criteria, this) + +/** + * Delete documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @throws DocumentException If called on a SQLite connection + */ +fun Connection.deleteByJsonPath(tableName: String, path: String) = + Delete.byJsonPath(tableName, path, this) diff --git a/src/kotlinx/src/main/module-info.md b/src/kotlinx/src/main/module-info.md new file mode 100644 index 0000000..92e1012 --- /dev/null +++ b/src/kotlinx/src/main/module-info.md @@ -0,0 +1,11 @@ +# Module kotlinx + +This module contains an implementation of the document store API which uses a `kotlinx.serialization`-based serializer (relies on reified generics) + +# Package solutions.bitbadger.documents.kotlinx + +The document store API based on `kotlinx.serialization` + +# Package solutions.bitbadger.documents.kotlinx.extensions + +Extensions on the Java `Connection` object for document manipulation diff --git a/src/kotlinx/src/test/java/module-info.java b/src/kotlinx/src/test/java/module-info.java new file mode 100644 index 0000000..959ffd5 --- /dev/null +++ b/src/kotlinx/src/test/java/module-info.java @@ -0,0 +1,14 @@ +module solutions.bitbadger.documents.kotlinx.tests { + requires solutions.bitbadger.documents.core; + requires solutions.bitbadger.documents.kotlinx; + requires java.sql; + requires kotlin.stdlib; + requires kotlinx.serialization.json; + requires kotlin.test.junit5; + + exports solutions.bitbadger.documents.kotlinx.tests; + exports solutions.bitbadger.documents.kotlinx.tests.integration; + + opens solutions.bitbadger.documents.kotlinx.tests; + opens solutions.bitbadger.documents.kotlinx.tests.integration; +} diff --git a/src/kotlinx/src/test/kotlin/DocumentConfigTest.kt b/src/kotlinx/src/test/kotlin/DocumentConfigTest.kt new file mode 100644 index 0000000..882ed3b --- /dev/null +++ b/src/kotlinx/src/test/kotlin/DocumentConfigTest.kt @@ -0,0 +1,22 @@ +package solutions.bitbadger.documents.kotlinx.tests + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.kotlinx.DocumentConfig +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Unit tests for the `Configuration` object + */ +@DisplayName("KotlinX | DocumentConfig") +class DocumentConfigTest { + + @Test + @DisplayName("Default JSON options are as expected") + fun defaultJsonOptions() { + assertTrue(DocumentConfig.options.configuration.encodeDefaults, "Encode Defaults should have been set") + assertFalse(DocumentConfig.options.configuration.explicitNulls, "Explicit Nulls should not have been set") + assertTrue(DocumentConfig.options.configuration.coerceInputValues, "Coerce Input Values should have been set") + } +} diff --git a/src/kotlinx/src/test/kotlin/Types.kt b/src/kotlinx/src/test/kotlin/Types.kt new file mode 100644 index 0000000..426de88 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/Types.kt @@ -0,0 +1,68 @@ +package solutions.bitbadger.documents.kotlinx.tests + +import kotlinx.serialization.Serializable +import solutions.bitbadger.documents.kotlinx.extensions.insert +import solutions.bitbadger.documents.kotlinx.tests.integration.ThrowawayDatabase + +/** The test table name to use for integration tests */ +const val TEST_TABLE = "test_table" + +@Serializable +data class NumIdDocument(val key: Int, val text: String) { + constructor() : this(0, "") +} + +@Serializable +data class SubDocument(val foo: String, val bar: String) { + constructor() : this("", "") +} + +@Serializable +data class ArrayDocument(val id: String, val values: List) { + + constructor() : this("", listOf()) + + companion object { + /** A set of documents used for integration tests */ + val testDocuments = listOf( + ArrayDocument("first", listOf("a", "b", "c")), + ArrayDocument("second", listOf("c", "d", "e")), + ArrayDocument("third", listOf("x", "y", "z")) + ) + } +} + +@Serializable +data class JsonDocument(val id: String, val value: String = "", val numValue: Int = 0, val sub: SubDocument? = null) { + + constructor() : this("") + + companion object { + /** Documents to use for testing */ + private val testDocuments = listOf( + JsonDocument("one", "FIRST!", 0, null), + JsonDocument("two", "another", 10, SubDocument("green", "blue")), + JsonDocument("three", "", 4, null), + JsonDocument("four", "purple", 17, SubDocument("green", "red")), + JsonDocument("five", "purple", 18, null) + ) + + fun load(db: ThrowawayDatabase, tableName: String = TEST_TABLE) = + testDocuments.forEach { db.conn.insert(tableName, it) } + + /** Document ID `one` as a JSON string */ + val one = """{"id":"one","value":"FIRST!","numValue":0}""" + + /** Document ID `two` as a JSON string */ + val two = """{"id":"two","value":"another","numValue":10,"sub":{"foo":"green","bar":"blue"}}""" + + /** Document ID `three` as a JSON string */ + val three = """{"id":"three","value":"","numValue":4}""" + + /** Document ID `four` as a JSON string */ + val four = """{"id":"four","value":"purple","numValue":17,"sub":{"foo":"green","bar":"red"}}""" + + /** Document ID `five` as a JSON string */ + val five = """{"id":"five","value":"purple","numValue":18}""" + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/CountFunctions.kt b/src/kotlinx/src/test/kotlin/integration/CountFunctions.kt new file mode 100644 index 0000000..2f31b3d --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/CountFunctions.kt @@ -0,0 +1,72 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import kotlin.test.assertEquals + +/** + * Integration tests for the `Count` object + */ +object CountFunctions { + + fun all(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should have been 5 documents in the table") + } + + fun byFieldsNumeric(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 3L, + db.conn.countByFields(TEST_TABLE, listOf(Field.between("numValue", 10, 20))), + "There should have been 3 matching documents" + ) + } + + fun byFieldsAlpha(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 1L, + db.conn.countByFields(TEST_TABLE, listOf(Field.between("value", "aardvark", "apple"))), + "There should have been 1 matching document" + ) + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 2L, + db.conn.countByContains(TEST_TABLE, mapOf("value" to "purple")), + "There should have been 2 matching documents" + ) + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0L, + db.conn.countByContains(TEST_TABLE, mapOf("value" to "magenta")), + "There should have been no matching documents" + ) + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 2L, + db.conn.countByJsonPath(TEST_TABLE, "$.numValue ? (@ < 5)"), + "There should have been 2 matching documents" + ) + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0L, + db.conn.countByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no matching documents" + ) + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt new file mode 100644 index 0000000..a98e90e --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt @@ -0,0 +1,162 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.Results +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.ArrayDocument +import solutions.bitbadger.documents.kotlinx.tests.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import solutions.bitbadger.documents.query.* +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +/** + * Integration tests for the `Custom` object + */ +object CustomFunctions { + + fun listEmpty(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.deleteByFields(TEST_TABLE, listOf(Field.exists(Configuration.idField))) + val result = db.conn.customList(FindQuery.all(TEST_TABLE), mapFunc = Results::fromData) + assertEquals(0, result.size, "There should have been no results") + } + + fun listAll(db: ThrowawayDatabase) { + JsonDocument.load(db) + val result = db.conn.customList(FindQuery.all(TEST_TABLE), mapFunc = Results::fromData) + assertEquals(5, result.size, "There should have been 5 results") + } + + fun jsonArrayEmpty(db: ThrowawayDatabase) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), "The test table should be empty") + assertEquals( + "[]", + db.conn.customJsonArray(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), + "An empty list was not represented correctly" + ) + } + + fun jsonArraySingle(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3"))) + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), + "A single document list was not represented correctly" + ) + } + + fun jsonArrayMany(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"first","values":["a","b","c"]},""" + + """{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]"""), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE) + orderBy(listOf(Field.named("id"))), listOf(), + Results::jsonFromData), + "A multiple document list was not represented correctly" + ) + } + + fun writeJsonArrayEmpty(db: ThrowawayDatabase) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), "The test table should be empty") + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), listOf(), writer, Results::jsonFromData) + assertEquals("[]", output.toString(), "An empty list was not represented correctly") + } + + fun writeJsonArraySingle(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3"))) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), listOf(), writer, Results::jsonFromData) + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), + output.toString(), + "A single document list was not represented correctly" + ) + } + + fun writeJsonArrayMany(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE) + orderBy(listOf(Field.named("id"))), listOf(), writer, + Results::jsonFromData) + assertEquals( + JsonFunctions.maybeJsonB("""[{"id":"first","values":["a","b","c"]},""" + + """{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]"""), + output.toString(), + "A multiple document list was not represented correctly" + ) + } + + fun singleNone(db: ThrowawayDatabase) = + assertNull( + db.conn.customSingle(FindQuery.all(TEST_TABLE), mapFunc = Results::fromData), + "There should not have been a document returned" + ) + + fun singleOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertNotNull( + db.conn.customSingle(FindQuery.all(TEST_TABLE), mapFunc = Results::fromData), + "There should not have been a document returned" + ) + } + + fun jsonSingleNone(db: ThrowawayDatabase) = + assertEquals("{}", db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), + "An empty document was not represented correctly") + + fun jsonSingleOne(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, ArrayDocument("me", listOf("myself", "i"))) + assertEquals( + JsonFunctions.maybeJsonB("{\"id\":\"me\",\"values\":[\"myself\",\"i\"]}"), + db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), listOf(), Results::jsonFromData), + "A single document was not represented correctly" + ) + } + + fun nonQueryChanges(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), mapFunc = Results::toCount), + "There should have been 5 documents in the table" + ) + db.conn.customNonQuery("DELETE FROM $TEST_TABLE") + assertEquals( + 0L, db.conn.customScalar(CountQuery.all(TEST_TABLE), mapFunc = Results::toCount), + "There should have been no documents in the table" + ) + } + + fun nonQueryNoChanges(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), mapFunc = Results::toCount), + "There should have been 5 documents in the table" + ) + db.conn.customNonQuery( + DeleteQuery.byId(TEST_TABLE, "eighty-two"), + listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) + ) + assertEquals( + 5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), mapFunc = Results::toCount), + "There should still have been 5 documents in the table" + ) + } + + fun scalar(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 3L, + db.conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount), + "The number 3 should have been returned" + ) + } + +} \ No newline at end of file diff --git a/src/kotlinx/src/test/kotlin/integration/DefinitionFunctions.kt b/src/kotlinx/src/test/kotlin/integration/DefinitionFunctions.kt new file mode 100644 index 0000000..3e05234 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/DefinitionFunctions.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Definition` object / `ensure*` connection extension functions + */ +object DefinitionFunctions { + + fun ensureTable(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("ensured"), "The 'ensured' table should not exist") + assertFalse(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should not exist") + db.conn.ensureTable("ensured") + assertTrue(db.dbObjectExists("ensured"), "The 'ensured' table should exist") + assertTrue(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should now exist") + } + + fun ensureFieldIndex(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should not exist") + db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category")) + assertTrue(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should now exist") + } + + fun ensureDocumentIndexFull(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist") + db.conn.ensureTable("doc_table") + assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist") + assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist") + db.conn.ensureDocumentIndex("doc_table", DocumentIndex.FULL) + assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist") + } + + fun ensureDocumentIndexOptimized(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist") + db.conn.ensureTable("doc_table") + assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist") + assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist") + db.conn.ensureDocumentIndex("doc_table", DocumentIndex.OPTIMIZED) + assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist") + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/DeleteFunctions.kt b/src/kotlinx/src/test/kotlin/integration/DeleteFunctions.kt new file mode 100644 index 0000000..13eaf04 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/DeleteFunctions.kt @@ -0,0 +1,69 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import kotlin.test.assertEquals + +/** + * Integration tests for the `Delete` object + */ +object DeleteFunctions { + + fun byIdMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteById(TEST_TABLE, "four") + assertEquals(4, db.conn.countAll(TEST_TABLE), "There should now be 4 documents in the table") + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteById(TEST_TABLE, "negative four") + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByFields(TEST_TABLE, listOf(Field.notEqual("value", "purple"))) + assertEquals(2, db.conn.countAll(TEST_TABLE), "There should now be 2 documents in the table") + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByFields(TEST_TABLE, listOf(Field.equal("value", "crimson"))) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByContains(TEST_TABLE, mapOf("value" to "purple")) + assertEquals(3, db.conn.countAll(TEST_TABLE), "There should now be 3 documents in the table") + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByContains(TEST_TABLE, mapOf("target" to "acquired")) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByJsonPath(TEST_TABLE, "$.value ? (@ == \"purple\")") + assertEquals(3, db.conn.countAll(TEST_TABLE), "There should now be 3 documents in the table") + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)") + assertEquals(5, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/DocumentFunctions.kt b/src/kotlinx/src/test/kotlin/integration/DocumentFunctions.kt new file mode 100644 index 0000000..4715998 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/DocumentFunctions.kt @@ -0,0 +1,130 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.NumIdDocument +import solutions.bitbadger.documents.kotlinx.tests.SubDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import kotlin.test.* + +/** + * Integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +object DocumentFunctions { + + fun insertDefault(db: ThrowawayDatabase) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) + db.conn.insert(TEST_TABLE, doc) + val after = db.conn.findAll(TEST_TABLE) + assertEquals(1, after.size, "There should be one document in the table") + assertEquals(doc, after[0], "The document should be what was inserted") + } + + fun insertDupe(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) + assertThrows("Inserting a document with a duplicate key should have thrown an exception") { + db.conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) + } + } + + fun insertNumAutoId(db: ThrowawayDatabase) { + try { + Configuration.autoIdStrategy = AutoId.NUMBER + Configuration.idField = "key" + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, NumIdDocument(0, "one")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "two")) + db.conn.insert(TEST_TABLE, NumIdDocument(77, "three")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "four")) + + val after = db.conn.findAll(TEST_TABLE, listOf(Field.named("key"))) + assertEquals(4, after.size, "There should have been 4 documents returned") + assertEquals( + "1|2|77|78", after.joinToString("|") { it.key.toString() }, + "The IDs were not generated correctly" + ) + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idField = "id" + } + } + + fun insertUUIDAutoId(db: ThrowawayDatabase) { + try { + Configuration.autoIdStrategy = AutoId.UUID + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, JsonDocument("")) + + val after = db.conn.findAll(TEST_TABLE) + assertEquals(1, after.size, "There should have been 1 document returned") + assertEquals(32, after[0].id.length, "The ID was not generated correctly") + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + } + } + + fun insertStringAutoId(db: ThrowawayDatabase) { + try { + Configuration.autoIdStrategy = AutoId.RANDOM_STRING + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, JsonDocument("")) + + Configuration.idStringLength = 21 + db.conn.insert(TEST_TABLE, JsonDocument("")) + + val after = db.conn.findAll(TEST_TABLE) + assertEquals(2, after.size, "There should have been 2 documents returned") + assertEquals(16, after[0].id.length, "The first document's ID was not generated correctly") + assertEquals(21, after[1].id.length, "The second document's ID was not generated correctly") + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idStringLength = 16 + } + } + + fun saveMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.save(TEST_TABLE, JsonDocument("two", numValue = 44)) + val doc = db.conn.findById(TEST_TABLE, "two") + assertNotNull(doc, "There should have been a document returned") + assertEquals("two", doc.id, "An incorrect document was returned") + assertEquals("", doc.value, "The \"value\" field was not updated") + assertEquals(44, doc.numValue, "The \"numValue\" field was not updated") + assertNull(doc.sub, "The \"sub\" field was not updated") + } + + fun saveNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.save(TEST_TABLE, JsonDocument("test", sub = SubDocument("a", "b"))) + assertNotNull( + db.conn.findById(TEST_TABLE, "test"), + "The test document should have been saved" + ) + } + + fun updateMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.update(TEST_TABLE, "one", JsonDocument("one", "howdy", 8, SubDocument("y", "z"))) + val doc = db.conn.findById(TEST_TABLE, "one") + assertNotNull(doc, "There should have been a document returned") + assertEquals("one", doc.id, "An incorrect document was returned") + assertEquals("howdy", doc.value, "The \"value\" field was not updated") + assertEquals(8, doc.numValue, "The \"numValue\" field was not updated") + assertNotNull(doc.sub, "The sub-document should not be null") + assertEquals("y", doc.sub.foo, "The sub-document \"foo\" field was not updated") + assertEquals("z", doc.sub.bar, "The sub-document \"bar\" field was not updated") + } + + fun updateNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse { db.conn.existsById(TEST_TABLE, "two-hundred") } + db.conn.update(TEST_TABLE, "two-hundred", JsonDocument("two-hundred", numValue = 200)) + assertFalse { db.conn.existsById(TEST_TABLE, "two-hundred") } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/ExistsFunctions.kt b/src/kotlinx/src/test/kotlin/integration/ExistsFunctions.kt new file mode 100644 index 0000000..800c60c --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/ExistsFunctions.kt @@ -0,0 +1,66 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Exists` object + */ +object ExistsFunctions { + + fun byIdMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("The document with ID \"three\" should exist") { db.conn.existsById(TEST_TABLE, "three") } + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("The document with ID \"seven\" should not exist") { db.conn.existsById(TEST_TABLE, "seven") } + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByFields(TEST_TABLE, listOf(Field.equal("numValue", 10))) + } + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("No matching documents should have been found") { + db.conn.existsByFields(TEST_TABLE, listOf(Field.equal("nothing", "none"))) + } + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByContains(TEST_TABLE, mapOf("value" to "purple")) + } + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("Matching documents should not have been found") { + db.conn.existsByContains(TEST_TABLE, mapOf("value" to "violet")) + } + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertTrue("Matching documents should have been found") { + db.conn.existsByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + } + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("Matching documents should not have been found") { + db.conn.existsByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10.1)") + } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/FindFunctions.kt b/src/kotlinx/src/test/kotlin/integration/FindFunctions.kt new file mode 100644 index 0000000..58b8e87 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/FindFunctions.kt @@ -0,0 +1,300 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.ArrayDocument +import solutions.bitbadger.documents.kotlinx.tests.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.NumIdDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Integration tests for the `Find` object + */ +object FindFunctions { + + fun allDefault(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals(5, db.conn.findAll(TEST_TABLE).size, "There should have been 5 documents returned") + } + + fun allAscending(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findAll(TEST_TABLE, listOf(Field.named("id"))) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals( + "five|four|one|three|two", + docs.joinToString("|") { it.id }, + "The documents were not ordered correctly" + ) + } + + fun allDescending(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findAll(TEST_TABLE, listOf(Field.named("id DESC"))) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals( + "two|three|one|four|five", + docs.joinToString("|") { it.id }, + "The documents were not ordered correctly" + ) + } + + fun allNumOrder(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findAll( + TEST_TABLE, + listOf(Field.named("sub.foo NULLS LAST"), Field.named("n:numValue")) + ) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals( + "two|four|one|three|five", + docs.joinToString("|") { it.id }, + "The documents were not ordered correctly" + ) + } + + fun allEmpty(db: ThrowawayDatabase) = + assertEquals(0, db.conn.findAll(TEST_TABLE).size, "There should have been no documents returned") + + fun byIdString(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findById(TEST_TABLE, "two") + assertNotNull(doc, "The document should have been returned") + assertEquals("two", doc.id, "An incorrect document was returned") + } + + fun byIdNumber(db: ThrowawayDatabase) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + val doc = db.conn.findById(TEST_TABLE, 18) + assertNotNull(doc, "The document should have been returned") + } finally { + Configuration.idField = "id" + } + } + + fun byIdNotFound(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertNull( + db.conn.findById(TEST_TABLE, "x"), + "There should have been no document returned" + ) + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByFields( + TEST_TABLE, + listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), + FieldMatch.ALL + ) + assertEquals(1, docs.size, "There should have been a document returned") + assertEquals("four", docs[0].id, "The incorrect document was returned") + } + + fun byFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByFields( + TEST_TABLE, + listOf(Field.equal("value", "purple")), + orderBy = listOf(Field.named("id")) + ) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("five|four", docs.joinToString("|") { it.id }, "The documents were not ordered correctly") + } + + fun byFieldsMatchNumIn(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + assertEquals(1, docs.size, "There should have been a document returned") + assertEquals("three", docs[0].id, "The incorrect document was returned") + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0, + db.conn.findByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))).size, + "There should have been no documents returned" + ) + } + + fun byFieldsMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val docs = + db.conn.findByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + assertEquals(2, docs.size, "There should have been two documents returned") + assertTrue(listOf("first", "second").contains(docs[0].id), "An incorrect document was returned (${docs[0].id})") + assertTrue(listOf("first", "second").contains(docs[1].id), "An incorrect document was returned (${docs[1].id})") + } + + fun byFieldsNoMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals( + 0, + db.conn.findByFields( + TEST_TABLE, + listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) + ).size, + "There should have been no documents returned" + ) + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByContains>(TEST_TABLE, mapOf("value" to "purple")) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertTrue(listOf("four", "five").contains(docs[0].id), "An incorrect document was returned (${docs[0].id})") + assertTrue(listOf("four", "five").contains(docs[1].id), "An incorrect document was returned (${docs[1].id})") + } + + fun byContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByContains>>( + TEST_TABLE, + mapOf("sub" to mapOf("foo" to "green")), + listOf(Field.named("value")) + ) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("two|four", docs.joinToString("|") { it.id }, "The documents were not ordered correctly") + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0, + db.conn.findByContains>(TEST_TABLE, mapOf("value" to "indigo")).size, + "There should have been no documents returned" + ) + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertTrue(listOf("four", "five").contains(docs[0].id), "An incorrect document was returned (${docs[0].id})") + assertTrue(listOf("four", "five").contains(docs[1].id), "An incorrect document was returned (${docs[1].id})") + } + + fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val docs = db.conn.findByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("five|four", docs.joinToString("|") { it.id }, "The documents were not ordered correctly") + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0, + db.conn.findByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)").size, + "There should have been no documents returned" + ) + } + + fun firstByFieldsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) + assertNotNull(doc, "There should have been a document returned") + assertEquals("two", doc.id, "The incorrect document was returned") + } + + fun firstByFieldsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) + assertNotNull(doc, "There should have been a document returned") + assertTrue(listOf("two", "four").contains(doc.id), "An incorrect document was returned (${doc.id})") + } + + fun firstByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByFields( + TEST_TABLE, + listOf(Field.equal("sub.foo", "green")), + orderBy = listOf(Field.named("n:numValue DESC")) + ) + assertNotNull(doc, "There should have been a document returned") + assertEquals("four", doc.id, "An incorrect document was returned") + } + + fun firstByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertNull( + db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), + "There should have been no document returned" + ) + } + + fun firstByContainsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByContains>(TEST_TABLE, mapOf("value" to "FIRST!")) + assertNotNull(doc, "There should have been a document returned") + assertEquals("one", doc.id, "An incorrect document was returned") + } + + fun firstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByContains>(TEST_TABLE, mapOf("value" to "purple")) + assertNotNull(doc, "There should have been a document returned") + assertTrue(listOf("four", "five").contains(doc.id), "An incorrect document was returned (${doc.id})") + } + + fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByContains>( + TEST_TABLE, + mapOf("value" to "purple"), + listOf(Field.named("sub.bar NULLS FIRST")) + ) + assertNotNull(doc, "There should have been a document returned") + assertEquals("five", doc.id, "An incorrect document was returned") + } + + fun firstByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertNull( + db.conn.findFirstByContains>( + TEST_TABLE, + mapOf("value" to "indigo") + ), "There should have been no document returned" + ) + } + + fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + assertNotNull(doc, "There should have been a document returned") + assertEquals("two", doc.id, "An incorrect document was returned") + } + + fun firstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + assertNotNull(doc, "There should have been a document returned") + assertTrue(listOf("four", "five").contains(doc.id), "An incorrect document was returned (${doc.id})") + } + + fun firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath( + TEST_TABLE, + "$.numValue ? (@ > 10)", + listOf(Field.named("id DESC")) + ) + assertNotNull(doc, "There should have been a document returned") + assertEquals("four", doc.id, "An incorrect document was returned") + } + + fun firstByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertNull( + db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no document returned" + ) + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt new file mode 100644 index 0000000..8ba92e9 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt @@ -0,0 +1,719 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.* +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Tests for the JSON-returning functions + * + * NOTE: PostgreSQL JSONB columns do not preserve the original JSON with which a document was stored. These tests are + * the most complex within the library, as they have split testing based on the backing data store. The PostgreSQL tests + * check IDs (and, in the case of ordered queries, which ones occur before which others) vs. the entire JSON string. + * Meanwhile, SQLite stores JSON as text, and will return exactly the JSON it was given when it was originally written. + * These tests can ensure the expected round-trip of the entire JSON string. + */ +object JsonFunctions { + + /** + * PostgreSQL, when returning JSONB as a string, has spaces after commas and colons delineating fields and values. + * This function will do a crude string replacement to match the target string based on the dialect being tested. + * + * @param json The JSON which should be returned + * @return The actual expected JSON based on the database being tested + */ + fun maybeJsonB(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> json + Dialect.POSTGRESQL -> json.replace("\":", "\": ").replace(",\"", ", \"") + } + + /** + * Create a snippet of JSON to find a document ID + * + * @param id The ID of the document + * @return A connection-aware ID to check for presence and positioning + */ + private fun docId(id: String) = + maybeJsonB("{\"id\":\"$id\"") + + private fun checkAllDefault(json: String) { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + when (Configuration.dialect()) { + Dialect.SQLITE -> { + assertTrue(json.contains(JsonDocument.one), "Document 'one' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.two), "Document 'two' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.three), "Document 'three' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found in JSON ($json)") + } + Dialect.POSTGRESQL -> { + assertTrue(json.contains(docId("one")), "Document 'one' not found in JSON ($json)") + assertTrue(json.contains(docId("two")), "Document 'two' not found in JSON ($json)") + assertTrue(json.contains(docId("three")), "Document 'three' not found in JSON ($json)") + assertTrue(json.contains(docId("four")), "Document 'four' not found in JSON ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found in JSON ($json)") + } + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + fun allDefault(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkAllDefault(db.conn.jsonAll(TEST_TABLE)) + } + + fun writeAllDefault(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonAll(TEST_TABLE, writer) + checkAllDefault(output.toString()) + } + + private fun checkAllEmpty(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun allEmpty(db: ThrowawayDatabase) = + checkAllEmpty(db.conn.jsonAll(TEST_TABLE)) + + fun writeAllEmpty(db: ThrowawayDatabase) { + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonAll(TEST_TABLE, writer) + checkAllEmpty(output.toString()) + } + + private fun checkByIdString(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "An incorrect document was returned ($json)") + } + + fun byIdString(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByIdString(db.conn.jsonById(TEST_TABLE, "two")) + } + + fun writeByIdString(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, "two") + checkByIdString(output.toString()) + } + + private fun checkByIdNumber(json: String) = + assertEquals( + maybeJsonB("""{"key":18,"text":"howdy"}"""), + json, + "The document should have been found by numeric ID" + ) + + fun byIdNumber(db: ThrowawayDatabase) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + checkByIdNumber(db.conn.jsonById(TEST_TABLE, 18)) + } finally { + Configuration.idField = "id" + } + } + + fun writeByIdNumber(db: ThrowawayDatabase) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, 18) + checkByIdNumber(output.toString()) + } finally { + Configuration.idField = "id" + } + } + + private fun checkByIdNotFound(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun byIdNotFound(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByIdNotFound(db.conn.jsonById(TEST_TABLE, "x")) + } + + fun writeByIdNotFound(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, "x") + checkByIdNotFound(output.toString()) + } + + private fun checkByFieldsMatch(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals("[${JsonDocument.four}]", json, "The incorrect document was returned") + Dialect.POSTGRESQL -> { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("four")),"The incorrect document was returned ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByFieldsMatch( + db.conn.jsonByFields( + TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL + ) + ) + } + + fun writeByFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields( + TEST_TABLE, + writer, + listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), + FieldMatch.ALL + ) + checkByFieldsMatch(output.toString()) + } + + private fun checkByFieldsMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.five},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + + Dialect.POSTGRESQL -> { + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, "Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, "Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByFieldsMatchOrdered( + db.conn.jsonByFields( + TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + ) + ) + } + + fun writeByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields( + TEST_TABLE, writer, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + ) + checkByFieldsMatchOrdered(output.toString()) + } + + private fun checkByFieldsMatchNumIn(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals("[${JsonDocument.three}]", json, "The incorrect document was returned") + Dialect.POSTGRESQL -> { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("three")), "The incorrect document was returned ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byFieldsMatchNumIn(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByFieldsMatchNumIn(db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8))))) + } + + fun writeByFieldsMatchNumIn(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + checkByFieldsMatchNumIn(output.toString()) + } + + private fun checkByFieldsNoMatch(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByFieldsNoMatch(db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100)))) + } + + fun writeByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, listOf(Field.greater("numValue", 100))) + checkByFieldsNoMatch(output.toString()) + } + + private fun checkByFieldsMatchInArray(json: String) { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(json.contains(docId("first")), "The 'first' document was not found ($json)") + assertTrue(json.contains(docId("second")), "The 'second' document was not found ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + fun byFieldsMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + checkByFieldsMatchInArray( + db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + ) + } + + fun writeByFieldsMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + checkByFieldsMatchInArray(output.toString()) + } + + private fun checkByFieldsNoMatchInArray(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun byFieldsNoMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + checkByFieldsNoMatchInArray( + db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j")))) + ) + } + + fun writeByFieldsNoMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, listOf(Field.inArray("values", TEST_TABLE, listOf("j")))) + checkByFieldsNoMatchInArray(output.toString()) + } + + private fun checkByContainsMatch(json: String) { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + when (Configuration.dialect()) { + Dialect.SQLITE -> { + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found ($json)") + } + Dialect.POSTGRESQL -> { + assertTrue(json.contains(docId("four")), "Document 'four' not found ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found ($json)") + } + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByContainsMatch(db.conn.jsonByContains>(TEST_TABLE, mapOf("value" to "purple"))) + } + + fun writeByContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains>(TEST_TABLE, writer, mapOf("value" to "purple")) + checkByContainsMatch(output.toString()) + } + + private fun checkByContainsMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.two},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + Dialect.POSTGRESQL -> { + val twoIdx = json.indexOf(docId("two")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(twoIdx >= 0, "Document 'two' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(twoIdx < fourIdx, "Document 'two' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByContainsMatchOrdered( + db.conn.jsonByContains>>( + TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) + ) + ) + } + + fun writeByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains>>( + TEST_TABLE, writer, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) + ) + checkByContainsMatchOrdered(output.toString()) + } + + private fun checkByContainsNoMatch(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByContainsNoMatch(db.conn.jsonByContains>(TEST_TABLE, mapOf("value" to "indigo"))) + } + + fun writeByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains>(TEST_TABLE, writer, mapOf("value" to "indigo")) + checkByContainsNoMatch(output.toString()) + } + + private fun checkByJsonPathMatch(json: String) { + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + when (Configuration.dialect()) { + Dialect.SQLITE -> { + assertTrue(json.contains(JsonDocument.four), "Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), "Document 'five' not found ($json)") + } + Dialect.POSTGRESQL -> { + assertTrue(json.contains(docId("four")), "Document 'four' not found ($json)") + assertTrue(json.contains(docId("five")), "Document 'five' not found ($json)") + } + } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByJsonPathMatch(db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)")) + } + + fun writeByJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)") + checkByJsonPathMatch(output.toString()) + } + + private fun checkByJsonPathMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals( + "[${JsonDocument.five},${JsonDocument.four}]", json, "The documents were not ordered correctly" + ) + + Dialect.POSTGRESQL -> { + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, "Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, "Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, "Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") + } + } + + fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByJsonPathMatchOrdered( + db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + ) + } + + fun writeByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + checkByJsonPathMatchOrdered(output.toString()) + } + + private fun checkByJsonPathNoMatch(json: String) = + assertEquals("[]", json, "There should have been no documents returned") + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkByJsonPathNoMatch(db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)")) + } + + fun writeByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 100)") + checkByJsonPathNoMatch(output.toString()) + } + + private fun checkFirstByFieldsMatchOne(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "The incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "The incorrect document was returned ($json)") + } + + fun firstByFieldsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByFieldsMatchOne(db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another")))) + } + + fun writeFirstByFieldsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, listOf(Field.equal("value", "another"))) + checkFirstByFieldsMatchOne(output.toString()) + } + + private fun checkFirstByFieldsMatchMany(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.two) || json.contains(JsonDocument.four), + "Expected document 'two' or 'four' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("two")) || json.contains(docId("four")), + "Expected document 'two' or 'four' ($json)" + ) + } + + fun firstByFieldsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByFieldsMatchMany(db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green")))) + } + + fun writeFirstByFieldsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, listOf(Field.equal("sub.foo", "green"))) + checkFirstByFieldsMatchMany(output.toString()) + } + + private fun checkFirstByFieldsMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.four, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("four")), "An incorrect document was returned ($json)") + } + + fun firstByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByFieldsMatchOrdered( + db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) + ) + ) + } + + fun writeFirstByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields( + TEST_TABLE, + writer, + listOf(Field.equal("sub.foo", "green")), + orderBy = listOf(Field.named("n:numValue DESC")) + ) + checkFirstByFieldsMatchOrdered(output.toString()) + } + + private fun checkFirstByFieldsNoMatch(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun firstByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByFieldsNoMatch(db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent")))) + } + + fun writeFirstByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, listOf(Field.equal("value", "absent"))) + checkFirstByFieldsNoMatch(output.toString()) + } + + private fun checkFirstByContainsMatchOne(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.one, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("one")), "An incorrect document was returned ($json)") + } + + fun firstByContainsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsMatchOne( + db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "FIRST!")) + ) + } + + fun writeFirstByContainsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains>(TEST_TABLE, writer, mapOf("value" to "FIRST!")) + } + + private fun checkFirstByContainsMatchMany(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + "Expected document 'four' or 'five' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("four")) || json.contains(docId("five")), + "Expected document 'four' or 'five' ($json)" + ) + } + + fun firstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsMatchMany( + db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "purple")) + ) + } + + fun writeFirstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains>(TEST_TABLE, writer, mapOf("value" to "purple")) + checkFirstByContainsMatchMany(output.toString()) + } + + private fun checkFirstByContainsMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.five, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("five")), "An incorrect document was returned ($json)") + } + + fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsMatchOrdered( + db.conn.jsonFirstByContains>( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) + ) + } + + fun writeFirstByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains>( + TEST_TABLE, writer, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) + checkFirstByContainsMatchOrdered(output.toString()) + } + + private fun checkFirstByContainsNoMatch(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun firstByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsNoMatch( + db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "indigo")) + ) + } + + fun writeFirstByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains>(TEST_TABLE, writer, mapOf("value" to "indigo")) + checkFirstByContainsNoMatch(output.toString()) + } + + private fun checkFirstByJsonPathMatchOne(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "An incorrect document was returned ($json)") + } + + fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathMatchOne(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)")) + } + + fun writeFirstByJsonPathMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ == 10)") + checkFirstByJsonPathMatchOne(output.toString()) + } + + private fun checkFirstByJsonPathMatchMany(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertTrue( + json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + "Expected document 'four' or 'five' ($json)" + ) + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("four")) || json.contains(docId("five")), + "Expected document 'four' or 'five' ($json)" + ) + } + + fun firstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathMatchMany(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)")) + } + + fun writeFirstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)") + checkFirstByJsonPathMatchMany(output.toString()) + } + + private fun checkFirstByJsonPathMatchOrdered(json: String) = + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.four, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("four")), "An incorrect document was returned ($json)") + } + + fun firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathMatchOrdered( + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + ) + } + + fun writeFirstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + checkFirstByJsonPathMatchOrdered(output.toString()) + } + + private fun checkFirstByJsonPathNoMatch(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun firstByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathNoMatch(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)")) + } + + fun writeFirstByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 100)") + checkFirstByJsonPathNoMatch(output.toString()) + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/PatchFunctions.kt b/src/kotlinx/src/test/kotlin/integration/PatchFunctions.kt new file mode 100644 index 0000000..d8e81ab --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PatchFunctions.kt @@ -0,0 +1,90 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Integration tests for the `Patch` object + */ +object PatchFunctions { + + fun byIdMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.patchById(TEST_TABLE, "one", mapOf("numValue" to 44)) + val doc = db.conn.findById(TEST_TABLE, "one") + assertNotNull(doc, "There should have been a document returned") + assertEquals("one", doc.id, "An incorrect document was returned") + assertEquals(44, doc.numValue, "The document was not patched") + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse("Document with ID \"forty-seven\" should not exist") { + db.conn.existsById(TEST_TABLE, "forty-seven") + } + db.conn.patchById(TEST_TABLE, "forty-seven", mapOf("foo" to "green")) // no exception = pass + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.patchByFields(TEST_TABLE, listOf(Field.equal("value", "purple")), mapOf("numValue" to 77)) + assertEquals( + 2, + db.conn.countByFields(TEST_TABLE, listOf(Field.equal("numValue", 77))), + "There should have been 2 documents with numeric value 77" + ) + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val fields = listOf(Field.equal("value", "burgundy")) + assertFalse("There should be no documents with value of \"burgundy\"") { + db.conn.existsByFields(TEST_TABLE, fields) + } + db.conn.patchByFields(TEST_TABLE, fields, mapOf("foo" to "green")) // no exception = pass + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val contains = mapOf("value" to "another") + db.conn.patchByContains(TEST_TABLE, contains, mapOf("numValue" to 12)) + val doc = db.conn.findFirstByContains>(TEST_TABLE, contains) + assertNotNull(doc, "There should have been a document returned") + assertEquals("two", doc.id, "The incorrect document was returned") + assertEquals(12, doc.numValue, "The document was not updated") + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val contains = mapOf("value" to "updated") + assertFalse("There should be no matching documents") { db.conn.existsByContains(TEST_TABLE, contains) } + db.conn.patchByContains(TEST_TABLE, contains, mapOf("sub.foo" to "green")) // no exception = pass + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val path = "$.numValue ? (@ > 10)" + db.conn.patchByJsonPath(TEST_TABLE, path, mapOf("value" to "blue")) + val docs = db.conn.findByJsonPath(TEST_TABLE, path) + assertEquals(2, docs.size, "There should have been two documents returned") + docs.forEach { + assertTrue(listOf("four", "five").contains(it.id), "An incorrect document was returned (${it.id})") + assertEquals("blue", it.value, "The value for ID ${it.id} was incorrect") + } + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val path = "$.numValue ? (@ > 100)" + assertFalse("There should be no documents with numeric values over 100") { + db.conn.existsByJsonPath(TEST_TABLE, path) + } + db.conn.patchByJsonPath(TEST_TABLE, path, mapOf("value" to "blue")) // no exception = pass + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/PgDB.kt b/src/kotlinx/src/test/kotlin/integration/PgDB.kt new file mode 100644 index 0000000..a372702 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PgDB.kt @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.Results +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE + +/** + * A wrapper for a throwaway PostgreSQL database + */ +class PgDB : ThrowawayDatabase() { + + init { + Configuration.connectionString = connString("postgres") + Configuration.dbConn().use { it.customNonQuery("CREATE DATABASE $dbName") } + Configuration.connectionString = connString(dbName) + } + + override val conn = Configuration.dbConn() + + init { + conn.ensureTable(TEST_TABLE) + } + + override fun close() { + conn.close() + Configuration.connectionString = connString("postgres") + Configuration.dbConn().use { it.customNonQuery("DROP DATABASE $dbName") } + Configuration.connectionString = null + } + + override fun dbObjectExists(name: String) = + conn.customScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = :name) AS it", + listOf(Parameter(":name", ParameterType.STRING, name)), Results::toExists) + + companion object { + + /** + * Create a connection string for the given database + * + * @param database The database to which the library should connect + * @return The connection string for the database + */ + private fun connString(database: String) = + "jdbc:postgresql://localhost/$database?user=postgres&password=postgres" + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLCountIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCountIT.kt new file mode 100644 index 0000000..93f56d7 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCountIT.kt @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Count") +class PostgreSQLCountIT { + + @Test + @DisplayName("all counts all documents") + fun all() = + PgDB().use(CountFunctions::all) + + @Test + @DisplayName("byFields counts documents by a numeric value") + fun byFieldsNumeric() = + PgDB().use(CountFunctions::byFieldsNumeric) + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + fun byFieldsAlpha() = + PgDB().use(CountFunctions::byFieldsAlpha) + + @Test + @DisplayName("byContains counts documents when matches are found") + fun byContainsMatch() = + PgDB().use(CountFunctions::byContainsMatch) + + @Test + @DisplayName("byContains counts documents when no matches are found") + fun byContainsNoMatch() = + PgDB().use(CountFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath counts documents when matches are found") + fun byJsonPathMatch() = + PgDB().use(CountFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath counts documents when no matches are found") + fun byJsonPathNoMatch() = + PgDB().use(CountFunctions::byJsonPathNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt new file mode 100644 index 0000000..2d2f271 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -0,0 +1,87 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName + +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Custom") +class PostgreSQLCustomIT { + + @Test + @DisplayName("list succeeds with empty list") + fun listEmpty() = + PgDB().use(CustomFunctions::listEmpty) + + @Test + @DisplayName("list succeeds with a non-empty list") + fun listAll() = + PgDB().use(CustomFunctions::listAll) + + @Test + @DisplayName("jsonArray succeeds with empty array") + fun jsonArrayEmpty() = + PgDB().use(CustomFunctions::jsonArrayEmpty) + + @Test + @DisplayName("jsonArray succeeds with a single-item array") + fun jsonArraySingle() = + PgDB().use(CustomFunctions::jsonArraySingle) + + @Test + @DisplayName("jsonArray succeeds with a multi-item array") + fun jsonArrayMany() = + PgDB().use(CustomFunctions::jsonArrayMany) + + @Test + @DisplayName("writeJsonArray succeeds with empty array") + fun writeJsonArrayEmpty() = + PgDB().use(CustomFunctions::writeJsonArrayEmpty) + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + fun writeJsonArraySingle() = + PgDB().use(CustomFunctions::writeJsonArraySingle) + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + fun writeJsonArrayMany() = + PgDB().use(CustomFunctions::writeJsonArrayMany) + + @Test + @DisplayName("single succeeds when document not found") + fun singleNone() = + PgDB().use(CustomFunctions::singleNone) + + @Test + @DisplayName("single succeeds when a document is found") + fun singleOne() = + PgDB().use(CustomFunctions::singleOne) + + @Test + @DisplayName("jsonSingle succeeds when document not found") + fun jsonSingleNone() = + PgDB().use(CustomFunctions::jsonSingleNone) + + @Test + @DisplayName("jsonSingle succeeds when a document is found") + fun jsonSingleOne() = + PgDB().use(CustomFunctions::jsonSingleOne) + + @Test + @DisplayName("nonQuery makes changes") + fun nonQueryChanges() = + PgDB().use(CustomFunctions::nonQueryChanges) + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + fun nonQueryNoChanges() = + PgDB().use(CustomFunctions::nonQueryNoChanges) + + @Test + @DisplayName("scalar succeeds") + fun scalar() = + PgDB().use(CustomFunctions::scalar) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt new file mode 100644 index 0000000..4b898f0 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt @@ -0,0 +1,31 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Definition") +class PostgreSQLDefinitionIT { + + @Test + @DisplayName("ensureTable creates table and index") + fun ensureTable() = + PgDB().use(DefinitionFunctions::ensureTable) + + @Test + @DisplayName("ensureFieldIndex creates an index") + fun ensureFieldIndex() = + PgDB().use(DefinitionFunctions::ensureFieldIndex) + + @Test + @DisplayName("ensureDocumentIndex creates a full index") + fun ensureDocumentIndexFull() = + PgDB().use(DefinitionFunctions::ensureDocumentIndexFull) + + @Test + @DisplayName("ensureDocumentIndex creates an optimized index") + fun ensureDocumentIndexOptimized() = + PgDB().use(DefinitionFunctions::ensureDocumentIndexOptimized) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLDeleteIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDeleteIT.kt new file mode 100644 index 0000000..280acbf --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDeleteIT.kt @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Delete") +class PostgreSQLDeleteIT { + + @Test + @DisplayName("byId deletes a matching ID") + fun byIdMatch() = + PgDB().use(DeleteFunctions::byIdMatch) + + @Test + @DisplayName("byId succeeds when no ID matches") + fun byIdNoMatch() = + PgDB().use(DeleteFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields deletes matching documents") + fun byFieldsMatch() = + PgDB().use(DeleteFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(DeleteFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains deletes matching documents") + fun byContainsMatch() = + PgDB().use(DeleteFunctions::byContainsMatch) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(DeleteFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath deletes matching documents") + fun byJsonPathMatch() = + PgDB().use(DeleteFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(DeleteFunctions::byJsonPathNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLDocumentIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDocumentIT.kt new file mode 100644 index 0000000..6292f7f --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDocumentIT.kt @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Document") +class PostgreSQLDocumentIT { + + @Test + @DisplayName("insert works with default values") + fun insertDefault() = + PgDB().use(DocumentFunctions::insertDefault) + + @Test + @DisplayName("insert fails with duplicate key") + fun insertDupe() = + PgDB().use(DocumentFunctions::insertDupe) + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + fun insertNumAutoId() = + PgDB().use(DocumentFunctions::insertNumAutoId) + + @Test + @DisplayName("insert succeeds with UUID auto ID") + fun insertUUIDAutoId() = + PgDB().use(DocumentFunctions::insertUUIDAutoId) + + @Test + @DisplayName("insert succeeds with random string auto ID") + fun insertStringAutoId() = + PgDB().use(DocumentFunctions::insertStringAutoId) + + @Test + @DisplayName("save updates an existing document") + fun saveMatch() = + PgDB().use(DocumentFunctions::saveMatch) + + @Test + @DisplayName("save inserts a new document") + fun saveNoMatch() = + PgDB().use(DocumentFunctions::saveNoMatch) + + @Test + @DisplayName("update replaces an existing document") + fun updateMatch() = + PgDB().use(DocumentFunctions::updateMatch) + + @Test + @DisplayName("update succeeds when no document exists") + fun updateNoMatch() = + PgDB().use(DocumentFunctions::updateNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLExistsIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLExistsIT.kt new file mode 100644 index 0000000..db868e0 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLExistsIT.kt @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Exists") +class PostgreSQLExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + fun byIdMatch() = + PgDB().use(ExistsFunctions::byIdMatch) + + @Test + @DisplayName("byId returns false when no document matches the ID") + fun byIdNoMatch() = + PgDB().use(ExistsFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields returns true when documents match") + fun byFieldsMatch() = + PgDB().use(ExistsFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields returns false when no documents match") + fun byFieldsNoMatch() = + PgDB().use(ExistsFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains returns true when documents match") + fun byContainsMatch() = + PgDB().use(ExistsFunctions::byContainsMatch) + + @Test + @DisplayName("byContains returns false when no documents match") + fun byContainsNoMatch() = + PgDB().use(ExistsFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath returns true when documents match") + fun byJsonPathMatch() = + PgDB().use(ExistsFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath returns false when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(ExistsFunctions::byJsonPathNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLFindIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLFindIT.kt new file mode 100644 index 0000000..c6eedea --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLFindIT.kt @@ -0,0 +1,171 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Find") +class PostgreSQLFindIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + PgDB().use(FindFunctions::allDefault) + + @Test + @DisplayName("all sorts data ascending") + fun allAscending() = + PgDB().use(FindFunctions::allAscending) + + @Test + @DisplayName("all sorts data descending") + fun allDescending() = + PgDB().use(FindFunctions::allDescending) + + @Test + @DisplayName("all sorts data numerically") + fun allNumOrder() = + PgDB().use(FindFunctions::allNumOrder) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + PgDB().use(FindFunctions::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + PgDB().use(FindFunctions::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + PgDB().use(FindFunctions::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + PgDB().use(FindFunctions::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + PgDB().use(FindFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + PgDB().use(FindFunctions::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + PgDB().use(FindFunctions::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(FindFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + PgDB().use(FindFunctions::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + PgDB().use(FindFunctions::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains retrieves matching documents") + fun byContainsMatch() = + PgDB().use(FindFunctions::byContainsMatch) + + @Test + @DisplayName("byContains retrieves ordered matching documents") + fun byContainsMatchOrdered() = + PgDB().use(FindFunctions::byContainsMatchOrdered) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(FindFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath retrieves matching documents") + fun byJsonPathMatch() = + PgDB().use(FindFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + fun byJsonPathMatchOrdered() = + PgDB().use(FindFunctions::byJsonPathMatchOrdered) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(FindFunctions::byJsonPathNoMatch) + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + PgDB().use(FindFunctions::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + PgDB().use(FindFunctions::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + PgDB().use(FindFunctions::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + PgDB().use(FindFunctions::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains retrieves a matching document") + fun firstByContainsMatchOne() = + PgDB().use(FindFunctions::firstByContainsMatchOne) + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + fun firstByContainsMatchMany() = + PgDB().use(FindFunctions::firstByContainsMatchMany) + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + fun firstByContainsMatchOrdered() = + PgDB().use(FindFunctions::firstByContainsMatchOrdered) + + @Test + @DisplayName("firstByContains returns null when no document matches") + fun firstByContainsNoMatch() = + PgDB().use(FindFunctions::firstByContainsNoMatch) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + fun firstByJsonPathMatchOne() = + PgDB().use(FindFunctions::firstByJsonPathMatchOne) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + fun firstByJsonPathMatchMany() = + PgDB().use(FindFunctions::firstByJsonPathMatchMany) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + fun firstByJsonPathMatchOrdered() = + PgDB().use(FindFunctions::firstByJsonPathMatchOrdered) + + @Test + @DisplayName("firstByJsonPath returns null when no document matches") + fun firstByJsonPathNoMatch() = + PgDB().use(FindFunctions::firstByJsonPathNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt new file mode 100644 index 0000000..b41be7c --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt @@ -0,0 +1,301 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Json") +class PostgreSQLJsonIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + PgDB().use(JsonFunctions::allDefault) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + PgDB().use(JsonFunctions::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + PgDB().use(JsonFunctions::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + PgDB().use(JsonFunctions::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + PgDB().use(JsonFunctions::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + PgDB().use(JsonFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + PgDB().use(JsonFunctions::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + PgDB().use(JsonFunctions::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(JsonFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + PgDB().use(JsonFunctions::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + PgDB().use(JsonFunctions::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains retrieves matching documents") + fun byContainsMatch() = + PgDB().use(JsonFunctions::byContainsMatch) + + @Test + @DisplayName("byContains retrieves ordered matching documents") + fun byContainsMatchOrdered() = + PgDB().use(JsonFunctions::byContainsMatchOrdered) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(JsonFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath retrieves matching documents") + fun byJsonPathMatch() = + PgDB().use(JsonFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + fun byJsonPathMatchOrdered() = + PgDB().use(JsonFunctions::byJsonPathMatchOrdered) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(JsonFunctions::byJsonPathNoMatch) + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + PgDB().use(JsonFunctions::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + PgDB().use(JsonFunctions::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + PgDB().use(JsonFunctions::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + PgDB().use(JsonFunctions::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains retrieves a matching document") + fun firstByContainsMatchOne() = + PgDB().use(JsonFunctions::firstByContainsMatchOne) + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + fun firstByContainsMatchMany() = + PgDB().use(JsonFunctions::firstByContainsMatchMany) + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + fun firstByContainsMatchOrdered() = + PgDB().use(JsonFunctions::firstByContainsMatchOrdered) + + @Test + @DisplayName("firstByContains returns null when no document matches") + fun firstByContainsNoMatch() = + PgDB().use(JsonFunctions::firstByContainsNoMatch) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + fun firstByJsonPathMatchOne() = + PgDB().use(JsonFunctions::firstByJsonPathMatchOne) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + fun firstByJsonPathMatchMany() = + PgDB().use(JsonFunctions::firstByJsonPathMatchMany) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + fun firstByJsonPathMatchOrdered() = + PgDB().use(JsonFunctions::firstByJsonPathMatchOrdered) + + @Test + @DisplayName("firstByJsonPath returns null when no document matches") + fun firstByJsonPathNoMatch() = + PgDB().use(JsonFunctions::firstByJsonPathNoMatch) + + @Test + @DisplayName("writeAll retrieves all documents") + fun writeAllDefault() = + PgDB().use(JsonFunctions::writeAllDefault) + + @Test + @DisplayName("writeAll succeeds with an empty table") + fun writeAllEmpty() = + PgDB().use(JsonFunctions::writeAllEmpty) + + @Test + @DisplayName("writeById retrieves a document via a string ID") + fun writeByIdString() = + PgDB().use(JsonFunctions::writeByIdString) + + @Test + @DisplayName("writeById retrieves a document via a numeric ID") + fun writeByIdNumber() = + PgDB().use(JsonFunctions::writeByIdNumber) + + @Test + @DisplayName("writeById returns null when a matching ID is not found") + fun writeByIdNotFound() = + PgDB().use(JsonFunctions::writeByIdNotFound) + + @Test + @DisplayName("writeByFields retrieves matching documents") + fun writeByFieldsMatch() = + PgDB().use(JsonFunctions::writeByFieldsMatch) + + @Test + @DisplayName("writeByFields retrieves ordered matching documents") + fun writeByFieldsMatchOrdered() = + PgDB().use(JsonFunctions::writeByFieldsMatchOrdered) + + @Test + @DisplayName("writeByFields retrieves matching documents with a numeric IN clause") + fun writeByFieldsMatchNumIn() = + PgDB().use(JsonFunctions::writeByFieldsMatchNumIn) + + @Test + @DisplayName("writeByFields succeeds when no documents match") + fun writeByFieldsNoMatch() = + PgDB().use(JsonFunctions::writeByFieldsNoMatch) + + @Test + @DisplayName("writeByFields retrieves matching documents with an IN_ARRAY comparison") + fun writeByFieldsMatchInArray() = + PgDB().use(JsonFunctions::writeByFieldsMatchInArray) + + @Test + @DisplayName("writeByFields succeeds when no documents match an IN_ARRAY comparison") + fun writeByFieldsNoMatchInArray() = + PgDB().use(JsonFunctions::writeByFieldsNoMatchInArray) + + @Test + @DisplayName("writeByContains retrieves matching documents") + fun writeByContainsMatch() = + PgDB().use(JsonFunctions::writeByContainsMatch) + + @Test + @DisplayName("writeByContains retrieves ordered matching documents") + fun writeByContainsMatchOrdered() = + PgDB().use(JsonFunctions::writeByContainsMatchOrdered) + + @Test + @DisplayName("writeByContains succeeds when no documents match") + fun writeByContainsNoMatch() = + PgDB().use(JsonFunctions::writeByContainsNoMatch) + + @Test + @DisplayName("writeByJsonPath retrieves matching documents") + fun writeByJsonPathMatch() = + PgDB().use(JsonFunctions::writeByJsonPathMatch) + + @Test + @DisplayName("writeByJsonPath retrieves ordered matching documents") + fun writeByJsonPathMatchOrdered() = + PgDB().use(JsonFunctions::writeByJsonPathMatchOrdered) + + @Test + @DisplayName("writeByJsonPath succeeds when no documents match") + fun writeByJsonPathNoMatch() = + PgDB().use(JsonFunctions::writeByJsonPathNoMatch) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document") + fun writeFirstByFieldsMatchOne() = + PgDB().use(JsonFunctions::writeFirstByFieldsMatchOne) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many") + fun writeFirstByFieldsMatchMany() = + PgDB().use(JsonFunctions::writeFirstByFieldsMatchMany) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many (ordered)") + fun writeFirstByFieldsMatchOrdered() = + PgDB().use(JsonFunctions::writeFirstByFieldsMatchOrdered) + + @Test + @DisplayName("writeFirstByFields returns null when no document matches") + fun writeFirstByFieldsNoMatch() = + PgDB().use(JsonFunctions::writeFirstByFieldsNoMatch) + + @Test + @DisplayName("writeFirstByContains retrieves a matching document") + fun writeFirstByContainsMatchOne() = + PgDB().use(JsonFunctions::writeFirstByContainsMatchOne) + + @Test + @DisplayName("writeFirstByContains retrieves a matching document among many") + fun writeFirstByContainsMatchMany() = + PgDB().use(JsonFunctions::writeFirstByContainsMatchMany) + + @Test + @DisplayName("writeFirstByContains retrieves a matching document among many (ordered)") + fun writeFirstByContainsMatchOrdered() = + PgDB().use(JsonFunctions::writeFirstByContainsMatchOrdered) + + @Test + @DisplayName("writeFirstByContains returns null when no document matches") + fun writeFirstByContainsNoMatch() = + PgDB().use(JsonFunctions::writeFirstByContainsNoMatch) + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document") + fun writeFirstByJsonPathMatchOne() = + PgDB().use(JsonFunctions::writeFirstByJsonPathMatchOne) + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document among many") + fun writeFirstByJsonPathMatchMany() = + PgDB().use(JsonFunctions::writeFirstByJsonPathMatchMany) + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document among many (ordered)") + fun writeFirstByJsonPathMatchOrdered() = + PgDB().use(JsonFunctions::writeFirstByJsonPathMatchOrdered) + + @Test + @DisplayName("writeFirstByJsonPath returns null when no document matches") + fun writeFirstByJsonPathNoMatch() = + PgDB().use(JsonFunctions::writeFirstByJsonPathNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLPatchIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLPatchIT.kt new file mode 100644 index 0000000..afb7066 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLPatchIT.kt @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: Patch") +class PostgreSQLPatchIT { + + @Test + @DisplayName("byId patches an existing document") + fun byIdMatch() = + PgDB().use(PatchFunctions::byIdMatch) + + @Test + @DisplayName("byId succeeds for a non-existent document") + fun byIdNoMatch() = + PgDB().use(PatchFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields patches matching document") + fun byFieldsMatch() = + PgDB().use(PatchFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(PatchFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains patches matching document") + fun byContainsMatch() = + PgDB().use(PatchFunctions::byContainsMatch) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(PatchFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath patches matching document") + fun byJsonPathMatch() = + PgDB().use(PatchFunctions::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(PatchFunctions::byJsonPathNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt new file mode 100644 index 0000000..13f07ec --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt @@ -0,0 +1,71 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("KotlinX | PostgreSQL: RemoveFields") +class PostgreSQLRemoveFieldsIT { + + @Test + @DisplayName("byId removes fields from an existing document") + fun byIdMatchFields() = + PgDB().use(RemoveFieldsFunctions::byIdMatchFields) + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + fun byIdMatchNoFields() = + PgDB().use(RemoveFieldsFunctions::byIdMatchNoFields) + + @Test + @DisplayName("byId succeeds when no document exists") + fun byIdNoMatch() = + PgDB().use(RemoveFieldsFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields removes fields from matching documents") + fun byFieldsMatchFields() = + PgDB().use(RemoveFieldsFunctions::byFieldsMatchFields) + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + fun byFieldsMatchNoFields() = + PgDB().use(RemoveFieldsFunctions::byFieldsMatchNoFields) + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + fun byFieldsNoMatch() = + PgDB().use(RemoveFieldsFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains removes fields from matching documents") + fun byContainsMatchFields() = + PgDB().use(RemoveFieldsFunctions::byContainsMatchFields) + + @Test + @DisplayName("byContains succeeds when fields do not exist on matching documents") + fun byContainsMatchNoFields() = + PgDB().use(RemoveFieldsFunctions::byContainsMatchNoFields) + + @Test + @DisplayName("byContains succeeds when no matching documents exist") + fun byContainsNoMatch() = + PgDB().use(RemoveFieldsFunctions::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath removes fields from matching documents") + fun byJsonPathMatchFields() = + PgDB().use(RemoveFieldsFunctions::byJsonPathMatchFields) + + @Test + @DisplayName("byJsonPath succeeds when fields do not exist on matching documents") + fun byJsonPathMatchNoFields() = + PgDB().use(RemoveFieldsFunctions::byJsonPathMatchNoFields) + + @Test + @DisplayName("byJsonPath succeeds when no matching documents exist") + fun byJsonPathNoMatch() = + PgDB().use(RemoveFieldsFunctions::byJsonPathNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/RemoveFieldsFunctions.kt b/src/kotlinx/src/test/kotlin/integration/RemoveFieldsFunctions.kt new file mode 100644 index 0000000..068ed52 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/RemoveFieldsFunctions.kt @@ -0,0 +1,108 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import kotlin.test.* + +/** + * Integration tests for the `RemoveFields` object + */ +object RemoveFieldsFunctions { + + fun byIdMatchFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.removeFieldsById(TEST_TABLE, "two", listOf("sub", "value")) + val doc = db.conn.findById(TEST_TABLE, "two") + assertNotNull(doc, "There should have been a document returned") + assertEquals("", doc.value, "The value should have been empty") + assertNull(doc.sub, "The sub-document should have been removed") + } + + fun byIdMatchNoFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse { db.conn.existsByFields(TEST_TABLE, listOf(Field.exists("a_field_that_does_not_exist"))) } + db.conn.removeFieldsById(TEST_TABLE, "one", listOf("a_field_that_does_not_exist")) // no exception = pass + } + + fun byIdNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse { db.conn.existsById(TEST_TABLE, "fifty") } + db.conn.removeFieldsById(TEST_TABLE, "fifty", listOf("sub")) // no exception = pass + } + + fun byFieldsMatchFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + val fields = listOf(Field.equal("numValue", 17)) + db.conn.removeFieldsByFields(TEST_TABLE, fields, listOf("sub")) + val doc = db.conn.findFirstByFields(TEST_TABLE, fields) + assertNotNull(doc, "The document should have been returned") + assertEquals("four", doc.id, "An incorrect document was returned") + assertNull(doc.sub, "The sub-document should have been removed") + } + + fun byFieldsMatchNoFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse { db.conn.existsByFields(TEST_TABLE, listOf(Field.exists("nada"))) } + db.conn.removeFieldsByFields(TEST_TABLE, listOf(Field.equal("numValue", 17)), listOf("nada")) // no exn = pass + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val fields = listOf(Field.notEqual("missing", "nope")) + assertFalse { db.conn.existsByFields(TEST_TABLE, fields) } + db.conn.removeFieldsByFields(TEST_TABLE, fields, listOf("value")) // no exception = pass + } + + fun byContainsMatchFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + val criteria = mapOf("sub" to mapOf("foo" to "green")) + db.conn.removeFieldsByContains(TEST_TABLE, criteria, listOf("value")) + val docs = db.conn.findByContains>>(TEST_TABLE, criteria) + assertEquals(2, docs.size, "There should have been 2 documents returned") + docs.forEach { + assertTrue(listOf("two", "four").contains(it.id), "An incorrect document was returned (${it.id})") + assertEquals("", it.value, "The value should have been empty") + } + } + + fun byContainsMatchNoFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse { db.conn.existsByFields(TEST_TABLE, listOf(Field.exists("invalid_field"))) } + db.conn.removeFieldsByContains(TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf("invalid_field")) + // no exception = pass + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val contains = mapOf("value" to "substantial") + assertFalse { db.conn.existsByContains(TEST_TABLE, contains) } + db.conn.removeFieldsByContains(TEST_TABLE, contains, listOf("numValue")) + } + + fun byJsonPathMatchFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + val path = "$.value ? (@ == \"purple\")" + db.conn.removeFieldsByJsonPath(TEST_TABLE, path, listOf("sub")) + val docs = db.conn.findByJsonPath(TEST_TABLE, path) + assertEquals(2, docs.size, "There should have been 2 documents returned") + docs.forEach { + assertTrue(listOf("four", "five").contains(it.id), "An incorrect document was returned (${it.id})") + assertNull(it.sub, "The sub-document should have been removed") + } + } + + fun byJsonPathMatchNoFields(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertFalse { db.conn.existsByFields(TEST_TABLE, listOf(Field.exists("submarine"))) } + db.conn.removeFieldsByJsonPath(TEST_TABLE, "$.value ? (@ == \"purple\")", listOf("submarine")) // no exn = pass + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val path = "$.value ? (@ == \"mauve\")" + assertFalse { db.conn.existsByJsonPath(TEST_TABLE, path) } + db.conn.removeFieldsByJsonPath(TEST_TABLE, path, listOf("value")) // no exception = pass + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteCountIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteCountIT.kt new file mode 100644 index 0000000..32b256e --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteCountIT.kt @@ -0,0 +1,40 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Count") +class SQLiteCountIT { + + @Test + @DisplayName("all counts all documents") + fun all() = + SQLiteDB().use(CountFunctions::all) + + @Test + @DisplayName("byFields counts documents by a numeric value") + fun byFieldsNumeric() = + SQLiteDB().use(CountFunctions::byFieldsNumeric) + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + fun byFieldsAlpha() = + SQLiteDB().use(CountFunctions::byFieldsAlpha) + + @Test + @DisplayName("byContains fails") + fun byContainsMatch() { + assertThrows { SQLiteDB().use(CountFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathMatch() { + assertThrows { SQLiteDB().use(CountFunctions::byJsonPathMatch) } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt new file mode 100644 index 0000000..5c6194d --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -0,0 +1,86 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * SQLite integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Custom") +class SQLiteCustomIT { + + @Test + @DisplayName("list succeeds with empty list") + fun listEmpty() = + SQLiteDB().use(CustomFunctions::listEmpty) + + @Test + @DisplayName("list succeeds with a non-empty list") + fun listAll() = + SQLiteDB().use(CustomFunctions::listAll) + + @Test + @DisplayName("jsonArray succeeds with empty array") + fun jsonArrayEmpty() = + SQLiteDB().use(CustomFunctions::jsonArrayEmpty) + + @Test + @DisplayName("jsonArray succeeds with a single-item array") + fun jsonArraySingle() = + SQLiteDB().use(CustomFunctions::jsonArraySingle) + + @Test + @DisplayName("jsonArray succeeds with a multi-item array") + fun jsonArrayMany() = + SQLiteDB().use(CustomFunctions::jsonArrayMany) + + @Test + @DisplayName("writeJsonArray succeeds with empty array") + fun writeJsonArrayEmpty() = + SQLiteDB().use(CustomFunctions::writeJsonArrayEmpty) + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + fun writeJsonArraySingle() = + SQLiteDB().use(CustomFunctions::writeJsonArraySingle) + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + fun writeJsonArrayMany() = + SQLiteDB().use(CustomFunctions::writeJsonArrayMany) + + @Test + @DisplayName("single succeeds when document not found") + fun singleNone() = + SQLiteDB().use(CustomFunctions::singleNone) + + @Test + @DisplayName("single succeeds when a document is found") + fun singleOne() = + SQLiteDB().use(CustomFunctions::singleOne) + + @Test + @DisplayName("jsonSingle succeeds when document not found") + fun jsonSingleNone() = + SQLiteDB().use(CustomFunctions::jsonSingleNone) + + @Test + @DisplayName("jsonSingle succeeds when a document is found") + fun jsonSingleOne() = + SQLiteDB().use(CustomFunctions::jsonSingleOne) + + @Test + @DisplayName("nonQuery makes changes") + fun nonQueryChanges() = + SQLiteDB().use(CustomFunctions::nonQueryChanges) + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + fun nonQueryNoChanges() = + SQLiteDB().use(CustomFunctions::nonQueryNoChanges) + + @Test + @DisplayName("scalar succeeds") + fun scalar() = + SQLiteDB().use(CustomFunctions::scalar) +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt new file mode 100644 index 0000000..ab69d33 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlinx.extensions.* +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import solutions.bitbadger.documents.kotlinx.Results +import java.io.File + +/** + * A wrapper for a throwaway SQLite database + */ +class SQLiteDB : ThrowawayDatabase() { + + init { + Configuration.connectionString = "jdbc:sqlite:$dbName" + } + + override val conn = Configuration.dbConn() + + init { + conn.ensureTable(TEST_TABLE) + } + + override fun close() { + conn.close() + File(dbName).delete() + } + + override fun dbObjectExists(name: String) = + conn.customScalar("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = :name) AS it", + listOf(Parameter(":name", ParameterType.STRING, name)), Results::toExists) +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteDefinitionIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteDefinitionIT.kt new file mode 100644 index 0000000..2793631 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteDefinitionIT.kt @@ -0,0 +1,35 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Definition") +class SQLiteDefinitionIT { + + @Test + @DisplayName("ensureTable creates table and index") + fun ensureTable() = + SQLiteDB().use(DefinitionFunctions::ensureTable) + + @Test + @DisplayName("ensureFieldIndex creates an index") + fun ensureFieldIndex() = + SQLiteDB().use(DefinitionFunctions::ensureFieldIndex) + + @Test + @DisplayName("ensureDocumentIndex fails for full index") + fun ensureDocumentIndexFull() { + assertThrows { SQLiteDB().use(DefinitionFunctions::ensureDocumentIndexFull) } + } + + @Test + @DisplayName("ensureDocumentIndex fails for optimized index") + fun ensureDocumentIndexOptimized() { + assertThrows { SQLiteDB().use(DefinitionFunctions::ensureDocumentIndexOptimized) } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteDeleteIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteDeleteIT.kt new file mode 100644 index 0000000..0480e4a --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteDeleteIT.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Delete") +class SQLiteDeleteIT { + + @Test + @DisplayName("byId deletes a matching ID") + fun byIdMatch() = + SQLiteDB().use(DeleteFunctions::byIdMatch) + + @Test + @DisplayName("byId succeeds when no ID matches") + fun byIdNoMatch() = + SQLiteDB().use(DeleteFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields deletes matching documents") + fun byFieldsMatch() = + SQLiteDB().use(DeleteFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(DeleteFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(DeleteFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(DeleteFunctions::byJsonPathMatch) } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteDocumentIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteDocumentIT.kt new file mode 100644 index 0000000..c220dc1 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteDocumentIT.kt @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test + +/** + * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Document") +class SQLiteDocumentIT { + + @Test + @DisplayName("insert works with default values") + fun insertDefault() = + SQLiteDB().use(DocumentFunctions::insertDefault) + + @Test + @DisplayName("insert fails with duplicate key") + fun insertDupe() = + SQLiteDB().use(DocumentFunctions::insertDupe) + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + fun insertNumAutoId() = + SQLiteDB().use(DocumentFunctions::insertNumAutoId) + + @Test + @DisplayName("insert succeeds with UUID auto ID") + fun insertUUIDAutoId() = + SQLiteDB().use(DocumentFunctions::insertUUIDAutoId) + + @Test + @DisplayName("insert succeeds with random string auto ID") + fun insertStringAutoId() = + SQLiteDB().use(DocumentFunctions::insertStringAutoId) + + @Test + @DisplayName("save updates an existing document") + fun saveMatch() = + SQLiteDB().use(DocumentFunctions::saveMatch) + + @Test + @DisplayName("save inserts a new document") + fun saveNoMatch() = + SQLiteDB().use(DocumentFunctions::saveNoMatch) + + @Test + @DisplayName("update replaces an existing document") + fun updateMatch() = + SQLiteDB().use(DocumentFunctions::updateMatch) + + @Test + @DisplayName("update succeeds when no document exists") + fun updateNoMatch() = + SQLiteDB().use(DocumentFunctions::updateNoMatch) +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteExistsIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteExistsIT.kt new file mode 100644 index 0000000..843ee2a --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteExistsIT.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Exists") +class SQLiteExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + fun byIdMatch() = + SQLiteDB().use(ExistsFunctions::byIdMatch) + + @Test + @DisplayName("byId returns false when no document matches the ID") + fun byIdNoMatch() = + SQLiteDB().use(ExistsFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields returns true when documents match") + fun byFieldsMatch() = + SQLiteDB().use(ExistsFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields returns false when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(ExistsFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(ExistsFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(ExistsFunctions::byJsonPathMatch) } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteFindIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteFindIT.kt new file mode 100644 index 0000000..9350620 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteFindIT.kt @@ -0,0 +1,127 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Find") +class SQLiteFindIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + SQLiteDB().use(FindFunctions::allDefault) + + @Test + @DisplayName("all sorts data ascending") + fun allAscending() = + SQLiteDB().use(FindFunctions::allAscending) + + @Test + @DisplayName("all sorts data descending") + fun allDescending() = + SQLiteDB().use(FindFunctions::allDescending) + + @Test + @DisplayName("all sorts data numerically") + fun allNumOrder() = + SQLiteDB().use(FindFunctions::allNumOrder) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + SQLiteDB().use(FindFunctions::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + SQLiteDB().use(FindFunctions::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + SQLiteDB().use(FindFunctions::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + SQLiteDB().use(FindFunctions::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + SQLiteDB().use(FindFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + SQLiteDB().use(FindFunctions::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + SQLiteDB().use(FindFunctions::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(FindFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + SQLiteDB().use(FindFunctions::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + SQLiteDB().use(FindFunctions::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(FindFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(FindFunctions::byJsonPathMatch) } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + SQLiteDB().use(FindFunctions::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + SQLiteDB().use(FindFunctions::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + SQLiteDB().use(FindFunctions::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + SQLiteDB().use(FindFunctions::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains fails") + fun firstByContainsFails() { + assertThrows { SQLiteDB().use(FindFunctions::firstByContainsMatchOne) } + } + + @Test + @DisplayName("firstByJsonPath fails") + fun firstByJsonPathFails() { + assertThrows { SQLiteDB().use(FindFunctions::firstByJsonPathMatchOne) } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt new file mode 100644 index 0000000..b6e8b04 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt @@ -0,0 +1,211 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Json") +class SQLiteJsonIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + SQLiteDB().use(JsonFunctions::allDefault) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + SQLiteDB().use(JsonFunctions::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + SQLiteDB().use(JsonFunctions::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + SQLiteDB().use(JsonFunctions::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + SQLiteDB().use(JsonFunctions::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + SQLiteDB().use(JsonFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + SQLiteDB().use(JsonFunctions::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + SQLiteDB().use(JsonFunctions::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(JsonFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + SQLiteDB().use(JsonFunctions::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + SQLiteDB().use(JsonFunctions::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(JsonFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(JsonFunctions::byJsonPathMatch) } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + SQLiteDB().use(JsonFunctions::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + SQLiteDB().use(JsonFunctions::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + SQLiteDB().use(JsonFunctions::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + SQLiteDB().use(JsonFunctions::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains fails") + fun firstByContainsFails() { + assertThrows { SQLiteDB().use(JsonFunctions::firstByContainsMatchOne) } + } + + @Test + @DisplayName("firstByJsonPath fails") + fun firstByJsonPathFails() { + assertThrows { SQLiteDB().use(JsonFunctions::firstByJsonPathMatchOne) } + } + + @Test + @DisplayName("writeAll retrieves all documents") + fun writeAllDefault() = + SQLiteDB().use(JsonFunctions::writeAllDefault) + + @Test + @DisplayName("writeAll succeeds with an empty table") + fun writeAllEmpty() = + SQLiteDB().use(JsonFunctions::writeAllEmpty) + + @Test + @DisplayName("writeById retrieves a document via a string ID") + fun writeByIdString() = + SQLiteDB().use(JsonFunctions::writeByIdString) + + @Test + @DisplayName("writeById retrieves a document via a numeric ID") + fun writeByIdNumber() = + SQLiteDB().use(JsonFunctions::writeByIdNumber) + + @Test + @DisplayName("writeById returns null when a matching ID is not found") + fun writeByIdNotFound() = + SQLiteDB().use(JsonFunctions::writeByIdNotFound) + + @Test + @DisplayName("writeByFields retrieves matching documents") + fun writeByFieldsMatch() = + SQLiteDB().use(JsonFunctions::writeByFieldsMatch) + + @Test + @DisplayName("writeByFields retrieves ordered matching documents") + fun writeByFieldsMatchOrdered() = + SQLiteDB().use(JsonFunctions::writeByFieldsMatchOrdered) + + @Test + @DisplayName("writeByFields retrieves matching documents with a numeric IN clause") + fun writeByFieldsMatchNumIn() = + SQLiteDB().use(JsonFunctions::writeByFieldsMatchNumIn) + + @Test + @DisplayName("writeByFields succeeds when no documents match") + fun writeByFieldsNoMatch() = + SQLiteDB().use(JsonFunctions::writeByFieldsNoMatch) + + @Test + @DisplayName("writeByFields retrieves matching documents with an IN_ARRAY comparison") + fun writeByFieldsMatchInArray() = + SQLiteDB().use(JsonFunctions::writeByFieldsMatchInArray) + + @Test + @DisplayName("writeByFields succeeds when no documents match an IN_ARRAY comparison") + fun writeByFieldsNoMatchInArray() = + SQLiteDB().use(JsonFunctions::writeByFieldsNoMatchInArray) + + @Test + @DisplayName("writeByContains fails") + fun writeByContainsFails() { + assertThrows { SQLiteDB().use(JsonFunctions::writeByContainsMatch) } + } + + @Test + @DisplayName("writeByJsonPath fails") + fun writeByJsonPathFails() { + assertThrows { SQLiteDB().use(JsonFunctions::writeByJsonPathMatch) } + } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document") + fun writeFirstByFieldsMatchOne() = + SQLiteDB().use(JsonFunctions::writeFirstByFieldsMatchOne) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many") + fun writeFirstByFieldsMatchMany() = + SQLiteDB().use(JsonFunctions::writeFirstByFieldsMatchMany) + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many (ordered)") + fun writeFirstByFieldsMatchOrdered() = + SQLiteDB().use(JsonFunctions::writeFirstByFieldsMatchOrdered) + + @Test + @DisplayName("writeFirstByFields returns null when no document matches") + fun writeFirstByFieldsNoMatch() = + SQLiteDB().use(JsonFunctions::writeFirstByFieldsNoMatch) + + @Test + @DisplayName("writeFirstByContains fails") + fun writeFirstByContainsFails() { + assertThrows { SQLiteDB().use(JsonFunctions::writeFirstByContainsMatchOne) } + } + + @Test + @DisplayName("writeFirstByJsonPath fails") + fun writeFirstByJsonPathFails() { + assertThrows { SQLiteDB().use(JsonFunctions::writeFirstByJsonPathMatchOne) } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/SQLitePatchIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLitePatchIT.kt new file mode 100644 index 0000000..e119839 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLitePatchIT.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: Patch") +class SQLitePatchIT { + + @Test + @DisplayName("byId patches an existing document") + fun byIdMatch() = + SQLiteDB().use(PatchFunctions::byIdMatch) + + @Test + @DisplayName("byId succeeds for a non-existent document") + fun byIdNoMatch() = + SQLiteDB().use(PatchFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields patches matching document") + fun byFieldsMatch() = + SQLiteDB().use(PatchFunctions::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(PatchFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(PatchFunctions::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(PatchFunctions::byJsonPathMatch) } + } +} \ No newline at end of file diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt new file mode 100644 index 0000000..3bea0b4 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt @@ -0,0 +1,55 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import kotlin.test.Test + +/** + * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("KotlinX | SQLite: RemoveFields") +class SQLiteRemoveFieldsIT { + + @Test + @DisplayName("byId removes fields from an existing document") + fun byIdMatchFields() = + SQLiteDB().use(RemoveFieldsFunctions::byIdMatchFields) + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + fun byIdMatchNoFields() = + SQLiteDB().use(RemoveFieldsFunctions::byIdMatchNoFields) + + @Test + @DisplayName("byId succeeds when no document exists") + fun byIdNoMatch() = + SQLiteDB().use(RemoveFieldsFunctions::byIdNoMatch) + + @Test + @DisplayName("byFields removes fields from matching documents") + fun byFieldsMatchFields() = + SQLiteDB().use(RemoveFieldsFunctions::byFieldsMatchFields) + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + fun byFieldsMatchNoFields() = + SQLiteDB().use(RemoveFieldsFunctions::byFieldsMatchNoFields) + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + fun byFieldsNoMatch() = + SQLiteDB().use(RemoveFieldsFunctions::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(RemoveFieldsFunctions::byContainsMatchFields) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(RemoveFieldsFunctions::byJsonPathMatchFields) } + } +} diff --git a/src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt b/src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt new file mode 100644 index 0000000..437e7c6 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt @@ -0,0 +1,24 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.AutoId +import java.sql.Connection + +/** + * Common interface for PostgreSQL and SQLite throwaway databases + */ +abstract class ThrowawayDatabase : AutoCloseable { + + /** The name of the throwaway database */ + protected val dbName = "throwaway_${AutoId.generateRandomString(8)}" + + /** The database connection for the throwaway database */ + abstract val conn: Connection + + /** + * Determine if a database object exists + * + * @param name The name of the object whose existence should be checked + * @return True if the object exists, false if not + */ + abstract fun dbObjectExists(name: String): Boolean +} diff --git a/src/scala/pom.xml b/src/scala/pom.xml new file mode 100644 index 0000000..09ac775 --- /dev/null +++ b/src/scala/pom.xml @@ -0,0 +1,167 @@ + + + 4.0.0 + + solutions.bitbadger + documents + 1.0.0-RC1 + ../../pom.xml + + + solutions.bitbadger.documents + scala + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (Scala Library) + https://relationaldocs.bitbadger.solutions/jvm/ + + + + MIT License + https://www.opensource.org/licenses/mit-license.php + + + + + + Daniel J. Summers + daniel@bitbadger.solutions + Bit Badger Solutions + https://bitbadger.solutions + + + + + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git + https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents + + + + ${project.basedir}/src/main/scala + ${project.basedir}/src/test/scala + + + org.apache.maven.plugins + maven-source-plugin + ${sourcePlugin.version} + + + attach-sources + + jar-no-fork + + + + + + net.alchim31.maven + scala-maven-plugin + 4.9.2 + + + + compile + testCompile + + + + attach-javadocs + + doc-jar + + + -nobootcp + dotty.tools.scaladoc.Main + ${project.build.outputDirectory} + + **/*.tasty + + + + org.scala-lang + scaladoc_3 + ${scala.version} + + + + + + + ${java.version} + ${java.version} + + + + maven-surefire-plugin + ${surefire.version} + + + maven-failsafe-plugin + ${failsafe.version} + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + Deployment-scala-${project.version} + central + + + + + + + + solutions.bitbadger.documents + core + ${project.version} + + + org.scala-lang + scala3-library_3 + ${scala.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + + + + \ No newline at end of file diff --git a/src/scala/scala.iml b/src/scala/scala.iml new file mode 100644 index 0000000..6ad0dbf --- /dev/null +++ b/src/scala/scala.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/scala/src/main/scala/Count.scala b/src/scala/src/main/scala/Count.scala new file mode 100644 index 0000000..df5a319 --- /dev/null +++ b/src/scala/src/main/scala/Count.scala @@ -0,0 +1,105 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Field, FieldMatch} +import solutions.bitbadger.documents.java.Count as CoreCount + +import java.sql.Connection + +import _root_.scala.jdk.CollectionConverters.* + +/** + * Functions to count documents + */ +object Count: + + /** + * Count all documents in the table + * + * @param tableName The name of the table in which documents should be counted + * @param conn The connection over which documents should be counted + * @return A count of the documents in the table + * @throws DocumentException If any dependent process does + */ + def all(tableName: String, conn: Connection): Long = + CoreCount.all(tableName, conn) + + /** + * Count all documents in the table (creates connection) + * + * @param tableName The name of the table in which documents should be counted + * @return A count of the documents in the table + * @throws DocumentException If no connection string has been set + */ + def all(tableName: String): Long = + CoreCount.all(tableName) + + /** + * Count documents using a field comparison + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, default `ALL`) + * @param conn The connection on which the deletion should be executed + * @return A count of the matching documents in the table + * @throws DocumentException If no dialect has been configured + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection): Long = + CoreCount.byFields(tableName, fields.asJava, howMatched.orNull, conn) + + /** + * Count documents using a field comparison (creates connection) + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return A count of the matching documents in the table + * @throws DocumentException If no connection string has been set + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None): Long = + CoreCount.byFields(tableName, fields.asJava, howMatched.orNull) + + /** + * Count documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the count should be executed + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A, conn: Connection): Long = + CoreCount.byContains(tableName, criteria, conn) + + /** + * Count documents using a JSON containment query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A): Long = + CoreCount.byContains(tableName, criteria) + + /** + * Count documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @param conn The connection on which the count should be executed + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String, conn: Connection): Long = + CoreCount.byJsonPath(tableName, path, conn) + + /** + * Count documents using a JSON Path match query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String): Long = + CoreCount.byJsonPath(tableName, path) diff --git a/src/scala/src/main/scala/Custom.scala b/src/scala/src/main/scala/Custom.scala new file mode 100644 index 0000000..63704b4 --- /dev/null +++ b/src/scala/src/main/scala/Custom.scala @@ -0,0 +1,360 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Configuration, Parameter} + +import java.io.PrintWriter +import java.sql.{Connection, ResultSet} +import scala.reflect.ClassTag +import scala.util.Using + +object Custom: + + /** + * Execute a query that returns a list of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If parameters are invalid + */ + def list[Doc](query: String, parameters: Seq[Parameter[?]], conn: Connection, + mapFunc: (ResultSet, ClassTag[Doc]) => Doc)(using tag: ClassTag[Doc]): List[Doc] = + Using(Parameters.apply(conn, query, parameters)) { stmt => Results.toCustomList[Doc](stmt, mapFunc) }.get + + /** + * Execute a query that returns a list of results + * + * @param query The query to retrieve the results + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If parameters are invalid + */ + def list[Doc](query: String, conn: Connection, mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): List[Doc] = + list(query, Nil, conn, mapFunc) + + /** + * Execute a query that returns a list of results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If parameters are invalid + */ + def list[Doc](query: String, parameters: Seq[Parameter[?]], mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): List[Doc] = + Using(Configuration.dbConn()) { conn => list[Doc](query, parameters, conn, mapFunc) }.get + + /** + * Execute a query that returns a list of results (creates connection) + * + * @param query The query to retrieve the results + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If parameters are invalid + */ + def list[Doc](query: String, mapFunc: (ResultSet, ClassTag[Doc]) => Doc)(using tag: ClassTag[Doc]): List[Doc] = + list(query, List(), mapFunc) + + /** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def jsonArray(query: String, parameters: Seq[Parameter[?]], conn: Connection, mapFunc: ResultSet => String): String = + Using(Parameters.apply(conn, query, parameters)) { stmt => Results.toJsonArray(stmt, mapFunc) }.get + + /** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def jsonArray(query: String, conn: Connection, mapFunc: ResultSet => String): String = + jsonArray(query, Nil, conn, mapFunc) + + /** + * Execute a query that returns a JSON array of results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def jsonArray(query: String, parameters: Seq[Parameter[?]], mapFunc: ResultSet => String): String = + Using(Configuration.dbConn()) { conn => jsonArray(query, parameters, conn, mapFunc) }.get + + /** + * Execute a query that returns a JSON array of results (creates connection) + * + * @param query The query to retrieve the results + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def jsonArray(query: String, mapFunc: ResultSet => String): String = + jsonArray(query, Nil, mapFunc) + + /** + * Execute a query that writes a JSON array of results to the given `PrintWriter` + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def writeJsonArray(query: String, parameters: Seq[Parameter[?]], writer: PrintWriter, conn: Connection, + mapFunc: ResultSet => String): Unit = + Using(Parameters.apply(conn, query, parameters)) { stmt => Results.writeJsonArray(writer, stmt, mapFunc) } + + /** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param writer The writer to which the results should be written + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def writeJsonArray(query: String, conn: Connection, writer: PrintWriter, mapFunc: ResultSet => String): Unit = + writeJsonArray(query, Nil, writer, conn, mapFunc) + + /** + * Execute a query that returns a JSON array of results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def writeJsonArray(query: String, parameters: Seq[Parameter[?]], writer: PrintWriter, + mapFunc: ResultSet => String): Unit = + Using(Configuration.dbConn()) { conn => writeJsonArray(query, parameters, writer, conn, mapFunc) } + + /** + * Execute a query that returns a JSON array of results (creates connection) + * + * @param query The query to retrieve the results + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def writeJsonArray(query: String, writer: PrintWriter, mapFunc: ResultSet => String): Unit = + writeJsonArray(query, Nil, writer, mapFunc) + + /** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return An `Option` value, with the document if one matches the query + * @throws DocumentException If parameters are invalid + */ + def single[Doc](query: String, parameters: Seq[Parameter[?]], conn: Connection, + mapFunc: (ResultSet, ClassTag[Doc]) => Doc)(using tag: ClassTag[Doc]): Option[Doc] = + list[Doc](s"$query LIMIT 1", parameters, conn, mapFunc).headOption + + /** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return An `Option` value, with the document if one matches the query + * @throws DocumentException If parameters are invalid + */ + def single[Doc](query: String, conn: Connection, mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): Option[Doc] = + list[Doc](s"$query LIMIT 1", List(), conn, mapFunc).headOption + + /** + * Execute a query that returns one or no results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return An `Option` value, with the document if one matches the query + * @throws DocumentException If parameters are invalid + */ + def single[Doc](query: String, parameters: Seq[Parameter[?]], mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): Option[Doc] = + Using(Configuration.dbConn()) { conn => single[Doc](query, parameters, conn, mapFunc) }.get + + /** + * Execute a query that returns one or no results (creates connection) + * + * @param query The query to retrieve the results + * @param mapFunc The mapping function between the document and the domain item + * @return An `Option` value, with the document if one matches the query + * @throws DocumentException If parameters are invalid + */ + def single[Doc](query: String, mapFunc: (ResultSet, ClassTag[Doc]) => Doc)(using tag: ClassTag[Doc]): Option[Doc] = + single[Doc](query, List(), mapFunc) + + /** + * Execute a query that returns JSON for one or no documents + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + def jsonSingle(query: String, parameters: Seq[Parameter[?]], conn: Connection, mapFunc: ResultSet => String): String = + val result = jsonArray("$query LIMIT 1", parameters, conn, mapFunc) + result match + case "[]" => "{}" + case _ => result.substring(1, result.length - 1) + + /** + * Execute a query that returns JSON for one or no documents + * + * @param query The query to retrieve the results + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + def jsonSingle(query: String, conn: Connection, mapFunc: ResultSet => String): String = + jsonSingle(query, Nil, conn, mapFunc) + + /** + * Execute a query that returns JSON for one or no documents (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + def jsonSingle(query: String, parameters: Seq[Parameter[?]], mapFunc: ResultSet => String): String = + Using(Configuration.dbConn()) { conn => jsonSingle(query, parameters, conn, mapFunc) }.get + + /** + * Execute a query that returns JSON for one or no documents (creates connection) + * + * @param query The query to retrieve the results + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + def jsonSingle(query: String, mapFunc: ResultSet => String): String = + jsonSingle(query, Nil, mapFunc) + + /** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @throws DocumentException If parameters are invalid + */ + def nonQuery(query: String, parameters: Seq[Parameter[?]], conn: Connection): Unit = + Using(Parameters.apply(conn, query, parameters)) { stmt => stmt.executeUpdate() } + + /** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param conn The connection over which the query should be executed + * @throws DocumentException If parameters are invalid + */ + def nonQuery(query: String, conn: Connection): Unit = + nonQuery(query, List(), conn) + + /** + * Execute a query that returns no results (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @throws DocumentException If parameters are invalid + */ + def nonQuery(query: String, parameters: Seq[Parameter[?]]): Unit = + Using(Configuration.dbConn()) { conn => nonQuery(query, parameters, conn) } + + /** + * Execute a query that returns no results (creates connection) + * + * @param query The query to retrieve the results + * @throws DocumentException If parameters are invalid + */ + def nonQuery(query: String): Unit = + nonQuery(query, List()) + + /** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If parameters are invalid + */ + def scalar[A](query: String, parameters: Seq[Parameter[?]], conn: Connection, mapFunc: (ResultSet, ClassTag[A]) => A) + (using tag: ClassTag[A]): A = + Using(Parameters.apply(conn, query, parameters)) { stmt => + Using(stmt.executeQuery()) { rs => + rs.next() + mapFunc(rs, tag) + }.get + }.get + + /** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param conn The connection over which the query should be executed + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If parameters are invalid + */ + def scalar[A](query: String, conn: Connection, mapFunc: (ResultSet, ClassTag[A]) => A)(using tag: ClassTag[A]): A = + scalar[A](query, List(), conn, mapFunc) + + /** + * Execute a query that returns a scalar result (creates connection) + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If parameters are invalid + */ + def scalar[A](query: String, parameters: Seq[Parameter[?]], mapFunc: (ResultSet, ClassTag[A]) => A) + (using tag: ClassTag[A]): A = + Using(Configuration.dbConn()) { conn => scalar[A](query, parameters, conn, mapFunc) }.get + + /** + * Execute a query that returns a scalar result (creates connection) + * + * @param query The query to retrieve the result + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If parameters are invalid + */ + def scalar[A](query: String, mapFunc: (ResultSet, ClassTag[A]) => A)(using tag: ClassTag[A]): A = + scalar[A](query, List(), mapFunc) diff --git a/src/scala/src/main/scala/Definition.scala b/src/scala/src/main/scala/Definition.scala new file mode 100644 index 0000000..4e0c2cc --- /dev/null +++ b/src/scala/src/main/scala/Definition.scala @@ -0,0 +1,72 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.java.Definition as CoreDefinition + +import java.sql.Connection +import _root_.scala.jdk.CollectionConverters.* + +object Definition: + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + * @param conn The connection on which the query should be executed + * @throws DocumentException If the dialect is not configured + */ + def ensureTable(tableName: String, conn: Connection): Unit = + CoreDefinition.ensureTable(tableName, conn) + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + * @throws DocumentException If no connection string has been set + */ + def ensureTable(tableName: String): Unit = + CoreDefinition.ensureTable(tableName) + + /** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed + * @param conn The connection on which the query should be executed + * @throws DocumentException If any dependent process does + */ + def ensureFieldIndex(tableName: String, indexName: String, fields: Seq[String], conn: Connection): Unit = + CoreDefinition.ensureFieldIndex(tableName, indexName, fields.asJava, conn) + + /** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed + * @throws DocumentException If no connection string has been set, or if any dependent process does + */ + def ensureFieldIndex(tableName: String, indexName: String, fields: Seq[String]): Unit = + CoreDefinition.ensureFieldIndex(tableName, indexName, fields.asJava) + + /** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @param conn The connection on which the query should be executed + * @throws DocumentException If called on a SQLite connection + */ + def ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection): Unit = + CoreDefinition.ensureDocumentIndex(tableName, indexType, conn) + + /** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def ensureDocumentIndex(tableName: String, indexType: DocumentIndex): Unit = + CoreDefinition.ensureDocumentIndex(tableName, indexType) diff --git a/src/scala/src/main/scala/Delete.scala b/src/scala/src/main/scala/Delete.scala new file mode 100644 index 0000000..bd67103 --- /dev/null +++ b/src/scala/src/main/scala/Delete.scala @@ -0,0 +1,98 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Field, FieldMatch} +import solutions.bitbadger.documents.java.Delete as CoreDelete + +import java.sql.Connection +import _root_.scala.jdk.CollectionConverters.* + +/** + * Functions to delete documents + */ +object Delete: + + /** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If no dialect has been configured + */ + def byId[Key](tableName: String, docId: Key, conn: Connection): Unit = + CoreDelete.byId(tableName, docId, conn) + + /** + * Delete a document by its ID (creates connection) + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + * @throws DocumentException If no connection string has been set + */ + def byId[Key](tableName: String, docId: Key): Unit = + CoreDelete.byId(tableName, docId) + + /** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection): Unit = + CoreDelete.byFields(tableName, fields.asJava, howMatched.orNull, conn) + + /** + * Delete documents using a field comparison (creates connection) + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None): Unit = + CoreDelete.byFields(tableName, fields.asJava, howMatched.orNull) + + /** + * Delete documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A, conn: Connection): Unit = + CoreDelete.byContains(tableName, criteria, conn) + + /** + * Delete documents using a JSON containment query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A): Unit = + CoreDelete.byContains(tableName, criteria) + + /** + * Delete documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @param conn The connection on which the deletion should be executed + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String, conn: Connection): Unit = + CoreDelete.byJsonPath(tableName, path, conn) + + /** + * Delete documents using a JSON Path match query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String): Unit = + CoreDelete.byJsonPath(tableName, path) diff --git a/src/scala/src/main/scala/Document.scala b/src/scala/src/main/scala/Document.scala new file mode 100644 index 0000000..cab75fe --- /dev/null +++ b/src/scala/src/main/scala/Document.scala @@ -0,0 +1,72 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.java.Document as CoreDocument + +import java.sql.Connection + +object Document: + + /** + * Insert a new document + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + * @param conn The connection on which the query should be executed + * @throws DocumentException If IDs are misconfigured, or if the database command fails + */ + def insert[Doc](tableName: String, document: Doc, conn: Connection): Unit = + CoreDocument.insert(tableName, document, conn) + + /** + * Insert a new document (creates connection) + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + * @throws DocumentException If IDs are misconfigured, or if the database command fails + */ + def insert[Doc](tableName: String, document: Doc): Unit = + CoreDocument.insert(tableName, document) + + /** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + * @param conn The connection on which the query should be executed + * @throws DocumentException If the database command fails + */ + def save[Doc](tableName: String, document: Doc, conn: Connection): Unit = + CoreDocument.save(tableName, document, conn) + + /** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert"; creates connection) + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + * @throws DocumentException If the database command fails + */ + def save[Doc](tableName: String, document: Doc): Unit = + CoreDocument.save(tableName, document) + + /** + * Update (replace) a document by its ID + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + * @param conn The connection on which the query should be executed + * @throws DocumentException If no dialect has been configured, or if the database command fails + */ + def update[Key, Doc](tableName: String, docId: Key, document: Doc, conn: Connection): Unit = + CoreDocument.update(tableName, docId, document, conn) + + /** + * Update (replace) a document by its ID (creates connection) + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + * @throws DocumentException If no dialect has been configured, or if the database command fails + */ + def update[Key, Doc](tableName: String, docId: Key, document: Doc): Unit = + CoreDocument.update(tableName, docId, document) diff --git a/src/scala/src/main/scala/Exists.scala b/src/scala/src/main/scala/Exists.scala new file mode 100644 index 0000000..ef1c4fb --- /dev/null +++ b/src/scala/src/main/scala/Exists.scala @@ -0,0 +1,106 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Field, FieldMatch} +import solutions.bitbadger.documents.java.Exists as CoreExists + +import java.sql.Connection +import _root_.scala.jdk.CollectionConverters.* + +/** + * Functions to determine whether documents exist + */ +object Exists: + + /** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @param conn The connection on which the existence check should be executed + * @return True if the document exists, false if not + * @throws DocumentException If no dialect has been configured + */ + def byId[Key](tableName: String, docId: Key, conn: Connection): Boolean = + CoreExists.byId(tableName, docId, conn) + + /** + * Determine a document's existence by its ID (creates connection) + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @return True if the document exists, false if not + * @throws DocumentException If no connection string has been set + */ + def byId[Key](tableName: String, docId: Key): Boolean = + CoreExists.byId(tableName, docId) + + /** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection): Boolean = + CoreExists.byFields(tableName, fields.asJava, howMatched.orNull, conn) + + /** + * Determine document existence using a field comparison (creates connection) + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return True if any matching documents exist, false if not + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None): Boolean = + CoreExists.byFields(tableName, fields.asJava, howMatched.orNull) + + /** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A, conn: Connection): Boolean = + CoreExists.byContains(tableName, criteria, conn) + + /** + * Determine document existence using a JSON containment query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @return True if any matching documents exist, false if not + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A): Boolean = + CoreExists.byContains(tableName, criteria) + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @param conn The connection on which the existence check should be executed + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String, conn: Connection): Boolean = + CoreExists.byJsonPath(tableName, path, conn) + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @return True if any matching documents exist, false if not + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String): Boolean = + CoreExists.byJsonPath(tableName, path) diff --git a/src/scala/src/main/scala/Find.scala b/src/scala/src/main/scala/Find.scala new file mode 100644 index 0000000..8f6a2fc --- /dev/null +++ b/src/scala/src/main/scala/Find.scala @@ -0,0 +1,369 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Configuration, Field, FieldMatch, Parameter, ParameterType} +import solutions.bitbadger.documents.query.{FindQuery, QueryUtils} + +import java.sql.Connection +import scala.reflect.ClassTag +import scala.jdk.CollectionConverters.* +import scala.util.Using + +/** + * Functions to find and retrieve documents + */ +object Find: + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents from the given table + * @throws DocumentException If query execution fails + */ + def all[Doc](tableName: String, orderBy: Seq[Field[?]], conn: Connection)(using tag: ClassTag[Doc]): List[Doc] = + Custom.list[Doc](FindQuery.all(tableName) + QueryUtils.orderBy(orderBy.asJava), conn, Results.fromData) + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param conn The connection over which documents should be retrieved + * @return A list of documents from the given table + * @throws DocumentException If query execution fails + */ + def all[Doc](tableName: String, conn: Connection)(using tag: ClassTag[Doc]): List[Doc] = + all[Doc](tableName, List(), conn) + + /** + * Retrieve all documents in the given table (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents from the given table + * @throws DocumentException If no connection string has been set, or if query execution fails + */ + def all[Doc](tableName: String, orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): List[Doc] = + Using(Configuration.dbConn()) { conn => all[Doc](tableName, orderBy, conn) }.get + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @param conn The connection over which documents should be retrieved + * @return An `Option` with the document if it is found + * @throws DocumentException If no dialect has been configured + */ + def byId[Key, Doc](tableName: String, docId: Key, conn: Connection)(using tag: ClassTag[Doc]): Option[Doc] = + Custom.single[Doc](FindQuery.byId(tableName, docId), + Parameters.addFields(Field.equal(Configuration.idField, docId, ":id") :: Nil).toSeq, conn, Results.fromData) + + /** + * Retrieve a document by its ID (creates connection) + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return An `Option` with the document if it is found + * @throws DocumentException If no connection string has been set + */ + def byId[Key, Doc](tableName: String, docId: Key)(using tag: ClassTag[Doc]): Option[Doc] = + Using(Configuration.dbConn()) { conn => byId[Key, Doc](tableName, docId, conn) }.get + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], orderBy: Seq[Field[?]], + conn: Connection)(using tag: ClassTag[Doc]): List[Doc] = + val named = Parameters.nameFields(fields) + Custom.list[Doc]( + FindQuery.byFields(tableName, named.asJava, howMatched.orNull) + QueryUtils.orderBy(orderBy.asJava), + Parameters.addFields(named).toSeq, conn, Results.fromData) + + /** + * Retrieve documents using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection) + (using tag: ClassTag[Doc]): List[Doc] = + byFields[Doc](tableName, fields, howMatched, List(), conn) + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields[Doc](tableName: String, fields: Seq[Field[?]], orderBy: Seq[Field[?]], conn: Connection) + (using tag: ClassTag[Doc]): List[Doc] = + byFields[Doc](tableName, fields, None, orderBy, conn) + + /** + * Retrieve documents using a field comparison, ordering results by the given fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the field comparison + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def byFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): List[Doc] = + Using(Configuration.dbConn()) { conn => byFields[Doc](tableName, fields, howMatched, orderBy, conn) }.get + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + def byContains[Doc, A](tableName: String, criteria: A, orderBy: Seq[Field[?]], conn: Connection) + (using tag: ClassTag[Doc]): List[Doc] = + Custom.list[Doc](FindQuery.byContains(tableName) + QueryUtils.orderBy(orderBy.asJava), + Parameters.json(":criteria", criteria) :: Nil, conn, Results.fromData) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + def byContains[Doc, A](tableName: String, criteria: A, conn: Connection)(using tag: ClassTag[Doc]): List[Doc] = + byContains[Doc, A](tableName, criteria, List(), conn) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON containment query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byContains[Doc, A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = Nil) + (using tag: ClassTag[Doc]): List[Doc] = + Using(Configuration.dbConn()) { conn => byContains[Doc, A](tableName, criteria, orderBy, conn) }.get + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]], conn: Connection) + (using tag: ClassTag[Doc]): List[Doc] = + Custom.list[Doc](FindQuery.byJsonPath(tableName) + QueryUtils.orderBy(orderBy.asJava), + Parameter(":path", ParameterType.STRING, path) :: Nil, conn, Results.fromData) + + /** + * Retrieve documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath[Doc](tableName: String, path: String, conn: Connection)(using tag: ClassTag[Doc]): List[Doc] = + byJsonPath[Doc](tableName, path, List(), conn) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]] = Nil) + (using tag: ClassTag[Doc]): List[Doc] = + Using(Configuration.dbConn()) { conn => byJsonPath[Doc](tableName, path, orderBy, conn) }.get + + /** + * Retrieve the first document using a field comparison and ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return An `Option` with the first document matching the field comparison if found + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def firstByFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], + orderBy: Seq[Field[?]], conn: Connection)(using tag: ClassTag[Doc]): Option[Doc] = + val named = Parameters.nameFields(fields) + Custom.single[Doc]( + FindQuery.byFields(tableName, named.asJava, howMatched.orNull) + QueryUtils.orderBy(orderBy.asJava), + Parameters.addFields(named).toSeq, conn, Results.fromData) + + /** + * Retrieve the first document using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @return An `Option` with the first document matching the field comparison if found + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def firstByFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection) + (using tag: ClassTag[Doc]): Option[Doc] = + firstByFields[Doc](tableName, fields, howMatched, List(), conn) + + /** + * Retrieve the first document using a field comparison and ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return An `Option` with the first document matching the field comparison if found + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def firstByFields[Doc](tableName: String, fields: Seq[Field[?]], orderBy: Seq[Field[?]], conn: Connection) + (using tag: ClassTag[Doc]): Option[Doc] = + firstByFields[Doc](tableName, fields, None, orderBy, conn) + + /** + * Retrieve the first document using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param conn The connection over which documents should be retrieved + * @return An `Option` with the first document matching the field comparison if found + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def firstByFields[Doc](tableName: String, fields: Seq[Field[?]], conn: Connection) + (using tag: ClassTag[Doc]): Option[Doc] = + firstByFields[Doc](tableName, fields, None, List(), conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Option` with the first document matching the field comparison if found + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def firstByFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): Option[Doc] = + Using(Configuration.dbConn()) { conn => firstByFields[Doc](tableName, fields, howMatched, orderBy, conn) }.get + + /** + * Retrieve the first document using a JSON containment query and ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return An `Option` with the first document matching the JSON containment query if found + * @throws DocumentException If called on a SQLite connection + */ + def firstByContains[Doc, A](tableName: String, criteria: A, orderBy: Seq[Field[?]], conn: Connection) + (using tag: ClassTag[Doc]): Option[Doc] = + Custom.single[Doc](FindQuery.byContains(tableName) + QueryUtils.orderBy(orderBy.asJava), + Parameters.json(":criteria", criteria) :: Nil, conn, Results.fromData) + + /** + * Retrieve the first document using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return An `Option` with the first document matching the JSON containment query if found + * @throws DocumentException If called on a SQLite connection + */ + def firstByContains[Doc, A](tableName: String, criteria: A, conn: Connection)(using tag: ClassTag[Doc]): Option[Doc] = + firstByContains[Doc, A](tableName, criteria, List(), conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Option` with the first document matching the JSON containment query if found + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def firstByContains[Doc, A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = Nil) + (using tag: ClassTag[Doc]): Option[Doc] = + Using(Configuration.dbConn()) { conn => firstByContains[Doc, A](tableName, criteria, orderBy, conn) }.get + + /** + * Retrieve the first document using a JSON Path match query and ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return An `Optional` item, with the first document matching the JSON Path match query if found + * @throws DocumentException If called on a SQLite connection + */ + def firstByJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]], conn: Connection) + (using tag: ClassTag[Doc]): Option[Doc] = + Custom.single[Doc](FindQuery.byJsonPath(tableName) + QueryUtils.orderBy(orderBy.asJava), + Parameter(":path", ParameterType.STRING, path) :: Nil, conn, Results.fromData) + + /** + * Retrieve the first document using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return An `Option` with the first document matching the JSON Path match query if found + * @throws DocumentException If called on a SQLite connection + */ + def firstByJsonPath[Doc](tableName: String, path: String, conn: Connection)(using tag: ClassTag[Doc]): Option[Doc] = + firstByJsonPath[Doc](tableName, path, List(), conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return An `Optional` item, with the first document matching the JSON Path match query if found + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def firstByJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]] = Nil) + (using tag: ClassTag[Doc]): Option[Doc] = + Using(Configuration.dbConn()) { conn => firstByJsonPath[Doc](tableName, path, orderBy, conn) }.get diff --git a/src/scala/src/main/scala/Json.scala b/src/scala/src/main/scala/Json.scala new file mode 100644 index 0000000..9c2c4e5 --- /dev/null +++ b/src/scala/src/main/scala/Json.scala @@ -0,0 +1,688 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Field, FieldMatch} +import solutions.bitbadger.documents.java.Json as CoreJson + +import java.io.PrintWriter +import java.sql.Connection +import _root_.scala.jdk.CollectionConverters.* + +object Json: + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents from the given table + * @throws DocumentException If query execution fails + */ + def all(tableName: String, orderBy: Seq[Field[?]], conn: Connection): String = + CoreJson.all(tableName, orderBy.asJava, conn) + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents from the given table + * @throws DocumentException If query execution fails + */ + def all(tableName: String, conn: Connection): String = + CoreJson.all(tableName, conn) + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents from the given table + * @throws DocumentException If no connection string has been set, or if query execution fails + */ + def all(tableName: String, orderBy: Seq[Field[?]] = Nil): String = + CoreJson.all(tableName, orderBy.asJava) + + /** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If query execution fails + */ + def writeAll(tableName: String, writer: PrintWriter, orderBy: Seq[Field[?]], conn: Connection): Unit = + CoreJson.writeAll(tableName, writer, orderBy.asJava, conn) + + /** + * Write all documents in the given table to the given `PrintWriter` + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If query execution fails + */ + def writeAll(tableName: String, writer: PrintWriter, conn: Connection): Unit = + CoreJson.writeAll(tableName, writer, conn) + + /** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If query execution fails + */ + def writeAll(tableName: String, writer: PrintWriter, orderBy: Seq[Field[?]]): Unit = + CoreJson.writeAll(tableName, writer, orderBy.asJava) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @param conn The connection over which documents should be retrieved + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no dialect has been configured + */ + def byId[Key](tableName: String, docId: Key, conn: Connection): String = + CoreJson.byId(tableName, docId, conn) + + /** + * Retrieve a document by its ID (creates connection) + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no connection string has been set + */ + def byId[Key](tableName: String, docId: Key): String = + CoreJson.byId(tableName, docId) + + /** + * Write a document to the given `PrintWriter` by its ID + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured + */ + def writeById[Key](tableName: String, writer: PrintWriter, docId: Key, conn: Connection): Unit = + CoreJson.writeById(tableName, writer, docId, conn) + + /** + * Write a document to the given `PrintWriter` by its ID (creates connection) + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @throws DocumentException If no dialect has been configured + */ + def writeById[Key](tableName: String, writer: PrintWriter, docId: Key): Unit = + CoreJson.writeById(tableName, writer, docId) + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], orderBy: Seq[Field[?]], + conn: Connection): String = + CoreJson.byFields(tableName, fields.asJava, howMatched.orNull, orderBy.asJava, conn) + + /** + * Retrieve documents using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection): String = + CoreJson.byFields(tableName, fields.asJava, howMatched.orNull, conn) + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], orderBy: Seq[Field[?]], conn: Connection): String = + CoreJson.byFields(tableName, fields.asJava, null, orderBy.asJava, conn) + + /** + * Retrieve documents using a field comparison, ordering results by the given fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = Nil): String = + CoreJson.byFields(tableName, fields.asJava, howMatched.orNull, orderBy.asJava) + + /** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], howMatched: Option[FieldMatch], + orderBy: Seq[Field[?]], conn: Connection): Unit = + CoreJson.writeByFields(tableName, writer, fields.asJava, howMatched.orNull, orderBy.asJava, conn) + + /** + * Write documents to the given `PrintWriter` using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], howMatched: Option[FieldMatch], + conn: Connection): Unit = + CoreJson.writeByFields(tableName, writer, fields.asJava, howMatched.orNull, conn) + + /** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], orderBy: Seq[Field[?]], + conn: Connection): Unit = + CoreJson.writeByFields(tableName, writer, fields.asJava, null, orderBy.asJava, conn) + + /** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields (creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], + howMatched: Option[FieldMatch] = None, orderBy: Seq[Field[?]] = Nil): Unit = + CoreJson.writeByFields(tableName, writer, fields.asJava, howMatched.orNull, orderBy.asJava) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A, orderBy: Seq[Field[?]], conn: Connection): String = + CoreJson.byContains(tableName, criteria, orderBy.asJava, conn) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A, conn: Connection): String = + CoreJson.byContains(tableName, criteria, conn) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = Nil): String = + CoreJson.byContains(tableName, criteria, orderBy.asJava) + + /** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + def writeByContains[A](tableName: String, writer: PrintWriter, criteria: A, orderBy: Seq[Field[?]], + conn: Connection): Unit = + CoreJson.writeByContains(tableName, writer, criteria, orderBy.asJava, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + def writeByContains[A](tableName: String, writer: PrintWriter, criteria: A, conn: Connection): Unit = + CoreJson.writeByContains(tableName, writer, criteria, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ + def writeByContains[A](tableName: String, writer: PrintWriter, criteria: A, orderBy: Seq[Field[?]] = Nil): Unit = + CoreJson.writeByContains(tableName, writer, criteria, orderBy.asJava) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String, orderBy: Seq[Field[?]], conn: Connection): String = + CoreJson.byJsonPath(tableName, path, orderBy.asJava, conn) + + /** + * Retrieve documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String, conn: Connection): String = + CoreJson.byJsonPath(tableName, path, conn) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String, orderBy: Seq[Field[?]] = Nil): String = + CoreJson.byJsonPath(tableName, path, orderBy.asJava) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + def writeByJsonPath(tableName: String, writer: PrintWriter, path: String, orderBy: Seq[Field[?]], + conn: Connection): Unit = + CoreJson.writeByJsonPath(tableName, writer, path, orderBy.asJava, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + def writeByJsonPath(tableName: String, writer: PrintWriter, path: String, conn: Connection): Unit = + CoreJson.writeByJsonPath(tableName, writer, path, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields + * (PostgreSQL only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ + def writeByJsonPath(tableName: String, writer: PrintWriter, path: String, orderBy: Seq[Field[?]] = Nil): Unit = + CoreJson.writeByJsonPath(tableName, writer, path, orderBy.asJava) + + /** + * Retrieve the first document using a field comparison and ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def firstByFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], orderBy: Seq[Field[?]], + conn: Connection): String = + CoreJson.firstByFields(tableName, fields.asJava, howMatched.orNull, orderBy.asJava, conn) + + /** + * Retrieve the first document using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def firstByFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], + conn: Connection): String = + CoreJson.firstByFields(tableName, fields.asJava, howMatched.orNull, conn) + + /** + * Retrieve the first document using a field comparison and ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def firstByFields(tableName: String, fields: Seq[Field[?]], orderBy: Seq[Field[?]], conn: Connection): String = + CoreJson.firstByFields(tableName, fields.asJava, null, orderBy.asJava, conn) + + /** + * Retrieve the first document using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def firstByFields(tableName: String, fields: Seq[Field[?]], conn: Connection): String = + CoreJson.firstByFields(tableName, fields.asJava, null, conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields (creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def firstByFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = Nil): String = + CoreJson.firstByFields(tableName, fields.asJava, howMatched.orNull, orderBy.asJava) + + /** + * Write the first document to the given `PrintWriter` using a field comparison and ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeFirstByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], howMatched: Option[FieldMatch], + orderBy: Seq[Field[?]], conn: Connection): Unit = + CoreJson.writeFirstByFields(tableName, writer, fields.asJava, howMatched.orNull, orderBy.asJava, conn) + + /** + * Write the first document to the given `PrintWriter` using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeFirstByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], howMatched: Option[FieldMatch], + conn: Connection): Unit = + CoreJson.writeFirstByFields(tableName, writer, fields.asJava, howMatched.orNull, conn) + + /** + * Write the first document to the given `PrintWriter` using a field comparison and ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeFirstByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], orderBy: Seq[Field[?]], + conn: Connection): Unit = + CoreJson.writeFirstByFields(tableName, writer, fields.asJava, null, orderBy.asJava, conn) + + /** + * Write the first document to the given `PrintWriter` using a field comparison + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeFirstByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], conn: Connection): Unit = + CoreJson.writeFirstByFields(tableName, writer, fields.asJava, null, conn) + + /** + * Write the first document to the given `PrintWriter` using a field comparison and ordering fields (creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def writeFirstByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], + howMatched: Option[FieldMatch] = None, orderBy: Seq[Field[?]] = Nil): Unit = + CoreJson.writeFirstByFields(tableName, writer, fields.asJava, howMatched.orNull, orderBy.asJava) + + /** + * Retrieve the first document using a JSON containment query and ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + def firstByContains[A](tableName: String, criteria: A, orderBy: Seq[Field[?]], conn: Connection): String = + CoreJson.firstByContains(tableName, criteria, orderBy.asJava, conn) + + /** + * Retrieve the first document using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + def firstByContains[A](tableName: String, criteria: A, conn: Connection): String = + CoreJson.firstByContains(tableName, criteria, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def firstByContains[A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = Nil): String = + CoreJson.firstByContains(tableName, criteria, orderBy.asJava) + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query and ordering fields (PostgreSQL + * only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + def writeFirstByContains[A](tableName: String, writer: PrintWriter, criteria: A, orderBy: Seq[Field[?]], + conn: Connection): Unit = + CoreJson.writeFirstByContains(tableName, writer, criteria, orderBy.asJava, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + def writeFirstByContains[A](tableName: String, writer: PrintWriter, criteria: A, conn: Connection): Unit = + CoreJson.writeFirstByContains(tableName, writer, criteria, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query and ordering fields (PostgreSQL + * only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ + def writeFirstByContains[A](tableName: String, writer: PrintWriter, criteria: A, orderBy: Seq[Field[?]] = Nil): Unit = + CoreJson.writeFirstByContains(tableName, writer, criteria, orderBy.asJava) + + /** + * Retrieve the first document using a JSON Path match query and ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + def firstByJsonPath(tableName: String, path: String, orderBy: Seq[Field[?]], conn: Connection): String = + CoreJson.firstByJsonPath(tableName, path, orderBy.asJava, conn) + + /** + * Retrieve the first document using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If called on a SQLite connection + */ + def firstByJsonPath(tableName: String, path: String, conn: Connection): String = + CoreJson.firstByJsonPath(tableName, path, conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only; creates + * connection) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def firstByJsonPath(tableName: String, path: String, orderBy: Seq[Field[?]] = Nil): String = + CoreJson.firstByJsonPath(tableName, path, orderBy.asJava) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query and ordering fields (PostgreSQL + * only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + def writeFirstByJsonPath(tableName: String, writer: PrintWriter, path: String, orderBy: Seq[Field[?]], + conn: Connection): Unit = + CoreJson.writeFirstByJsonPath(tableName, writer, path, orderBy.asJava, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param conn The connection over which documents should be retrieved + * @throws DocumentException If called on a SQLite connection + */ + def writeFirstByJsonPath(tableName: String, writer: PrintWriter, path: String, conn: Connection): Unit = + CoreJson.writeFirstByJsonPath(tableName, writer, path, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query and ordering fields (PostgreSQL + * only; creates connection) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If called on a SQLite connection + */ + def writeFirstByJsonPath(tableName: String, writer: PrintWriter, path: String, orderBy: Seq[Field[?]] = Nil): Unit = + CoreJson.writeFirstByJsonPath(tableName, writer, path, orderBy.asJava) diff --git a/src/scala/src/main/scala/Parameters.scala b/src/scala/src/main/scala/Parameters.scala new file mode 100644 index 0000000..b42c546 --- /dev/null +++ b/src/scala/src/main/scala/Parameters.scala @@ -0,0 +1,86 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Field, Op, Parameter, ParameterName} +import solutions.bitbadger.documents.java.Parameters as CoreParameters + +import java.sql.{Connection, PreparedStatement} +import java.util +import scala.collection.mutable +import scala.collection.mutable.ListBuffer +import _root_.scala.jdk.CollectionConverters.* + +/** + * Functions to assist with the creation and implementation of parameters for SQL queries + */ +object Parameters: + + /** + * Assign parameter names to any fields that do not have them assigned + * + * @param fields The collection of fields to be named + * @return The collection of fields with parameter names assigned + */ + def nameFields(fields: Seq[Field[?]]): Seq[Field[?]] = + val name = ParameterName() + fields.map { it => + if (it.getParameterName == null || it.getParameterName.isEmpty) + && !(Op.EXISTS :: Op.NOT_EXISTS :: Nil).contains(it.getComparison.getOp) then + it.withParameterName(name.derive(null)) + else + it + } + + /** + * Create a parameter by encoding a JSON object + * + * @param name The parameter name + * @param value The object to be encoded as JSON + * @return A parameter with the value encoded + */ + def json[A](name: String, value: A): Parameter[String] = + CoreParameters.json(name, value) + + /** + * Add field parameters to the given set of parameters + * + * @param fields The fields being compared in the query + * @param existing Any existing parameters for the query (optional, defaults to empty collection) + * @return A collection of parameters for the query + */ + def addFields(fields: Seq[Field[?]], + existing: mutable.Buffer[Parameter[?]] = ListBuffer()): mutable.Buffer[Parameter[?]] = + fields.foreach { it => it.appendParameter(new util.ArrayList[Parameter[?]]()).forEach(existing.append) } + existing + + /** + * Replace the parameter names in the query with question marks + * + * @param query The query with named placeholders + * @param parameters The parameters for the query + * @return The query, with name parameters changed to `?`s + */ + def replaceNamesInQuery(query: String, parameters: Seq[Parameter[?]]): String = + CoreParameters.replaceNamesInQuery(query, parameters.asJava) + + /** + * Apply the given parameters to the given query, returning a prepared statement + * + * @param conn The active JDBC connection + * @param query The query + * @param parameters The parameters for the query + * @return A `PreparedStatement` with the parameter names replaced with `?` and parameter values bound + * @throws DocumentException If parameter names are invalid or number value types are invalid + */ + def apply(conn: Connection, query: String, parameters: Seq[Parameter[?]]): PreparedStatement = + CoreParameters.apply(conn, query, parameters.asJava) + + /** + * Create parameters for field names to be removed from a document + * + * @param names The names of the fields to be removed + * @param parameterName The parameter name to use for the query + * @return A list of parameters to use for building the query + * @throws DocumentException If the dialect has not been set + */ + def fieldNames(names: Seq[String], parameterName: String = ":name"): mutable.Buffer[Parameter[?]] = + CoreParameters.fieldNames(names.asJava, parameterName).asScala.toBuffer diff --git a/src/scala/src/main/scala/Patch.scala b/src/scala/src/main/scala/Patch.scala new file mode 100644 index 0000000..e90e150 --- /dev/null +++ b/src/scala/src/main/scala/Patch.scala @@ -0,0 +1,120 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Field, FieldMatch} +import solutions.bitbadger.documents.java.Patch as CorePatch + +import java.sql.Connection +import _root_.scala.jdk.CollectionConverters.* + +/** + * Functions to patch (partially update) documents + */ +object Patch: + + /** + * Patch a document by its ID + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured + */ + def byId[Key, Patch](tableName: String, docId: Key, patch: Patch, conn: Connection): Unit = + CorePatch.byId(tableName, docId, patch, conn) + + /** + * Patch a document by its ID (creates connection) + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If no connection string has been set + */ + def byId[Key, Patch](tableName: String, docId: Key, patch: Patch): Unit = + CorePatch.byId(tableName, docId, patch) + + /** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields[Patch](tableName: String, fields: Seq[Field[?]], patch: Patch, howMatched: Option[FieldMatch], + conn: Connection): Unit = + CorePatch.byFields(tableName, fields.asJava, patch, howMatched.orNull, conn) + + /** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields[Patch](tableName: String, fields: Seq[Field[?]], patch: Patch, conn: Connection): Unit = + byFields(tableName, fields, patch, None, conn) + + /** + * Patch documents using a field comparison (creates connection) + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def byFields[Patch](tableName: String, fields: Seq[Field[?]], patch: Patch, + howMatched: Option[FieldMatch] = None): Unit = + CorePatch.byFields(tableName, fields.asJava, patch, howMatched.orNull) + + /** + * Patch documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + def byContains[A, Patch](tableName: String, criteria: A, patch: Patch, conn: Connection): Unit = + CorePatch.byContains(tableName, criteria, patch, conn) + + /** + * Patch documents using a JSON containment query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byContains[A, Patch](tableName: String, criteria: A, patch: Patch): Unit = + CorePatch.byContains(tableName, criteria, patch) + + /** + * Patch documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath[Patch](tableName: String, path: String, patch: Patch, conn: Connection): Unit = + CorePatch.byJsonPath(tableName, path, patch, conn) + + /** + * Patch documents using a JSON Path match query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byJsonPath[Patch](tableName: String, path: String, patch: Patch): Unit = + CorePatch.byJsonPath(tableName, path, patch) diff --git a/src/scala/src/main/scala/RemoveFields.scala b/src/scala/src/main/scala/RemoveFields.scala new file mode 100644 index 0000000..a605b7f --- /dev/null +++ b/src/scala/src/main/scala/RemoveFields.scala @@ -0,0 +1,120 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Field, FieldMatch} +import solutions.bitbadger.documents.java.RemoveFields as CoreRemoveFields + +import java.sql.Connection +import scala.jdk.CollectionConverters.* + +/** + * Functions to remove fields from documents + */ +object RemoveFields: + + /** + * Remove fields from a document by its ID + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured + */ + def byId[Key](tableName: String, docId: Key, toRemove: Seq[String], conn: Connection): Unit = + CoreRemoveFields.byId(tableName, docId, toRemove.asJava, conn) + + /** + * Remove fields from a document by its ID (creates connection) + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + * @throws DocumentException If no connection string has been set + */ + def byId[Key](tableName: String, docId: Key, toRemove: Seq[String]): Unit = + CoreRemoveFields.byId(tableName, docId, toRemove.asJava) + + /** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], toRemove: Seq[String], howMatched: Option[FieldMatch], + conn: Connection): Unit = + CoreRemoveFields.byFields(tableName, fields.asJava, toRemove.asJava, howMatched.orNull, conn) + + /** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], toRemove: Seq[String], conn: Connection): Unit = + byFields(tableName, fields, toRemove, None, conn) + + /** + * Remove fields from documents using a field comparison (creates connection) + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def byFields(tableName: String, fields: Seq[Field[?]], toRemove: Seq[String], + howMatched: Option[FieldMatch] = None): Unit = + CoreRemoveFields.byFields(tableName, fields.asJava, toRemove.asJava, howMatched.orNull) + + /** + * Remove fields from documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A, toRemove: Seq[String], conn: Connection): Unit = + CoreRemoveFields.byContains(tableName, criteria, toRemove.asJava, conn) + + /** + * Remove fields from documents using a JSON containment query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byContains[A](tableName: String, criteria: A, toRemove: Seq[String]): Unit = + CoreRemoveFields.byContains(tableName, criteria, toRemove.asJava) + + /** + * Remove fields from documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @param conn The connection on which the update should be executed + * @throws DocumentException If called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String, toRemove: Seq[String], conn: Connection): Unit = + CoreRemoveFields.byJsonPath(tableName, path, toRemove.asJava, conn) + + /** + * Remove fields from documents using a JSON Path match query (PostgreSQL only; creates connection) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def byJsonPath(tableName: String, path: String, toRemove: Seq[String]): Unit = + CoreRemoveFields.byJsonPath(tableName, path, toRemove.asJava) diff --git a/src/scala/src/main/scala/Results.scala b/src/scala/src/main/scala/Results.scala new file mode 100644 index 0000000..3b10978 --- /dev/null +++ b/src/scala/src/main/scala/Results.scala @@ -0,0 +1,143 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.java.Results as CoreResults + +import java.io.PrintWriter +import java.sql.{PreparedStatement, ResultSet, SQLException} +import scala.collection.mutable.ListBuffer +import scala.reflect.ClassTag +import scala.util.Using + +/** + * Functions to manipulate results + */ +object Results: + + /** + * Create a domain item from a document, specifying the field in which the document is found + * + * @param field The field name containing the JSON document + * @param rs A `ResultSet` set to the row with the document to be constructed + * @param ignored The class tag (placeholder used for signature; implicit tag used for serialization) + * @return The constructed domain item + */ + def fromDocument[Doc](field: String, rs: ResultSet, ignored: ClassTag[Doc])(using tag: ClassTag[Doc]): Doc = + CoreResults.fromDocument(field, rs, tag.runtimeClass.asInstanceOf[Class[Doc]]) + + /** + * Create a domain item from a document + * + * @param rs A `ResultSet` set to the row with the document to be constructed + * @param ignored The class tag (placeholder used for signature; implicit tag used for serialization) + * @return The constructed domain item + */ + def fromData[Doc](rs: ResultSet, ignored: ClassTag[Doc])(using tag: ClassTag[Doc]): Doc = + fromDocument[Doc]("data", rs, tag) + + /** + * Create a list of items for the results of the given command, using the specified mapping function + * + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to domain class instance + * @return A list of items from the query's result + * @throws DocumentException If there is a problem executing the query (unchecked) + */ + def toCustomList[Doc](stmt: PreparedStatement,mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): List[Doc] = + try + val buffer = ListBuffer[Doc]() + Using(stmt.executeQuery()) { rs => + while rs.next() do + buffer.append(mapFunc(rs, tag)) + } + buffer.toList + catch + case ex: SQLException => + throw DocumentException("Error retrieving documents from query: ${ex.message}", ex) + + /** + * Extract a count from the first column + * + * @param rs A `ResultSet` set to the row with the count to retrieve + * @return The count from the row + * @throws DocumentException If the dialect has not been set (unchecked) + */ + def toCount(rs: ResultSet, tag: ClassTag[Long] = ClassTag.Long): Long = + CoreResults.toCount(rs, Long.getClass) + + /** + * Extract a true/false value from the first column + * + * @param rs A `ResultSet` set to the row with the true/false value to retrieve + * @return The true/false value from the row + * @throws DocumentException If the dialect has not been set (unchecked) + */ + def toExists(rs: ResultSet, tag: ClassTag[Boolean] = ClassTag.Boolean): Boolean = + CoreResults.toExists(rs, Boolean.getClass) + + /** + * Retrieve the JSON text of a document, specifying the field in which the document is found + * + * @param field The field name containing the JSON document + * @param rs A `ResultSet` set to the row with the document to be constructed + * @return The JSON text of the document + */ + def jsonFromDocument(field: String, rs: ResultSet): String = + CoreResults.jsonFromDocument(field, rs) + + /** + * Retrieve the JSON text of a document, specifying the field in which the document is found + * + * @param rs A `ResultSet` set to the row with the document to be constructed + * @return The JSON text of the document + */ + def jsonFromData(rs: ResultSet): String = + CoreResults.jsonFromData(rs) + + /** + * Create a JSON array of items for the results of the given command, using the specified mapping function + * + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to JSON text + * @return A string with a JSON array of documents from the query's result + * @throws DocumentException If there is a problem executing the query (unchecked) + */ + def toJsonArray(stmt: PreparedStatement, mapFunc: ResultSet => String): String = + try + val results = StringBuilder("[") + Using(stmt.executeQuery()) { rs => + while rs.next() do + if (results.length > 2) results.append(",") + results.append(mapFunc(rs)) + } + results.append("]").toString() + catch + case ex: SQLException => throw DocumentException("Error retrieving documents from query: ${ex.message}", ex) + + /** + * Write a JSON array of items for the results of the given command to the given `PrintWriter`, using the specified + * mapping function + * + * @param writer The writer for the results of the query + * @param stmt The prepared statement to execute + * @param mapFunc The mapping function from data reader to JSON text + * @return A string with a JSON array of documents from the query's result + * @throws DocumentException If there is a problem executing the query (unchecked) + */ + def writeJsonArray(writer: PrintWriter, stmt: PreparedStatement, mapFunc: ResultSet => String): Unit = + try + writer.write("[") + Using(stmt.executeQuery()) { rs => + var isFirst = true + while rs.next() do + if isFirst then + isFirst = false + else + writer.write(",") + writer.write(mapFunc(rs)) + } + writer.write("]") + catch + case ex: SQLException => throw DocumentException("Error writing documents from query: ${ex.message}", ex) + diff --git a/src/scala/src/main/scala/extensions/package.scala b/src/scala/src/main/scala/extensions/package.scala new file mode 100644 index 0000000..68034e1 --- /dev/null +++ b/src/scala/src/main/scala/extensions/package.scala @@ -0,0 +1,776 @@ +package solutions.bitbadger.documents.scala.extensions + +import solutions.bitbadger.documents.{DocumentIndex, Field, FieldMatch, Parameter} +import solutions.bitbadger.documents.scala.* + +import java.io.PrintWriter +import java.sql.{Connection, ResultSet} +import scala.reflect.ClassTag + +extension (conn: Connection) + + // ~~~ CUSTOM QUERIES ~~~ + + /** + * Execute a query that returns a list of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If parameters are invalid + */ + def customList[Doc](query: String, parameters: Seq[Parameter[?]], mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): List[Doc] = + Custom.list[Doc](query, parameters, conn, mapFunc) + + /** + * Execute a query that returns a list of results + * + * @param query The query to retrieve the results + * @param mapFunc The mapping function between the document and the domain item + * @return A list of results for the given query + * @throws DocumentException If parameters are invalid + */ + def customList[Doc](query: String, mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): List[Doc] = + Custom.list[Doc](query, conn, mapFunc) + + /** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def customJsonArray(query: String, parameters: Seq[Parameter[?]], mapFunc: ResultSet => String): String = + Custom.jsonArray(query, parameters, conn, mapFunc) + + /** + * Execute a query that returns a JSON array of results + * + * @param query The query to retrieve the results + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def customJsonArray(query: String, mapFunc: ResultSet => String): String = + Custom.jsonArray(query, mapFunc) + + /** + * Execute a query that writes a JSON array of results to the given `PrintWriter` + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def writeCustomJsonArray(query: String, parameters: Seq[Parameter[?]], writer: PrintWriter, + mapFunc: ResultSet => String): Unit = + Custom.writeJsonArray(query, parameters, writer, conn, mapFunc) + + /** + * Execute a query that writes a JSON array of results to the given `PrintWriter` + * + * @param query The query to retrieve the results + * @param writer The writer to which the results should be written + * @param mapFunc The mapping function to extract the JSON from the query + * @return A JSON array of results for the given query + * @throws DocumentException If parameters are invalid + */ + def writeCustomJsonArray(query: String, writer: PrintWriter, mapFunc: ResultSet => String): Unit = + Custom.writeJsonArray(query, writer, mapFunc) + + /** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return An optional document, filled if one matches the query + * @throws DocumentException If parameters are invalid + */ + def customSingle[Doc](query: String, parameters: Seq[Parameter[?]], mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): Option[Doc] = + Custom.single[Doc](query, parameters, conn, mapFunc) + + /** + * Execute a query that returns one or no results + * + * @param query The query to retrieve the results + * @param mapFunc The mapping function between the document and the domain item + * @return An optional document, filled if one matches the query + * @throws DocumentException If parameters are invalid + */ + def customSingle[Doc](query: String, mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): Option[Doc] = + Custom.single[Doc](query, conn, mapFunc) + + /** + * Execute a query that returns JSON for one or no documents (creates connection) + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + def customJsonSingle(query: String, parameters: Seq[Parameter[?]], mapFunc: ResultSet => String): String = + Custom.jsonSingle(query, parameters, conn, mapFunc) + + /** + * Execute a query that returns JSON for one or no documents (creates connection) + * + * @param query The query to retrieve the results + * @param mapFunc The mapping function between the document and the domain item + * @return The JSON for the document if found, an empty object (`{}`) if not + * @throws DocumentException If parameters are invalid + */ + def customJsonSingle(query: String, mapFunc: ResultSet => String): String = + Custom.jsonSingle(query, mapFunc) + + /** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + * @throws DocumentException If parameters are invalid + */ + def customNonQuery(query: String, parameters: Seq[Parameter[?]] = Nil): Unit = + Custom.nonQuery(query, parameters, conn) + + /** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param parameters Parameters to use for the query + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If parameters are invalid + */ + def customScalar[A](query: String, parameters: Seq[Parameter[?]], mapFunc: (ResultSet, ClassTag[A]) => A) + (using tag: ClassTag[A]): A = + Custom.scalar[A](query, parameters, conn, mapFunc) + + /** + * Execute a query that returns a scalar result + * + * @param query The query to retrieve the result + * @param mapFunc The mapping function between the document and the domain item + * @return The scalar value from the query + * @throws DocumentException If parameters are invalid + */ + def customScalar[A](query: String, mapFunc: (ResultSet, ClassTag[A]) => A)(using tag: ClassTag[A]): A = + Custom.scalar[A](query, conn, mapFunc) + + // ~~~ DEFINITION QUERIES ~~~ + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + * @throws DocumentException If the dialect is not configured + */ + def ensureTable(tableName: String): Unit = + Definition.ensureTable(tableName, conn) + + /** + * Create an index on field(s) within documents in the specified table if necessary + * + * @param tableName The table to be indexed (may include schema) + * @param indexName The name of the index to create + * @param fields One or more fields to be indexed< + * @throws DocumentException If any dependent process does + */ + def ensureFieldIndex(tableName: String, indexName: String, fields: Seq[String]): Unit = + Definition.ensureFieldIndex(tableName, indexName, fields, conn) + + /** + * Create a document index on a table (PostgreSQL only) + * + * @param tableName The table to be indexed (may include schema) + * @param indexType The type of index to ensure + * @throws DocumentException If called on a SQLite connection + */ + def ensureDocumentIndex(tableName: String, indexType: DocumentIndex): Unit = + Definition.ensureDocumentIndex (tableName, indexType, conn) + + // ~~~ DOCUMENT MANIPULATION QUERIES ~~~ + + /** + * Insert a new document + * + * @param tableName The table into which the document should be inserted (may include schema) + * @param document The document to be inserted + * @throws DocumentException If IDs are misconfigured, or if the database command fails + */ + def insert[Doc](tableName: String, document: Doc): Unit = + Document.insert(tableName, document, conn) + + /** + * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") + * + * @param tableName The table in which the document should be saved (may include schema) + * @param document The document to be saved + * @throws DocumentException If the database command fails + */ + def save[Doc](tableName: String, document: Doc): Unit = + Document.save(tableName, document, conn) + + /** + * Update (replace) a document by its ID + * + * @param tableName The table in which the document should be replaced (may include schema) + * @param docId The ID of the document to be replaced + * @param document The document to be replaced + * @throws DocumentException If no dialect has been configured, or if the database command fails + */ + def update[Key, Doc](tableName: String, docId: Key, document: Doc): Unit = + Document.update(tableName, docId, document, conn) + + // ~~~ DOCUMENT COUNT QUERIES ~~~ + + /** + * Count all documents in the table + * + * @param tableName The name of the table in which documents should be counted + * @return A count of the documents in the table + * @throws DocumentException If any dependent process does + */ + def countAll(tableName: String): Long = + Count.all(tableName, conn) + + /** + * Count documents using a field comparison + * + * @param tableName The name of the table in which documents should be counted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, default `ALL`) + * @return A count of the matching documents in the table + * @throws DocumentException If the dialect has not been configured + */ + def countByFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None): Long = + Count.byFields(tableName, fields, howMatched, conn) + + /** + * Count documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param criteria The object for which JSON containment should be checked + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + def countByContains[A](tableName: String, criteria: A): Long = + Count.byContains(tableName, criteria, conn) + + /** + * Count documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be counted + * @param path The JSON path comparison to match + * @return A count of the matching documents in the table + * @throws DocumentException If called on a SQLite connection + */ + def countByJsonPath(tableName: String, path: String): Long = + Count.byJsonPath(tableName, path, conn) + + // ~~~ DOCUMENT EXISTENCE QUERIES ~~~ + + /** + * Determine a document's existence by its ID + * + * @param tableName The name of the table in which document existence should be checked + * @param docId The ID of the document to be checked + * @return True if the document exists, false if not + * @throws DocumentException If no dialect has been configured + */ + def existsById[Key](tableName: String, docId: Key): Boolean = + Exists.byId(tableName, docId, conn) + + /** + * Determine document existence using a field comparison + * + * @param tableName The name of the table in which document existence should be checked + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @return True if any matching documents exist, false if not + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def existsByFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None): Boolean = + Exists.byFields(tableName, fields, howMatched, conn) + + /** + * Determine document existence using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + def existsByContains[A](tableName: String, criteria: A): Boolean = + Exists.byContains(tableName, criteria, conn) + + /** + * Determine document existence using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document existence should be checked + * @param path The JSON path comparison to match + * @return True if any matching documents exist, false if not + * @throws DocumentException If called on a SQLite connection + */ + def existsByJsonPath(tableName: String, path: String): Boolean = + Exists.byJsonPath(tableName, path, conn) + + // ~~~ DOCUMENT RETRIEVAL QUERIES (Domain Objects) ~~~ + + /** + * Retrieve all documents in the given table, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents from the given table + * @throws DocumentException If query execution fails + */ + def findAll[Doc](tableName: String, orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): List[Doc] = + Find.all[Doc](tableName, orderBy, conn) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return The document if it is found, `None` otherwise + * @throws DocumentException If no dialect has been configured + */ + def findById[Key, Doc](tableName: String, docId: Key)(using tag: ClassTag[Doc]): Option[Doc] = + Find.byId[Key, Doc](tableName, docId, conn) + + /** + * Retrieve documents using a field comparison, ordering results by the optional given fields + * + * @param tableName The table from which the document should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the field comparison + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def findByFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): List[Doc] = + Find.byFields[Doc](tableName, fields, howMatched, orderBy, conn) + + /** + * Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL + * only) + * + * @param tableName The name of the table in which document existence should be checked + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON containment query + * @throws DocumentException If called on a SQLite connection + */ + def findByContains[Doc, A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = Nil) + (using tag: ClassTag[Doc]): List[Doc] = + Find.byContains[Doc, A](tableName, criteria, orderBy, conn) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A list of documents matching the JSON Path match query + * @throws DocumentException If called on a SQLite connection + */ + def findByJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]] = Nil) + (using tag: ClassTag[Doc]): List[Doc] = + Find.byJsonPath[Doc](tableName, path, orderBy, conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the field comparison, or `None` if no matches are found + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def findFirstByFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): Option[Doc] = + Find.firstByFields[Doc](tableName, fields, howMatched, orderBy, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the JSON containment query, or `None` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ + def findFirstByContains[Doc, A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = Nil) + (using tag: ClassTag[Doc]): Option[Doc] = + Find.firstByContains[Doc, A](tableName, criteria, orderBy, conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first document matching the JSON Path match query, or `None` if no matches are found + * @throws DocumentException If called on a SQLite connection + */ + def findFirstByJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]] = Nil) + (using tag: ClassTag[Doc]): Option[Doc] = + Find.firstByJsonPath[Doc](tableName, path, orderBy, conn) + + // ~~~ DOCUMENT RETRIEVAL QUERIES (Raw JSON) ~~~ + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents from the given table + * @throws DocumentException If no connection string has been set, or if query execution fails + */ + def jsonAll(tableName: String, orderBy: Seq[Field[?]] = Nil): String = + Json.all(tableName, orderBy, conn) + + /** + * Retrieve a document by its ID + * + * @param tableName The table from which the document should be retrieved + * @param docId The ID of the document to retrieve + * @return A JSON document if found, an empty JSON object if not found + * @throws DocumentException If no connection string has been set + */ + def jsonById[Key](tableName: String, docId: Key): String = + Json.byId(tableName, docId, conn) + + /** + * Retrieve documents using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the field comparison + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def jsonByFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = Nil): String = + Json.byFields(tableName, fields, howMatched, orderBy, conn) + + /** + * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON containment query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def jsonByContains[A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = Nil): String = + Json.byContains(tableName, criteria, orderBy, conn) + + /** + * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return A JSON array of documents matching the JSON Path match query + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def jsonByJsonPath(tableName: String, path: String, orderBy: Seq[Field[?]] = Nil): String = + Json.byJsonPath(tableName, path, orderBy, conn) + + /** + * Retrieve the first document using a field comparison and optional ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the field comparison if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def jsonFirstByFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = Nil): String = + Json.firstByFields(tableName, fields, howMatched, orderBy, conn) + + /** + * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def jsonFirstByContains[A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = Nil): String = + Json.firstByContains(tableName, criteria, orderBy, conn) + + /** + * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def jsonFirstByJsonPath(tableName: String, path: String, orderBy: Seq[Field[?]] = Nil): String = + Json.firstByJsonPath(tableName, path, orderBy, conn) + + // ~~~ DOCUMENT RETRIEVAL QUERIES (Write raw JSON to PrintWriter) ~~~ + + /** + * Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if query execution fails + */ + def writeJsonAll(tableName: String, writer: PrintWriter, orderBy: Seq[Field[?]] = Nil): Unit = + Json.writeAll(tableName, writer, orderBy, conn) + + /** + * Write a document to the given `PrintWriter` by its ID + * + * @param tableName The table from which the document should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param docId The ID of the document to retrieve + * @throws DocumentException If no connection string has been set + */ + def writeJsonById[Key](tableName: String, writer: PrintWriter, docId: Key): Unit = + Json.writeById(tableName, writer, docId, conn) + + /** + * Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def writeJsonByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], + howMatched: Option[FieldMatch] = None, orderBy: Seq[Field[?]] = Nil): Unit = + Json.writeByFields(tableName, writer, fields, howMatched, orderBy, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def writeJsonByContains[A](tableName: String, writer: PrintWriter, criteria: A, orderBy: Seq[Field[?]] = Nil): Unit = + Json.writeByContains(tableName, writer, criteria, orderBy, conn) + + /** + * Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields + * (PostgreSQL only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def writeJsonByJsonPath(tableName: String, writer: PrintWriter, path: String, orderBy: Seq[Field[?]] = Nil): Unit = + Json.writeByJsonPath(tableName, writer, path, orderBy, conn) + + /** + * Write the first document to the given `PrintWriter` using a field comparison and ordering fields + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`) + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if parameters are invalid + */ + def writeJsonFirstByFields(tableName: String, writer: PrintWriter, fields: Seq[Field[?]], + howMatched: Option[FieldMatch] = None, orderBy: Seq[Field[?]] = Nil): Unit = + Json.writeFirstByFields(tableName, writer, fields, howMatched, orderBy, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON containment query and ordering fields (PostgreSQL + * only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param criteria The object for which JSON containment should be checked + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def writeJsonFirstByContains[A](tableName: String, writer: PrintWriter, criteria: A, + orderBy: Seq[Field[?]] = Nil): Unit = + Json.writeFirstByContains(tableName, writer, criteria, orderBy, conn) + + /** + * Write the first document to the given `PrintWriter` using a JSON Path match query and ordering fields (PostgreSQL + * only) + * + * @param tableName The table from which documents should be retrieved + * @param writer The `PrintWriter` to which the results should be written + * @param path The JSON path comparison to match + * @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering) + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection + */ + def writeJsonFirstByJsonPath(tableName: String, writer: PrintWriter, path: String, + orderBy: Seq[Field[?]] = Nil): Unit = + Json.writeFirstByJsonPath(tableName, writer, path, orderBy, conn) + + // ~~~ DOCUMENT PATCH (PARTIAL UPDATE) QUERIES ~~~ + + /** + * Patch a document by its ID + * + * @param tableName The name of the table in which a document should be patched + * @param docId The ID of the document to be patched + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If no dialect has been configured + */ + def patchById[Key, Patch](tableName: String, docId: Key, patch: Patch): Unit = + Patch.byId(tableName, docId, patch, conn) + + /** + * Patch documents using a field comparison + * + * @param tableName The name of the table in which documents should be patched + * @param fields The fields which should be compared + * @param patch The object whose properties should be replaced in the document + * @param howMatched How the fields should be matched + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def patchByFields[Patch](tableName: String, fields: Seq[Field[?]], patch: Patch, + howMatched: Option[FieldMatch] = None): Unit = + Patch.byFields(tableName, fields, patch, howMatched, conn) + + /** + * Patch documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param criteria The object against which JSON containment should be checked + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If called on a SQLite connection + */ + def patchByContains[A, Patch](tableName: String, criteria: A, patch: Patch): Unit = + Patch.byContains(tableName, criteria, patch, conn) + + /** + * Patch documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which documents should be patched + * @param path The JSON path comparison to match + * @param patch The object whose properties should be replaced in the document + * @throws DocumentException If called on a SQLite connection + */ + def patchByJsonPath[Patch](tableName: String, path: String, patch: Patch): Unit = + Patch.byJsonPath(tableName, path, patch, conn) + + // ~~~ DOCUMENT FIELD REMOVAL QUERIES ~~~ + + /** + * Remove fields from a document by its ID + * + * @param tableName The name of the table in which the document's fields should be removed + * @param docId The ID of the document to have fields removed + * @param toRemove The names of the fields to be removed + * @throws DocumentException If no dialect has been configured + */ + def removeFieldsById[Key](tableName: String, docId: Key, toRemove: Seq[String]): Unit = + RemoveFields.byId(tableName, docId, toRemove, conn) + + /** + * Remove fields from documents using a field comparison + * + * @param tableName The name of the table in which document fields should be removed + * @param fields The fields which should be compared + * @param toRemove The names of the fields to be removed + * @param howMatched How the fields should be matched + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def removeFieldsByFields(tableName: String, fields: Seq[Field[?]], toRemove: Seq[String], + howMatched: Option[FieldMatch] = None): Unit = + RemoveFields.byFields(tableName, fields, toRemove, howMatched, conn) + + /** + * Remove fields from documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param criteria The object against which JSON containment should be checked + * @param toRemove The names of the fields to be removed + * @throws DocumentException If called on a SQLite connection + */ + def removeFieldsByContains[A](tableName: String, criteria: A, toRemove: Seq[String]): Unit = + RemoveFields.byContains(tableName, criteria, toRemove, conn) + + /** + * Remove fields from documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table in which document fields should be removed + * @param path The JSON path comparison to match + * @param toRemove The names of the fields to be removed + * @throws DocumentException If called on a SQLite connection + */ + def removeFieldsByJsonPath(tableName: String, path: String, toRemove: Seq[String]): Unit = + RemoveFields.byJsonPath(tableName, path, toRemove, conn) + + // ~~~ DOCUMENT DELETION QUERIES ~~~ + + /** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + * @throws DocumentException If no dialect has been configured + */ + def deleteById[Key](tableName: String, docId: Key): Unit = + Delete.byId(tableName, docId, conn) + + /** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @throws DocumentException If no dialect has been configured, or if parameters are invalid + */ + def deleteByFields(tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None): Unit = + Delete.byFields(tableName, fields, howMatched, conn) + + /** + * Delete documents using a JSON containment query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param criteria The object for which JSON containment should be checked + * @throws DocumentException If called on a SQLite connection + */ + def deleteByContains[A](tableName: String, criteria: A): Unit = + Delete.byContains(tableName, criteria, conn) + + /** + * Delete documents using a JSON Path match query (PostgreSQL only) + * + * @param tableName The name of the table from which documents should be deleted + * @param path The JSON path comparison to match + * @throws DocumentException If called on a SQLite connection + */ + def deleteByJsonPath(tableName: String, path: String): Unit = + Delete.byJsonPath(tableName, path, conn) diff --git a/src/scala/src/test/scala/AutoIdTest.scala b/src/scala/src/test/scala/AutoIdTest.scala new file mode 100644 index 0000000..36bbc46 --- /dev/null +++ b/src/scala/src/test/scala/AutoIdTest.scala @@ -0,0 +1,126 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.{AutoId, DocumentException} + +@DisplayName("Scala | AutoId") +class AutoIdTest: + + @Test + @DisplayName("Generates a UUID string") + def generateUUID(): Unit = + assertEquals(32, AutoId.generateUUID().length(), "The UUID should have been a 32-character string") + + @Test + @DisplayName("Generates a random hex character string of an even length") + def generateRandomStringEven(): Unit = + val result = AutoId.generateRandomString(8) + assertEquals(8, result.length(), s"There should have been 8 characters in $result") + + @Test + @DisplayName("Generates a random hex character string of an odd length") + def generateRandomStringOdd(): Unit = + val result = AutoId.generateRandomString(11) + assertEquals(11, result.length(), s"There should have been 11 characters in $result") + + @Test + @DisplayName("Generates different random hex character strings") + def generateRandomStringIsRandom(): Unit = + val result1 = AutoId.generateRandomString(16) + val result2 = AutoId.generateRandomString(16) + assertNotEquals(result1, result2, "There should have been 2 different strings generated") + + @Test + @DisplayName("needsAutoId fails for null document") + def needsAutoIdFailsForNullDocument(): Unit = + assertThrows(classOf[DocumentException], () => AutoId.needsAutoId(AutoId.DISABLED, null, "id")) + + @Test + @DisplayName("needsAutoId fails for missing ID property") + def needsAutoIdFailsForMissingId(): Unit = + assertThrows(classOf[DocumentException], () => AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id")) + + + @Test + @DisplayName("needsAutoId returns false if disabled") + def needsAutoIdFalseIfDisabled(): Unit = + assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false") + + @Test + @DisplayName("needsAutoId returns true for Number strategy and byte ID of 0") + def needsAutoIdTrueForByteWithZero(): Unit = + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id"), "Number Auto ID with 0 should return true") + + @Test + @DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0") + def needsAutoIdFalseForByteWithNonZero(): Unit = + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id"), "Number Auto ID with 77 should return false") + + @Test + @DisplayName("needsAutoId returns true for Number strategy and short ID of 0") + def needsAutoIdTrueForShortWithZero(): Unit = + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id"), "Number Auto ID with 0 should return true") + + @Test + @DisplayName("needsAutoId returns false for Number strategy and short ID of non-0") + def needsAutoIdFalseForShortWithNonZero(): Unit = + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id"), "Number Auto ID with 31 should return false") + + @Test + @DisplayName("needsAutoId returns true for Number strategy and int ID of 0") + def needsAutoIdTrueForIntWithZero(): Unit = + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id"), "Number Auto ID with 0 should return true") + + @Test + @DisplayName("needsAutoId returns false for Number strategy and int ID of non-0") + def needsAutoIdFalseForIntWithNonZero(): Unit = + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id"), "Number Auto ID with 6 should return false") + + @Test + @DisplayName("needsAutoId returns true for Number strategy and long ID of 0") + def needsAutoIdTrueForLongWithZero(): Unit = + assertTrue(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id"), "Number Auto ID with 0 should return true") + + @Test + @DisplayName("needsAutoId returns false for Number strategy and long ID of non-0") + def needsAutoIdFalseForLongWithNonZero(): Unit = + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id"), "Number Auto ID with 2 should return false") + + @Test + @DisplayName("needsAutoId fails for Number strategy and non-number ID") + def needsAutoIdFailsForNumberWithStringId(): Unit = + assertThrows(classOf[DocumentException], () => AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id")) + + @Test + @DisplayName("needsAutoId returns true for UUID strategy and blank ID") + def needsAutoIdTrueForUUIDWithBlank(): Unit = + assertTrue(AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id"), "UUID Auto ID with blank should return true") + + @Test + @DisplayName("needsAutoId returns false for UUID strategy and non-blank ID") + def needsAutoIdFalseForUUIDNotBlank(): Unit = + assertFalse(AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id"), + "UUID Auto ID with non-blank should return false") + + @Test + @DisplayName("needsAutoId fails for UUID strategy and non-string ID") + def needsAutoIdFailsForUUIDNonString(): Unit = + assertThrows(classOf[DocumentException], () => AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id")) + + @Test + @DisplayName("needsAutoId returns true for Random String strategy and blank ID") + def needsAutoIdTrueForRandomWithBlank(): Unit = + assertTrue(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id"), + "Random String Auto ID with blank should return true") + + @Test + @DisplayName("needsAutoId returns false for Random String strategy and non-blank ID") + def needsAutoIdFalseForRandomNotBlank(): Unit = + assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id"), + "Random String Auto ID with non-blank should return false") + + @Test + @DisplayName("needsAutoId fails for Random String strategy and non-string ID") + def needsAutoIdFailsForRandomNonString(): Unit = + assertThrows(classOf[DocumentException], () => AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id")) diff --git a/src/scala/src/test/scala/ByteIdClass.scala b/src/scala/src/test/scala/ByteIdClass.scala new file mode 100644 index 0000000..130f16a --- /dev/null +++ b/src/scala/src/test/scala/ByteIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +class ByteIdClass(var id: Byte) diff --git a/src/scala/src/test/scala/ConfigurationTest.scala b/src/scala/src/test/scala/ConfigurationTest.scala new file mode 100644 index 0000000..8060f85 --- /dev/null +++ b/src/scala/src/test/scala/ConfigurationTest.scala @@ -0,0 +1,33 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.{AutoId, Configuration, Dialect, DocumentException} + +@DisplayName("Scala | Configuration") +class ConfigurationTest: + + @Test + @DisplayName("Default ID field is `id`") + def defaultIdField(): Unit = + assertEquals("id", Configuration.idField, "Default ID field incorrect") + + @Test + @DisplayName("Default Auto ID strategy is `DISABLED`") + def defaultAutoId(): Unit = + assertEquals(AutoId.DISABLED, Configuration.autoIdStrategy, "Default Auto ID strategy should be `disabled`") + + @Test + @DisplayName("Default ID string length should be 16") + def defaultIdStringLength(): Unit = + assertEquals(16, Configuration.idStringLength, "Default ID string length should be 16") + + @Test + @DisplayName("Dialect is derived from connection string") + def dialectIsDerived(): Unit = + try + assertThrows(classOf[DocumentException], () => Configuration.dialect()) + Configuration.setConnectionString("jdbc:postgresql:db") + assertEquals(Dialect.POSTGRESQL, Configuration.dialect()) + finally + Configuration.setConnectionString(null) diff --git a/src/scala/src/test/scala/CountQueryTest.scala b/src/scala/src/test/scala/CountQueryTest.scala new file mode 100644 index 0000000..00879e6 --- /dev/null +++ b/src/scala/src/test/scala/CountQueryTest.scala @@ -0,0 +1,66 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.CountQuery + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | CountQuery") +class CountQueryTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("all generates correctly") + def all(): Unit = + assertEquals(s"SELECT COUNT(*) AS it FROM $TEST_TABLE", CountQuery.all(TEST_TABLE), + "Count query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + def byFieldsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0", + CountQuery.byFields(TEST_TABLE, List(Field.equal("test", "", ":field0")).asJava), + "Count query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | SQLite") + def byFieldsSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0", + CountQuery.byFields(TEST_TABLE, List(Field.equal("test", "", ":field0")).asJava), + "Count query not constructed correctly") + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + def byContainsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria", CountQuery.byContains(TEST_TABLE), + "Count query not constructed correctly") + + @Test + @DisplayName("byContains fails | SQLite") + def byContainsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => CountQuery.byContains(TEST_TABLE)) + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + def byJsonPathPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)", + CountQuery.byJsonPath(TEST_TABLE), "Count query not constructed correctly") + + @Test + @DisplayName("byJsonPath fails | SQLite") + def byJsonPathSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => CountQuery.byJsonPath(TEST_TABLE)) diff --git a/src/scala/src/test/scala/DefinitionQueryTest.scala b/src/scala/src/test/scala/DefinitionQueryTest.scala new file mode 100644 index 0000000..62e4263 --- /dev/null +++ b/src/scala/src/test/scala/DefinitionQueryTest.scala @@ -0,0 +1,104 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{Dialect, DocumentException, DocumentIndex} +import solutions.bitbadger.documents.query.DefinitionQuery + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | DefinitionQuery") +class DefinitionQueryTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("ensureTableFor generates correctly") + def ensureTableFor(): Unit = + assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", + DefinitionQuery.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly") + + @Test + @DisplayName("ensureTable generates correctly | PostgreSQL") + def ensureTablePostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"CREATE TABLE IF NOT EXISTS $TEST_TABLE (data JSONB NOT NULL)", + DefinitionQuery.ensureTable(TEST_TABLE)) + + @Test + @DisplayName("ensureTable generates correctly | SQLite") + def ensureTableSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"CREATE TABLE IF NOT EXISTS $TEST_TABLE (data TEXT NOT NULL)", + DefinitionQuery.ensureTable(TEST_TABLE)) + + @Test + @DisplayName("ensureTable fails when no dialect is set") + def ensureTableFailsUnknown(): Unit = + assertThrows(classOf[DocumentException], () => DefinitionQuery.ensureTable(TEST_TABLE)) + + @Test + @DisplayName("ensureKey generates correctly with schema") + def ensureKeyWithSchema(): Unit = + assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", + DefinitionQuery.ensureKey("test.table", Dialect.POSTGRESQL), + "CREATE INDEX for key statement with schema not constructed correctly") + + @Test + @DisplayName("ensureKey generates correctly without schema") + def ensureKeyWithoutSchema(): Unit = + assertEquals(s"CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))", + DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE), + "CREATE INDEX for key statement without schema not constructed correctly") + + @Test + @DisplayName("ensureIndexOn generates multiple fields and directions") + def ensureIndexOnMultipleFields(): Unit = + assertEquals("CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", + DefinitionQuery.ensureIndexOn("test.table", "gibberish", List("taco", "guac DESC", "salsa ASC").asJava, + Dialect.POSTGRESQL), + "CREATE INDEX for multiple field statement not constructed correctly") + + @Test + @DisplayName("ensureIndexOn generates nested field | PostgreSQL") + def ensureIndexOnNestedPostgres(): Unit = + assertEquals(s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data#>>'{a,b,c}'))", + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List("a.b.c").asJava, Dialect.POSTGRESQL), + "CREATE INDEX for nested PostgreSQL field incorrect") + + @Test + @DisplayName("ensureIndexOn generates nested field | SQLite") + def ensureIndexOnNestedSQLite(): Unit = + assertEquals(s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data->'a'->'b'->>'c'))", + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List("a.b.c").asJava, Dialect.SQLITE), + "CREATE INDEX for nested SQLite field incorrect") + + @Test + @DisplayName("ensureDocumentIndexOn generates Full | PostgreSQL") + def ensureDocumentIndexOnFullPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)", + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL), + "CREATE INDEX for full document index incorrect") + + @Test + @DisplayName("ensureDocumentIndexOn generates Optimized | PostgreSQL") + def ensureDocumentIndexOnOptimizedPostgres(): Unit = + ForceDialect.postgres() + assertEquals( + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data jsonb_path_ops)", + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED), + "CREATE INDEX for optimized document index incorrect") + + @Test + @DisplayName("ensureDocumentIndexOn fails | SQLite") + def ensureDocumentIndexOnFailsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], + () => DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL)) diff --git a/src/scala/src/test/scala/DeleteQueryTest.scala b/src/scala/src/test/scala/DeleteQueryTest.scala new file mode 100644 index 0000000..47ba9f5 --- /dev/null +++ b/src/scala/src/test/scala/DeleteQueryTest.scala @@ -0,0 +1,74 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.DeleteQuery + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | DeleteQuery") +class DeleteQueryTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + def byIdPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", DeleteQuery.byId(TEST_TABLE), + "Delete query not constructed correctly") + + @Test + @DisplayName("byId generates correctly | SQLite") + def byIdSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", DeleteQuery.byId(TEST_TABLE), + "Delete query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + def byFieldsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"DELETE FROM $TEST_TABLE WHERE data->>'a' = :b", + DeleteQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b")).asJava), + "Delete query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | SQLite") + def byFieldsSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"DELETE FROM $TEST_TABLE WHERE data->>'a' = :b", + DeleteQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b")).asJava), + "Delete query not constructed correctly") + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + def byContainsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"DELETE FROM $TEST_TABLE WHERE data @> :criteria", DeleteQuery.byContains(TEST_TABLE), + "Delete query not constructed correctly") + + @Test + @DisplayName("byContains fails | SQLite") + def byContainsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => DeleteQuery.byContains(TEST_TABLE)) + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + def byJsonPathPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)", + DeleteQuery.byJsonPath(TEST_TABLE), "Delete query not constructed correctly") + + @Test + @DisplayName("byJsonPath fails | SQLite") + def byJsonPathSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => DeleteQuery.byJsonPath(TEST_TABLE)) diff --git a/src/scala/src/test/scala/DialectTest.scala b/src/scala/src/test/scala/DialectTest.scala new file mode 100644 index 0000000..9940ef2 --- /dev/null +++ b/src/scala/src/test/scala/DialectTest.scala @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.{Dialect, DocumentException} + +@DisplayName("Scala | Dialect") +class DialectTest: + + @Test + @DisplayName("deriveFromConnectionString derives PostgreSQL correctly") + def derivesPostgres(): Unit = + assertEquals(Dialect.POSTGRESQL, Dialect.deriveFromConnectionString("jdbc:postgresql:db"), + "Dialect should have been PostgreSQL") + + @Test + @DisplayName("deriveFromConnectionString derives SQLite correctly") + def derivesSQLite(): Unit = + assertEquals(Dialect.SQLITE, Dialect.deriveFromConnectionString("jdbc:sqlite:memory"), + "Dialect should have been SQLite") + + @Test + @DisplayName("deriveFromConnectionString fails when the connection string is unknown") + def deriveFailsWhenUnknown(): Unit = + try + Dialect.deriveFromConnectionString("SQL Server") + fail("Dialect derivation should have failed") + catch + case ex: DocumentException => + assertNotNull(ex.getMessage, "The exception message should not have been null") + assertTrue(ex.getMessage.contains("[SQL Server]"), + "The connection string should have been in the exception message") diff --git a/src/scala/src/test/scala/DocumentIndexTest.scala b/src/scala/src/test/scala/DocumentIndexTest.scala new file mode 100644 index 0000000..0f2f1b0 --- /dev/null +++ b/src/scala/src/test/scala/DocumentIndexTest.scala @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.DocumentIndex + +@DisplayName("Scala | DocumentIndex") +class DocumentIndexTest: + + @Test + @DisplayName("FULL uses proper SQL") + def fullSQL(): Unit = + assertEquals("", DocumentIndex.FULL.getSql, "The SQL for Full is incorrect") + + @Test + @DisplayName("OPTIMIZED uses proper SQL") + def optimizedSQL(): Unit = + assertEquals(" jsonb_path_ops", DocumentIndex.OPTIMIZED.getSql, "The SQL for Optimized is incorrect") diff --git a/src/scala/src/test/scala/DocumentQueryTest.scala b/src/scala/src/test/scala/DocumentQueryTest.scala new file mode 100644 index 0000000..939a5d5 --- /dev/null +++ b/src/scala/src/test/scala/DocumentQueryTest.scala @@ -0,0 +1,109 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{AutoId, Configuration, DocumentException} +import solutions.bitbadger.documents.query.DocumentQuery + +@DisplayName("Scala | Query | DocumentQuery") +class DocumentQueryTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("insert generates no auto ID | PostgreSQL") + def insertNoAutoPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"INSERT INTO $TEST_TABLE VALUES (:data)", DocumentQuery.insert(TEST_TABLE)) + + @Test + @DisplayName("insert generates no auto ID | SQLite") + def insertNoAutoSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"INSERT INTO $TEST_TABLE VALUES (:data)", DocumentQuery.insert(TEST_TABLE)) + + @Test + @DisplayName("insert generates auto number | PostgreSQL") + def insertAutoNumberPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || ('{\"id\":' " + + s"|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $TEST_TABLE) || '}')::jsonb)", + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER)) + + @Test + @DisplayName("insert generates auto number | SQLite") + def insertAutoNumberSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', " + + s"(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $TEST_TABLE)))", + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER)) + + @Test + @DisplayName("insert generates auto UUID | PostgreSQL") + def insertAutoUUIDPostgres(): Unit = + ForceDialect.postgres() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + assertTrue(query.startsWith(s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\""), + s"Query start not correct (actual: $query)") + assertTrue(query.endsWith("\"}')"), "Query end not correct") + + @Test + @DisplayName("insert generates auto UUID | SQLite") + def insertAutoUUIDSQLite(): Unit = + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + assertTrue(query.startsWith(s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '"), + s"Query start not correct (actual: $query)") + assertTrue(query.endsWith("'))"), "Query end not correct") + + @Test + @DisplayName("insert generates auto random string | PostgreSQL") + def insertAutoRandomPostgres(): Unit = + try + ForceDialect.postgres() + Configuration.idStringLength = 8 + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + assertTrue(query.startsWith(s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\""), + s"Query start not correct (actual: $query)") + assertTrue(query.endsWith("\"}')"), "Query end not correct") + assertEquals(8, query.replace(s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", "") + .replace("\"}')", "").length, + "Random string length incorrect") + finally + Configuration.idStringLength = 16 + + @Test + @DisplayName("insert generates auto random string | SQLite") + def insertAutoRandomSQLite(): Unit = + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + assertTrue(query.startsWith(s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '"), + s"Query start not correct (actual: $query)") + assertTrue(query.endsWith("'))"), "Query end not correct") + assertEquals(Configuration.idStringLength, + query.replace(s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '", "").replace("'))", "").length, + "Random string length incorrect") + + @Test + @DisplayName("insert fails when no dialect is set") + def insertFailsUnknown(): Unit = + assertThrows(classOf[DocumentException], () => DocumentQuery.insert(TEST_TABLE)) + + @Test + @DisplayName("save generates correctly") + def save(): Unit = + ForceDialect.postgres() + assertEquals( + s"INSERT INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", + DocumentQuery.save(TEST_TABLE), "INSERT ON CONFLICT UPDATE statement not constructed correctly") + + @Test + @DisplayName("update generates successfully") + def update(): Unit = + assertEquals(s"UPDATE $TEST_TABLE SET data = :data", DocumentQuery.update(TEST_TABLE), + "Update query not constructed correctly") diff --git a/src/scala/src/test/scala/ExistsQueryTest.scala b/src/scala/src/test/scala/ExistsQueryTest.scala new file mode 100644 index 0000000..c198d7a --- /dev/null +++ b/src/scala/src/test/scala/ExistsQueryTest.scala @@ -0,0 +1,74 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.ExistsQuery + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | ExistsQuery") +class ExistsQueryTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + def byIdPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it", + ExistsQuery.byId(TEST_TABLE), "Exists query not constructed correctly") + + @Test + @DisplayName("byId generates correctly | SQLite") + def byIdSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it", + ExistsQuery.byId(TEST_TABLE), "Exists query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + def byFieldsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE (data->>'it')::numeric = :test) AS it", + ExistsQuery.byFields(TEST_TABLE, List(Field.equal("it", 7, ":test")).asJava), + "Exists query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | SQLite") + def byFieldsSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'it' = :test) AS it", + ExistsQuery.byFields(TEST_TABLE, List(Field.equal("it", 7, ":test")).asJava), + "Exists query not constructed correctly") + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + def byContainsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data @> :criteria) AS it", + ExistsQuery.byContains(TEST_TABLE), "Exists query not constructed correctly") + + @Test + @DisplayName("byContains fails | SQLite") + def byContainsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => ExistsQuery.byContains(TEST_TABLE)) + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + def byJsonPathPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)) AS it", + ExistsQuery.byJsonPath(TEST_TABLE), "Exists query not constructed correctly") + + @Test + @DisplayName("byJsonPath fails | SQLite") + def byJsonPathSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => ExistsQuery.byJsonPath(TEST_TABLE)) diff --git a/src/scala/src/test/scala/FieldMatchTest.scala b/src/scala/src/test/scala/FieldMatchTest.scala new file mode 100644 index 0000000..596e6f8 --- /dev/null +++ b/src/scala/src/test/scala/FieldMatchTest.scala @@ -0,0 +1,21 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.FieldMatch + +/** + * Unit tests for the `FieldMatch` enum + */ +@DisplayName("Scala | FieldMatch") +class FieldMatchTest: + + @Test + @DisplayName("ANY uses proper SQL") + def any(): Unit = + assertEquals("OR", FieldMatch.ANY.getSql, "ANY should use OR") + + @Test + @DisplayName("ALL uses proper SQL") + def all(): Unit = + assertEquals("AND", FieldMatch.ALL.getSql, "ALL should use AND") diff --git a/src/scala/src/test/scala/FieldTest.scala b/src/scala/src/test/scala/FieldTest.scala new file mode 100644 index 0000000..0b60727 --- /dev/null +++ b/src/scala/src/test/scala/FieldTest.scala @@ -0,0 +1,537 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.* + +import _root_.scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Field") +class FieldTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + // ~~~ INSTANCE METHODS ~~~ + + @Test + @DisplayName("withParameterName fails for invalid name") + def withParamNameFails(): Unit = + assertThrows(classOf[DocumentException], () => Field.equal("it", "").withParameterName("2424")) + + @Test + @DisplayName("withParameterName works with colon prefix") + def withParamNameColon(): Unit = + val field = Field.equal("abc", "22").withQualifier("me") + val withParam = field.withParameterName(":test") + assertNotSame(field, withParam, "A new Field instance should have been created") + assertEquals(field.getName, withParam.getName, "Name should have been preserved") + assertEquals(field.getComparison, withParam.getComparison, "Comparison should have been preserved") + assertEquals(":test", withParam.getParameterName, "Parameter name not set correctly") + assertEquals(field.getQualifier, withParam.getQualifier, "Qualifier should have been preserved") + + @Test + @DisplayName("withParameterName works with at-sign prefix") + def withParamNameAtSign(): Unit = + val field = Field.equal("def", "44") + val withParam = field.withParameterName("@unit") + assertNotSame(field, withParam, "A new Field instance should have been created") + assertEquals(field.getName, withParam.getName, "Name should have been preserved") + assertEquals(field.getComparison, withParam.getComparison, "Comparison should have been preserved") + assertEquals("@unit", withParam.getParameterName, "Parameter name not set correctly") + assertEquals(field.getQualifier, withParam.getQualifier, "Qualifier should have been preserved") + + @Test + @DisplayName("withQualifier sets qualifier correctly") + def withQualifier(): Unit = + val field = Field.equal("j", "k") + val withQual = field.withQualifier("test") + assertNotSame(field, withQual, "A new Field instance should have been created") + assertEquals(field.getName, withQual.getName, "Name should have been preserved") + assertEquals(field.getComparison, withQual.getComparison, "Comparison should have been preserved") + assertEquals(field.getParameterName, withQual.getParameterName, "Parameter Name should have been preserved") + assertEquals("test", withQual.getQualifier, "Qualifier not set correctly") + + @Test + @DisplayName("path generates for simple unqualified PostgreSQL field") + def pathPostgresSimpleUnqualified(): Unit = + assertEquals("data->>'SomethingCool'", + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct") + + @Test + @DisplayName("path generates for simple qualified PostgreSQL field") + def pathPostgresSimpleQualified(): Unit = + assertEquals("this.data->>'SomethingElse'", + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not correct") + + @Test + @DisplayName("path generates for nested unqualified PostgreSQL field") + def pathPostgresNestedUnqualified(): Unit = + assertEquals("data#>>'{My,Nested,Field}'", + Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct") + + @Test + @DisplayName("path generates for nested qualified PostgreSQL field") + def pathPostgresNestedQualified(): Unit = + assertEquals("bird.data#>>'{Nest,Away}'", + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not correct") + + @Test + @DisplayName("path generates for simple unqualified SQLite field") + def pathSQLiteSimpleUnqualified(): Unit = + assertEquals("data->>'SomethingCool'", + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct") + + @Test + @DisplayName("path generates for simple qualified SQLite field") + def pathSQLiteSimpleQualified(): Unit = + assertEquals("this.data->>'SomethingElse'", + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL), + "Path not correct") + + @Test + @DisplayName("path generates for nested unqualified SQLite field") + def pathSQLiteNestedUnqualified(): Unit = + assertEquals("data->'My'->'Nested'->>'Field'", + Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct") + + @Test + @DisplayName("path generates for nested qualified SQLite field") + def pathSQLiteNestedQualified(): Unit = + assertEquals("bird.data->'Nest'->>'Away'", + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL), + "Path not correct") + + @Test + @DisplayName("toWhere generates for exists w/o qualifier | PostgreSQL") + def toWhereExistsNoQualPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for exists w/o qualifier | SQLite") + def toWhereExistsNoQualSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for not-exists w/o qualifier | PostgreSQL") + def toWhereNotExistsNoQualPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for not-exists w/o qualifier | SQLite") + def toWhereNotExistsNoQualSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range | PostgreSQL") + def toWhereBetweenNoQualNumericPostgres(): Unit = + ForceDialect.postgres() + assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").toWhere, "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range | PostgreSQL") + def toWhereBetweenNoQualAlphaPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'city' BETWEEN :citymin AND :citymax", + Field.between("city", "Atlanta", "Chicago", ":city").toWhere, "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for BETWEEN w/o qualifier | SQLite") + def toWhereBetweenNoQualSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range | PostgreSQL") + def toWhereBetweenQualNumericPostgres(): Unit = + ForceDialect.postgres() + assertEquals("(test.data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").withQualifier("test").toWhere, "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range | PostgreSQL") + def toWhereBetweenQualAlphaPostgres(): Unit = + ForceDialect.postgres() + assertEquals("unit.data->>'city' BETWEEN :citymin AND :citymax", + Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for BETWEEN w/ qualifier | SQLite") + def toWhereBetweenQualSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("my.data->>'age' BETWEEN @agemin AND @agemax", + Field.between("age", 13, 17, "@age").withQualifier("my").toWhere, "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for IN/any, numeric values | PostgreSQL") + def toWhereAnyNumericPostgres(): Unit = + ForceDialect.postgres() + assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", + Field.any("even", List(2, 4, 6).asJava, ":nbr").toWhere, "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for IN/any, alphanumeric values | PostgreSQL") + def toWhereAnyAlphaPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'test' IN (:city_0, :city_1)", + Field.any("test", List("Atlanta", "Chicago").asJava, ":city").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for IN/any | SQLite") + def toWhereAnySQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'test' IN (:city_0, :city_1)", + Field.any("test", List("Atlanta", "Chicago").asJava, ":city").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for inArray | PostgreSQL") + def toWhereInArrayPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]", + Field.inArray("even", "tbl", List(2, 4, 6, 8).asJava, ":it").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for inArray | SQLite") + def toWhereInArraySQLite(): Unit = + ForceDialect.sqlite() + assertEquals("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, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for others w/o qualifier | PostgreSQL") + def toWhereOtherNoQualPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates for others w/o qualifier | SQLite") + def toWhereOtherNoQualSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates no-parameter w/ qualifier | PostgreSQL") + def toWhereNoParamWithQualPostgres(): Unit = + ForceDialect.postgres() + assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates no-parameter w/ qualifier | SQLite") + def toWhereNoParamWithQualSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere, + "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates parameter w/ qualifier | PostgreSQL") + def toWhereParamWithQualPostgres(): Unit = + ForceDialect.postgres() + assertEquals("(q.data->>'le_field')::numeric <= :it", + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere, "Field WHERE clause not generated correctly") + + @Test + @DisplayName("toWhere generates parameter w/ qualifier | SQLite") + def toWhereParamWithQualSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("q.data->>'le_field' <= :it", + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere, + "Field WHERE clause not generated correctly") + + // ~~~ STATIC CONSTRUCTOR TESTS ~~~ + + @Test + @DisplayName("equal constructs a field w/o parameter name") + def equalCtor(): Unit = + val field = Field.equal("Test", 14) + assertEquals("Test", field.getName, "Field name not filled correctly") + assertEquals(Op.EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals(14, field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("equal constructs a field w/ parameter name") + def equalParameterCtor(): Unit = + val field = Field.equal("Test", 14, ":w") + assertEquals("Test", field.getName, "Field name not filled correctly") + assertEquals(Op.EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals(14, field.getComparison.getValue, "Field comparison value not filled correctly") + assertEquals(":w", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("greater constructs a field w/o parameter name") + def greaterCtor(): Unit = + val field = Field.greater("Great", "night") + assertEquals("Great", field.getName, "Field name not filled correctly") + assertEquals(Op.GREATER, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("night", field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("greater constructs a field w/ parameter name") + def greaterParameterCtor(): Unit = + val field = Field.greater("Great", "night", ":yeah") + assertEquals("Great", field.getName, "Field name not filled correctly") + assertEquals(Op.GREATER, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("night", field.getComparison.getValue, "Field comparison value not filled correctly") + assertEquals(":yeah", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("greaterOrEqual constructs a field w/o parameter name") + def greaterOrEqualCtor(): Unit = + val field = Field.greaterOrEqual("Nice", 88L) + assertEquals("Nice", field.getName, "Field name not filled correctly") + assertEquals(Op.GREATER_OR_EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals(88L, field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("greaterOrEqual constructs a field w/ parameter name") + def greaterOrEqualParameterCtor(): Unit = + val field = Field.greaterOrEqual("Nice", 88L, ":nice") + assertEquals("Nice", field.getName, "Field name not filled correctly") + assertEquals(Op.GREATER_OR_EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals(88L, field.getComparison.getValue, "Field comparison value not filled correctly") + assertEquals(":nice", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("less constructs a field w/o parameter name") + def lessCtor(): Unit = + val field = Field.less("Lesser", "seven") + assertEquals("Lesser", field.getName, "Field name not filled correctly") + assertEquals(Op.LESS, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("seven", field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("less constructs a field w/ parameter name") + def lessParameterCtor(): Unit = + val field = Field.less("Lesser", "seven", ":max") + assertEquals("Lesser", field.getName, "Field name not filled correctly") + assertEquals(Op.LESS, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("seven", field.getComparison.getValue, "Field comparison value not filled correctly") + assertEquals(":max", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("lessOrEqual constructs a field w/o parameter name") + def lessOrEqualCtor(): Unit = + val field = Field.lessOrEqual("Nobody", "KNOWS") + assertEquals("Nobody", field.getName, "Field name not filled correctly") + assertEquals(Op.LESS_OR_EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("KNOWS", field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("lessOrEqual constructs a field w/ parameter name") + def lessOrEqualParameterCtor(): Unit = + val field = Field.lessOrEqual("Nobody", "KNOWS", ":nope") + assertEquals("Nobody", field.getName, "Field name not filled correctly") + assertEquals(Op.LESS_OR_EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("KNOWS", field.getComparison.getValue, "Field comparison value not filled correctly") + assertEquals(":nope", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("notEqual constructs a field w/o parameter name") + def notEqualCtor(): Unit = + val field = Field.notEqual("Park", "here") + assertEquals("Park", field.getName, "Field name not filled correctly") + assertEquals(Op.NOT_EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("here", field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("notEqual constructs a field w/ parameter name") + def notEqualParameterCtor(): Unit = + val field = Field.notEqual("Park", "here", ":now") + assertEquals("Park", field.getName, "Field name not filled correctly") + assertEquals(Op.NOT_EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("here", field.getComparison.getValue, "Field comparison value not filled correctly") + assertEquals(":now", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("between constructs a field w/o parameter name") + def betweenCtor(): Unit = + val field = Field.between("Age", 18, 49) + assertEquals("Age", field.getName, "Field name not filled correctly") + assertEquals(Op.BETWEEN, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals(18, field.getComparison.getValue.getFirst, "Field comparison min value not filled correctly") + assertEquals(49, field.getComparison.getValue.getSecond, "Field comparison max value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("between constructs a field w/ parameter name") + def betweenParameterCtor(): Unit = + val field = Field.between("Age", 18, 49, ":limit") + assertEquals("Age", field.getName, "Field name not filled correctly") + assertEquals(Op.BETWEEN, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals(18, field.getComparison.getValue.getFirst, "Field comparison min value not filled correctly") + assertEquals(49, field.getComparison.getValue.getSecond, "Field comparison max value not filled correctly") + assertEquals(":limit", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("any constructs a field w/o parameter name") + def anyCtor(): Unit = + val field = Field.any("Here", List(8, 16, 32).asJava) + assertEquals("Here", field.getName, "Field name not filled correctly") + assertEquals(Op.IN, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals(List(8, 16, 32).asJava, field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("any constructs a field w/ parameter name") + def anyParameterCtor(): Unit = + val field = Field.any("Here", List(8, 16, 32).asJava, ":list") + assertEquals("Here", field.getName, "Field name not filled correctly") + assertEquals(Op.IN, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals(List(8, 16, 32).asJava, field.getComparison.getValue, "Field comparison value not filled correctly") + assertEquals(":list", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("inArray constructs a field w/o parameter name") + def inArrayCtor(): Unit = + val field = Field.inArray("ArrayField", "table", List("z").asJava) + assertEquals("ArrayField", field.getName, "Field name not filled correctly") + assertEquals(Op.IN_ARRAY, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("table", field.getComparison.getValue.getFirst, "Field comparison table not filled correctly") + assertEquals(List("z").asJava, field.getComparison.getValue.getSecond, + "Field comparison values not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("inArray constructs a field w/ parameter name") + def inArrayParameterCtor(): Unit = + val field = Field.inArray("ArrayField", "table", List("z").asJava, ":a") + assertEquals("ArrayField", field.getName, "Field name not filled correctly") + assertEquals(Op.IN_ARRAY, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("table", field.getComparison.getValue.getFirst, "Field comparison table not filled correctly") + assertEquals(List("z").asJava, field.getComparison.getValue.getSecond, + "Field comparison values not filled correctly") + assertEquals(":a", field.getParameterName, "Field parameter name not filled correctly") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("exists constructs a field") + def existsCtor(): Unit = + val field = Field.exists("Groovy") + assertEquals("Groovy", field.getName, "Field name not filled correctly") + assertEquals(Op.EXISTS, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("", field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("notExists constructs a field") + def notExistsCtor(): Unit = + val field = Field.notExists("Groovy") + assertEquals("Groovy", field.getName, "Field name not filled correctly") + assertEquals(Op.NOT_EXISTS, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("", field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("named constructs a field") + def namedCtor(): Unit = + val field = Field.named("Tacos") + assertEquals("Tacos", field.getName, "Field name not filled correctly") + assertEquals(Op.EQUAL, field.getComparison.getOp, "Field comparison operation not filled correctly") + assertEquals("", field.getComparison.getValue, "Field comparison value not filled correctly") + assertNull(field.getParameterName, "The parameter name should have been null") + assertNull(field.getQualifier, "The qualifier should have been null") + + @Test + @DisplayName("static constructors fail for invalid parameter name") + def staticCtorsFailOnParamName(): Unit = + assertThrows(classOf[DocumentException], () => Field.equal("a", "b", "that ain't it, Jack...")) + + @Test + @DisplayName("nameToPath creates a simple PostgreSQL SQL name") + def nameToPathPostgresSimpleSQL(): Unit = + assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a simple SQLite SQL name") + def nameToPathSQLiteSimpleSQL(): Unit = + assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a nested PostgreSQL SQL name") + def nameToPathPostgresNestedSQL(): Unit = + assertEquals("data#>>'{A,Long,Path,to,the,Property}'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a nested SQLite SQL name") + def nameToPathSQLiteNestedSQL(): Unit = + assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a simple PostgreSQL JSON name") + def nameToPathPostgresSimpleJSON(): Unit = + assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a simple SQLite JSON name") + def nameToPathSQLiteSimpleJSON(): Unit = + assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a nested PostgreSQL JSON name") + def nameToPathPostgresNestedJSON(): Unit = + assertEquals("data#>'{A,Long,Path,to,the,Property}'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON), + "Path not constructed correctly") + + @Test + @DisplayName("nameToPath creates a nested SQLite JSON name") + def nameToPathSQLiteNestedJSON(): Unit = + assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'", + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON), + "Path not constructed correctly") diff --git a/src/scala/src/test/scala/FindQueryTest.scala b/src/scala/src/test/scala/FindQueryTest.scala new file mode 100644 index 0000000..a44b825 --- /dev/null +++ b/src/scala/src/test/scala/FindQueryTest.scala @@ -0,0 +1,79 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.FindQuery + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | FindQuery") +class FindQueryTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("all generates correctly") + def all(): Unit = + assertEquals(s"SELECT data FROM $TEST_TABLE", FindQuery.all(TEST_TABLE), "Find query not constructed correctly") + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + def byIdPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id", FindQuery.byId(TEST_TABLE), + "Find query not constructed correctly") + + @Test + @DisplayName("byId generates correctly | SQLite") + def byIdSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id", FindQuery.byId(TEST_TABLE), + "Find query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + def byFieldsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND (data->>'c')::numeric < :d", + FindQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b"), Field.less("c", 14, ":d")).asJava), + "Find query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | SQLite") + def byFieldsSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND data->>'c' < :d", + FindQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b"), Field.less("c", 14, ":d")).asJava), + "Find query not constructed correctly") + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + def byContainsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT data FROM $TEST_TABLE WHERE data @> :criteria", FindQuery.byContains(TEST_TABLE), + "Find query not constructed correctly") + + @Test + @DisplayName("byContains fails | SQLite") + def byContainsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => FindQuery.byContains(TEST_TABLE)) + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + def byJsonPathPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)", + FindQuery.byJsonPath(TEST_TABLE), "Find query not constructed correctly") + + @Test + @DisplayName("byJsonPath fails | SQLite") + def byJsonPathSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => FindQuery.byJsonPath(TEST_TABLE)) diff --git a/src/scala/src/test/scala/ForceDialect.scala b/src/scala/src/test/scala/ForceDialect.scala new file mode 100644 index 0000000..d233bd9 --- /dev/null +++ b/src/scala/src/test/scala/ForceDialect.scala @@ -0,0 +1,17 @@ +package solutions.bitbadger.documents.scala.tests + +import solutions.bitbadger.documents.Configuration + +/** + * These functions use a dummy connection string to force the given dialect for a given test + */ +object ForceDialect: + + def postgres(): Unit = + Configuration.setConnectionString(":postgresql:") + + def sqlite(): Unit = + Configuration.setConnectionString(":sqlite:") + + def none(): Unit = + Configuration.setConnectionString(null) diff --git a/src/scala/src/test/scala/IntIdClass.scala b/src/scala/src/test/scala/IntIdClass.scala new file mode 100644 index 0000000..c04bb11 --- /dev/null +++ b/src/scala/src/test/scala/IntIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +class IntIdClass(var id: Int) diff --git a/src/scala/src/test/scala/LongIdClass.scala b/src/scala/src/test/scala/LongIdClass.scala new file mode 100644 index 0000000..1f03ffa --- /dev/null +++ b/src/scala/src/test/scala/LongIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +class LongIdClass(var id: Long) diff --git a/src/scala/src/test/scala/OpTest.scala b/src/scala/src/test/scala/OpTest.scala new file mode 100644 index 0000000..b28d6fc --- /dev/null +++ b/src/scala/src/test/scala/OpTest.scala @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.Op + +@DisplayName("Scala | Op") +class OpTest: + + @Test + @DisplayName("EQUAL uses proper SQL") + def equalSQL(): Unit = + assertEquals("=", Op.EQUAL.getSql, "The SQL for equal is incorrect") + + @Test + @DisplayName("GREATER uses proper SQL") + def greaterSQL(): Unit = + assertEquals(">", Op.GREATER.getSql, "The SQL for greater is incorrect") + + @Test + @DisplayName("GREATER_OR_EQUAL uses proper SQL") + def greaterOrEqualSQL(): Unit = + assertEquals(">=", Op.GREATER_OR_EQUAL.getSql, "The SQL for greater-or-equal is incorrect") + + @Test + @DisplayName("LESS uses proper SQL") + def lessSQL(): Unit = + assertEquals("<", Op.LESS.getSql, "The SQL for less is incorrect") + + @Test + @DisplayName("LESS_OR_EQUAL uses proper SQL") + def lessOrEqualSQL(): Unit = + assertEquals("<=", Op.LESS_OR_EQUAL.getSql, "The SQL for less-or-equal is incorrect") + + @Test + @DisplayName("NOT_EQUAL uses proper SQL") + def notEqualSQL(): Unit = + assertEquals("<>", Op.NOT_EQUAL.getSql, "The SQL for not-equal is incorrect") + + @Test + @DisplayName("BETWEEN uses proper SQL") + def betweenSQL(): Unit = + assertEquals("BETWEEN", Op.BETWEEN.getSql, "The SQL for between is incorrect") + + @Test + @DisplayName("IN uses proper SQL") + def inSQL(): Unit = + assertEquals("IN", Op.IN.getSql, "The SQL for in is incorrect") + + @Test + @DisplayName("IN_ARRAY uses proper SQL") + def inArraySQL(): Unit = + assertEquals("??|", Op.IN_ARRAY.getSql, "The SQL for in-array is incorrect") + + @Test + @DisplayName("EXISTS uses proper SQL") + def existsSQL(): Unit = + assertEquals("IS NOT NULL", Op.EXISTS.getSql, "The SQL for exists is incorrect") + + @Test + @DisplayName("NOT_EXISTS uses proper SQL") + def notExistsSQL(): Unit = + assertEquals("IS NULL", Op.NOT_EXISTS.getSql, "The SQL for not-exists is incorrect") diff --git a/src/scala/src/test/scala/ParameterNameTest.scala b/src/scala/src/test/scala/ParameterNameTest.scala new file mode 100644 index 0000000..6231060 --- /dev/null +++ b/src/scala/src/test/scala/ParameterNameTest.scala @@ -0,0 +1,24 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.ParameterName + +@DisplayName("Scala | ParameterName") +class ParameterNameTest: + + @Test + @DisplayName("derive works when given existing names") + def withExisting(): Unit = + val names = ParameterName() + assertEquals(":taco", names.derive(":taco"), "Name should have been :taco") + assertEquals(":field0", names.derive(null), "Counter should not have advanced for named field") + + @Test + @DisplayName("derive works when given all anonymous fields") + def allAnonymous(): Unit = + val names = ParameterName() + assertEquals(":field0", names.derive(null), "Anonymous field name should have been returned") + assertEquals(":field1", names.derive(null), "Counter should have advanced from previous call") + assertEquals(":field2", names.derive(null), "Counter should have advanced from previous call") + assertEquals(":field3", names.derive(null), "Counter should have advanced from previous call") diff --git a/src/scala/src/test/scala/ParameterTest.scala b/src/scala/src/test/scala/ParameterTest.scala new file mode 100644 index 0000000..11524cf --- /dev/null +++ b/src/scala/src/test/scala/ParameterTest.scala @@ -0,0 +1,29 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Parameter, ParameterType} + +@DisplayName("Scala | Parameter") +class ParameterTest: + + @Test + @DisplayName("Construction with colon-prefixed name") + def ctorWithColon(): Unit = + val p = Parameter(":test", ParameterType.STRING, "ABC") + assertEquals(":test", p.getName, "Parameter name was incorrect") + assertEquals(ParameterType.STRING, p.getType, "Parameter type was incorrect") + assertEquals("ABC", p.getValue, "Parameter value was incorrect") + + @Test + @DisplayName("Construction with at-sign-prefixed name") + def ctorWithAtSign(): Unit = + val p = Parameter("@yo", ParameterType.NUMBER, null) + assertEquals("@yo", p.getName, "Parameter name was incorrect") + assertEquals(ParameterType.NUMBER, p.getType, "Parameter type was incorrect") + assertNull(p.getValue, "Parameter value was incorrect") + + @Test + @DisplayName("Construction fails with incorrect prefix") + def ctorFailsForPrefix(): Unit = + assertThrows(classOf[DocumentException], () => Parameter("it", ParameterType.JSON, "")) diff --git a/src/scala/src/test/scala/ParametersTest.scala b/src/scala/src/test/scala/ParametersTest.scala new file mode 100644 index 0000000..66e1093 --- /dev/null +++ b/src/scala/src/test/scala/ParametersTest.scala @@ -0,0 +1,100 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Field, Parameter, ParameterType} +import solutions.bitbadger.documents.scala.Parameters + +@DisplayName("Scala | Parameters") +class ParametersTest: + + /** + * Reset the dialect + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("nameFields works with no changes") + def nameFieldsNoChange(): Unit = + val fields = Field.equal("a", "", ":test") :: Field.exists("q") :: Field.equal("b", "", ":me") :: Nil + val named = Parameters.nameFields(fields).toList + assertEquals(fields.size, named.size, "There should have been 3 fields in the list") + assertSame(fields.head, named.head, "The first field should be the same") + assertSame(fields(1), named(1), "The second field should be the same") + assertSame(fields(2), named(2), "The third field should be the same") + + @Test + @DisplayName("nameFields works when changing fields") + def nameFieldsChange(): Unit = + val fields = Field.equal("a", "") :: Field.equal("e", "", ":hi") :: Field.equal("b", "") :: + Field.notExists("z") :: Nil + val named = Parameters.nameFields(fields).toList + assertEquals(fields.size, named.size, "There should have been 4 fields in the list") + assertNotSame(fields.head, named.head, "The first field should not be the same") + assertEquals(":field0", named.head.getParameterName, "First parameter name incorrect") + assertSame(fields(1), named(1), "The second field should be the same") + assertNotSame(fields(2), named(2), "The third field should not be the same") + assertEquals(":field1", named(2).getParameterName, "Third parameter name incorrect") + assertSame(fields(3), named(3), "The fourth field should be the same") + + @Test + @DisplayName("replaceNamesInQuery replaces successfully") + def replaceNamesInQuery(): Unit = + val parameters = Parameter(":data", ParameterType.JSON, "{}") :: + Parameter(":data_ext", ParameterType.STRING, "") :: Nil + val query = "SELECT data, data_ext FROM tbl WHERE data = :data AND data_ext = :data_ext AND more_data = :data" + assertEquals("SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?", + Parameters.replaceNamesInQuery(query, parameters), "Parameters not replaced correctly") + + @Test + @DisplayName("fieldNames generates a single parameter (PostgreSQL)") + def fieldNamesSinglePostgres(): Unit = + ForceDialect.postgres() + val nameParams = Parameters.fieldNames("test" :: Nil).toList + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name", nameParams.head.getName, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams.head.getType, "The parameter type is incorrect") + assertEquals("{test}", nameParams.head.getValue, "The parameter value is incorrect") + + @Test + @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") + def fieldNamesMultiplePostgres(): Unit = + ForceDialect.postgres() + val nameParams = Parameters.fieldNames("test" :: "this" :: "today" :: Nil).toList + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name", nameParams.head.getName, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams.head.getType, "The parameter type is incorrect") + assertEquals("{test,this,today}", nameParams.head.getValue, "The parameter value is incorrect") + + @Test + @DisplayName("fieldNames generates a single parameter (SQLite)") + def fieldNamesSingleSQLite(): Unit = + ForceDialect.sqlite() + val nameParams = Parameters.fieldNames("test" :: Nil).toList + assertEquals(1, nameParams.size, "There should be one name parameter") + assertEquals(":name0", nameParams.head.getName, "The parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams.head.getType, "The parameter type is incorrect") + assertEquals("test", nameParams.head.getValue, "The parameter value is incorrect") + + @Test + @DisplayName("fieldNames generates multiple parameters (SQLite)") + def fieldNamesMultipleSQLite(): Unit = + ForceDialect.sqlite() + val nameParams = Parameters.fieldNames("test" :: "this" :: "today" :: Nil).toList + assertEquals(3, nameParams.size, "There should be one name parameter") + assertEquals(":name0", nameParams.head.getName, "The first parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams.head.getType, "The first parameter type is incorrect") + assertEquals("test", nameParams.head.getValue, "The first parameter value is incorrect") + assertEquals(":name1", nameParams(1).getName, "The second parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams(1).getType, "The second parameter type is incorrect") + assertEquals("this", nameParams(1).getValue, "The second parameter value is incorrect") + assertEquals(":name2", nameParams(2).getName, "The third parameter name is incorrect") + assertEquals(ParameterType.STRING, nameParams(2).getType, "The third parameter type is incorrect") + assertEquals("today", nameParams(2).getValue, "The third parameter value is incorrect") + + @Test + @DisplayName("fieldNames fails if dialect not set") + def fieldNamesFails(): Unit = + assertThrows(classOf[DocumentException], () => Parameters.fieldNames(List())) diff --git a/src/scala/src/test/scala/PatchQueryTest.scala b/src/scala/src/test/scala/PatchQueryTest.scala new file mode 100644 index 0000000..92787dc --- /dev/null +++ b/src/scala/src/test/scala/PatchQueryTest.scala @@ -0,0 +1,72 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.PatchQuery + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | PatchQuery") +class PatchQueryTest: + + /** + * Reset the dialect + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + def byIdPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id", PatchQuery.byId(TEST_TABLE), + "Patch query not constructed correctly") + + @Test + @DisplayName("byId generates correctly | SQLite") + def byIdSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id", + PatchQuery.byId(TEST_TABLE), "Patch query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + def byFieldsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y", + PatchQuery.byFields(TEST_TABLE, List(Field.equal("z", "", ":y")).asJava), "Patch query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | SQLite") + def byFieldsSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y", + PatchQuery.byFields(TEST_TABLE, List(Field.equal("z", "", ":y")).asJava), "Patch query not constructed correctly") + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + def byContainsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria", + PatchQuery.byContains(TEST_TABLE), "Patch query not constructed correctly" ) + + @Test + @DisplayName("byContains fails | SQLite") + def byContainsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => PatchQuery.byContains(TEST_TABLE)) + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + def byJsonPathPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)", + PatchQuery.byJsonPath(TEST_TABLE), "Patch query not constructed correctly") + + @Test + @DisplayName("byJsonPath fails | SQLite") + def byJsonPathSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => PatchQuery.byJsonPath(TEST_TABLE)) diff --git a/src/scala/src/test/scala/QueryUtilsTest.scala b/src/scala/src/test/scala/QueryUtilsTest.scala new file mode 100644 index 0000000..77ca9fd --- /dev/null +++ b/src/scala/src/test/scala/QueryUtilsTest.scala @@ -0,0 +1,136 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{Dialect, Field, FieldMatch} +import solutions.bitbadger.documents.query.QueryUtils + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | Package Functions") +class QueryUtilsTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("statementWhere generates correctly") + def statementWhere(): Unit = + assertEquals("x WHERE y", QueryUtils.statementWhere("x", "y"), "Statements not combined correctly") + + @Test + @DisplayName("byId generates a numeric ID query | PostgreSQL") + def byIdNumericPostgres(): Unit = + ForceDialect.postgres() + assertEquals("test WHERE (data->>'id')::numeric = :id", QueryUtils.byId("test", 9)) + + @Test + @DisplayName("byId generates an alphanumeric ID query | PostgreSQL") + def byIdAlphaPostgres(): Unit = + ForceDialect.postgres() + assertEquals("unit WHERE data->>'id' = :id", QueryUtils.byId("unit", "18")) + + @Test + @DisplayName("byId generates ID query | SQLite") + def byIdSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("yo WHERE data->>'id' = :id", QueryUtils.byId("yo", 27)) + + @Test + @DisplayName("byFields generates default field query | PostgreSQL") + def byFieldsMultipleDefaultPostgres(): Unit = + ForceDialect.postgres() + assertEquals("this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value", + QueryUtils.byFields("this", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava)) + + @Test + @DisplayName("byFields generates default field query | SQLite") + def byFieldsMultipleDefaultSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("this WHERE data->>'a' = :the_a AND data->>'b' = :b_value", + QueryUtils.byFields("this", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava)) + + @Test + @DisplayName("byFields generates ANY field query | PostgreSQL") + def byFieldsMultipleAnyPostgres(): Unit = + ForceDialect.postgres() + assertEquals("that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value", + QueryUtils.byFields("that", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava, + FieldMatch.ANY)) + + @Test + @DisplayName("byFields generates ANY field query | SQLite") + def byFieldsMultipleAnySQLite(): Unit = + ForceDialect.sqlite() + assertEquals("that WHERE data->>'a' = :the_a OR data->>'b' = :b_value", + QueryUtils.byFields("that", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava, + FieldMatch.ANY)) + + @Test + @DisplayName("orderBy generates for no fields") + def orderByNone(): Unit = + assertEquals("", QueryUtils.orderBy(List().asJava, Dialect.POSTGRESQL), + "ORDER BY should have been blank (PostgreSQL)") + assertEquals("", QueryUtils.orderBy(List().asJava, Dialect.SQLITE), "ORDER BY should have been blank (SQLite)") + + @Test + @DisplayName("orderBy generates single, no direction | PostgreSQL") + def orderBySinglePostgres(): Unit = + assertEquals(" ORDER BY data->>'TestField'", + QueryUtils.orderBy(List(Field.named("TestField")).asJava, Dialect.POSTGRESQL), + "ORDER BY not constructed correctly") + + @Test + @DisplayName("orderBy generates single, no direction | SQLite") + def orderBySingleSQLite(): Unit = + assertEquals(" ORDER BY data->>'TestField'", + QueryUtils.orderBy(List(Field.named("TestField")).asJava, Dialect.SQLITE), + "ORDER BY not constructed correctly") + + @Test + @DisplayName("orderBy generates multiple with direction | PostgreSQL") + def orderByMultiplePostgres(): Unit = + assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + QueryUtils.orderBy( + List(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")).asJava, + Dialect.POSTGRESQL), + "ORDER BY not constructed correctly") + + @Test + @DisplayName("orderBy generates multiple with direction | SQLite") + def orderByMultipleSQLite(): Unit = + assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + QueryUtils.orderBy( + List(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")).asJava, + Dialect.SQLITE), + "ORDER BY not constructed correctly") + + @Test + @DisplayName("orderBy generates numeric ordering | PostgreSQL") + def orderByNumericPostgres(): Unit = + assertEquals(" ORDER BY (data->>'Test')::numeric", + QueryUtils.orderBy(List(Field.named("n:Test")).asJava, Dialect.POSTGRESQL), "ORDER BY not constructed correctly") + + @Test + @DisplayName("orderBy generates numeric ordering | SQLite") + def orderByNumericSQLite(): Unit = + assertEquals(" ORDER BY data->>'Test'", QueryUtils.orderBy(List(Field.named("n:Test")).asJava, Dialect.SQLITE), + "ORDER BY not constructed correctly") + + @Test + @DisplayName("orderBy generates case-insensitive ordering | PostgreSQL") + def orderByCIPostgres(): Unit = + assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", + QueryUtils.orderBy(List(Field.named("i:Test.Field DESC NULLS FIRST")).asJava, Dialect.POSTGRESQL), + "ORDER BY not constructed correctly") + + @Test + @DisplayName("orderBy generates case-insensitive ordering | SQLite") + def orderByCISQLite(): Unit = + assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", + QueryUtils.orderBy(List(Field.named("i:Test.Field ASC NULLS LAST")).asJava, Dialect.SQLITE), + "ORDER BY not constructed correctly") diff --git a/src/scala/src/test/scala/RemoveFieldsQueryTest.scala b/src/scala/src/test/scala/RemoveFieldsQueryTest.scala new file mode 100644 index 0000000..59296dc --- /dev/null +++ b/src/scala/src/test/scala/RemoveFieldsQueryTest.scala @@ -0,0 +1,82 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Field, Parameter, ParameterType} +import solutions.bitbadger.documents.query.RemoveFieldsQuery + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | RemoveFieldsQuery") +class RemoveFieldsQueryTest: + + /** + * Reset the dialect + */ + @AfterEach + def cleanUp(): Unit = + ForceDialect.none() + + @Test + @DisplayName("byId generates correctly | PostgreSQL") + def byIdPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data->>'id' = :id", + RemoveFieldsQuery.byId(TEST_TABLE, List(Parameter(":name", ParameterType.STRING, "{a,z}")).asJava), + "Remove Fields query not constructed correctly") + + @Test + @DisplayName("byId generates correctly | SQLite") + def byIdSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id", + RemoveFieldsQuery.byId(TEST_TABLE, + List(Parameter(":name0", ParameterType.STRING, "a"),Parameter(":name1", ParameterType.STRING, "z")).asJava), + "Remove Field query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | PostgreSQL") + def byFieldsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data->>'f' > :g", + RemoveFieldsQuery.byFields(TEST_TABLE, List(Parameter(":name", ParameterType.STRING, "{b,c}")).asJava, + List(Field.greater("f", "", ":g")).asJava), + "Remove Field query not constructed correctly") + + @Test + @DisplayName("byFields generates correctly | SQLite") + def byFieldsSQLite(): Unit = + ForceDialect.sqlite() + assertEquals(s"UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g", + RemoveFieldsQuery.byFields(TEST_TABLE, + List(Parameter(":name0", ParameterType.STRING, "b"), Parameter(":name1", ParameterType.STRING, "c")).asJava, + List(Field.greater("f", "", ":g")).asJava), + "Remove Field query not constructed correctly") + + @Test + @DisplayName("byContains generates correctly | PostgreSQL") + def byContainsPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data @> :criteria", + RemoveFieldsQuery.byContains(TEST_TABLE, List(Parameter(":name", ParameterType.STRING, "{m,n}")).asJava), + "Remove Field query not constructed correctly") + + @Test + @DisplayName("byContains fails | SQLite") + def byContainsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => RemoveFieldsQuery.byContains(TEST_TABLE, List().asJava)) + + @Test + @DisplayName("byJsonPath generates correctly | PostgreSQL") + def byJsonPathPostgres(): Unit = + ForceDialect.postgres() + assertEquals(s"UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)", + RemoveFieldsQuery.byJsonPath(TEST_TABLE, List(Parameter(":name", ParameterType.STRING, "{o,p}")).asJava), + "Remove Field query not constructed correctly") + + @Test + @DisplayName("byJsonPath fails | SQLite") + def byJsonPathSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => RemoveFieldsQuery.byJsonPath(TEST_TABLE, List().asJava)) diff --git a/src/scala/src/test/scala/ShortIdClass.scala b/src/scala/src/test/scala/ShortIdClass.scala new file mode 100644 index 0000000..35f2026 --- /dev/null +++ b/src/scala/src/test/scala/ShortIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +class ShortIdClass(var id: Short) diff --git a/src/scala/src/test/scala/StringIdClass.scala b/src/scala/src/test/scala/StringIdClass.scala new file mode 100644 index 0000000..2a97774 --- /dev/null +++ b/src/scala/src/test/scala/StringIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +class StringIdClass(var id: String) diff --git a/src/scala/src/test/scala/WhereTest.scala b/src/scala/src/test/scala/WhereTest.scala new file mode 100644 index 0000000..87e05e1 --- /dev/null +++ b/src/scala/src/test/scala/WhereTest.scala @@ -0,0 +1,141 @@ +package solutions.bitbadger.documents.scala.tests + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Field, FieldMatch} +import solutions.bitbadger.documents.query.Where + +import scala.jdk.CollectionConverters.* + +@DisplayName("Scala | Query | Where") +class WhereTest: + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + def cleanUp (): Unit = + ForceDialect.none() + + @Test + @DisplayName("byFields is blank when given no fields") + def byFieldsBlankIfEmpty(): Unit = + assertEquals("", Where.byFields(List().asJava)) + + @Test + @DisplayName("byFields generates one numeric field | PostgreSQL") + def byFieldsOneFieldPostgres(): Unit = + ForceDialect.postgres() + assertEquals("(data->>'it')::numeric = :that", Where.byFields(List(Field.equal("it", 9, ":that")).asJava)) + + @Test + @DisplayName("byFields generates one alphanumeric field | PostgreSQL") + def byFieldsOneAlphaFieldPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'it' = :that", Where.byFields(List(Field.equal("it", "", ":that")).asJava)) + + @Test + @DisplayName("byFields generates one field | SQLite") + def byFieldsOneFieldSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'it' = :that", Where.byFields(List(Field.equal("it", "", ":that")).asJava)) + + @Test + @DisplayName("byFields generates multiple fields w/ default match | PostgreSQL") + def byFieldsMultipleDefaultPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three", + Where.byFields( + List(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")).asJava)) + + @Test + @DisplayName("byFields generates multiple fields w/ default match | SQLite") + def byFieldsMultipleDefaultSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three", + Where.byFields( + List(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")).asJava)) + + @Test + @DisplayName("byFields generates multiple fields w/ ANY match | PostgreSQL") + def byFieldsMultipleAnyPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three", + Where.byFields( + List(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")).asJava, + FieldMatch.ANY)) + + @Test + @DisplayName("byFields generates multiple fields w/ ANY match | SQLite") + def byFieldsMultipleAnySQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three", + Where.byFields( + List(Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")).asJava, + FieldMatch.ANY)) + + @Test + @DisplayName("byId generates defaults for alphanumeric key | PostgreSQL") + def byIdDefaultAlphaPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'id' = :id", Where.byId()) + + @Test + @DisplayName("byId generates defaults for numeric key | PostgreSQL") + def byIdDefaultNumericPostgres(): Unit = + ForceDialect.postgres() + assertEquals("(data->>'id')::numeric = :id", Where.byId(":id", 5)) + + @Test + @DisplayName("byId generates defaults | SQLite") + def byIdDefaultSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'id' = :id", Where.byId()) + + @Test + @DisplayName("byId generates named ID | PostgreSQL") + def byIdDefaultNamedPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data->>'id' = :key", Where.byId(":key")) + + @Test + @DisplayName("byId generates named ID | SQLite") + def byIdDefaultNamedSQLite(): Unit = + ForceDialect.sqlite() + assertEquals("data->>'id' = :key", Where.byId(":key")) + + @Test + @DisplayName("jsonContains generates defaults | PostgreSQL") + def jsonContainsDefaultPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data @> :criteria", Where.jsonContains()) + + @Test + @DisplayName("jsonContains generates named parameter | PostgreSQL") + def jsonContainsNamedPostgres(): Unit = + ForceDialect.postgres() + assertEquals("data @> :it", Where.jsonContains(":it")) + + @Test + @DisplayName("jsonContains fails | SQLite") + def jsonContainsFailsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => Where.jsonContains()) + + @Test + @DisplayName("jsonPathMatches generates defaults | PostgreSQL") + def jsonPathMatchDefaultPostgres(): Unit = + ForceDialect.postgres() + assertEquals("jsonb_path_exists(data, :path::jsonpath)", Where.jsonPathMatches()) + + @Test + @DisplayName("jsonPathMatches generates named parameter | PostgreSQL") + def jsonPathMatchNamedPostgres(): Unit = + ForceDialect.postgres() + assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Where.jsonPathMatches(":jp")) + + @Test + @DisplayName("jsonPathMatches fails | SQLite") + def jsonPathFailsSQLite(): Unit = + ForceDialect.sqlite() + assertThrows(classOf[DocumentException], () => Where.jsonPathMatches()) diff --git a/src/scala/src/test/scala/integration/ArrayDocument.scala b/src/scala/src/test/scala/integration/ArrayDocument.scala new file mode 100644 index 0000000..83d1081 --- /dev/null +++ b/src/scala/src/test/scala/integration/ArrayDocument.scala @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.scala.tests.integration + +class ArrayDocument(val id: String = "", val values: List[String] = List()) + +object ArrayDocument: + + /** A set of documents used for integration tests */ + val testDocuments: List[ArrayDocument] = + ArrayDocument("first", "a" :: "b" :: "c" :: Nil) :: + ArrayDocument("second", "c" :: "d" :: "e" :: Nil) :: + ArrayDocument("third", "x" :: "y" :: "z" :: Nil) :: Nil diff --git a/src/scala/src/test/scala/integration/CountFunctions.scala b/src/scala/src/test/scala/integration/CountFunctions.scala new file mode 100644 index 0000000..0fce19b --- /dev/null +++ b/src/scala/src/test/scala/integration/CountFunctions.scala @@ -0,0 +1,42 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +object CountFunctions: + + def all(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should have been 5 documents in the table") + + def byFieldsNumeric(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(3L, db.conn.countByFields(TEST_TABLE, Field.between("numValue", 10, 20) :: Nil), + "There should have been 3 matching documents") + + def byFieldsAlpha(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(1L, db.conn.countByFields(TEST_TABLE, Field.between("value", "aardvark", "apple") :: Nil), + "There should have been 1 matching document") + + def byContainsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(2L, db.conn.countByContains(TEST_TABLE, Map.Map1("value", "purple")), + "There should have been 2 matching documents") + + def byContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(0L, db.conn.countByContains(TEST_TABLE, Map.Map1("value", "magenta")), + "There should have been no matching documents") + + def byJsonPathMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(2L, db.conn.countByJsonPath(TEST_TABLE, "$.numValue ? (@ < 5)"), + "There should have been 2 matching documents") + + def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(0L, db.conn.countByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no matching documents") diff --git a/src/scala/src/test/scala/integration/CustomFunctions.scala b/src/scala/src/test/scala/integration/CustomFunctions.scala new file mode 100644 index 0000000..b4acc2d --- /dev/null +++ b/src/scala/src/test/scala/integration/CustomFunctions.scala @@ -0,0 +1,109 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.query.{CountQuery, DeleteQuery, FindQuery, QueryUtils} +import solutions.bitbadger.documents.scala.Results +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE +import solutions.bitbadger.documents.{Configuration, Field, Parameter, ParameterType} + +import java.io.{PrintWriter, StringWriter} +import scala.jdk.CollectionConverters.* + +object CustomFunctions: + + def listEmpty(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + db.conn.deleteByFields(TEST_TABLE, Field.exists(Configuration.idField) :: Nil) + val result = db.conn.customList[JsonDocument](FindQuery.all(TEST_TABLE), Results.fromData) + assertEquals(0, result.size, "There should have been no results") + + def listAll(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val result = db.conn.customList[JsonDocument](FindQuery.all(TEST_TABLE), Results.fromData) + assertEquals(5, result.size, "There should have been 5 results") + + def jsonArrayEmpty(db: ThrowawayDatabase): Unit = + assertEquals(0L, db.conn.countAll(TEST_TABLE), "The test table should be empty") + assertEquals("[]", db.conn.customJsonArray(FindQuery.all(TEST_TABLE), Nil, Results.jsonFromData), + "An empty list was not represented correctly") + + def jsonArraySingle(db: ThrowawayDatabase): Unit = + db.conn.insert(TEST_TABLE, ArrayDocument("one", "2" :: "3" :: Nil)) + assertEquals(JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE), Nil, Results.jsonFromData), + "A single document list was not represented correctly") + + def jsonArrayMany(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + assertEquals(JsonFunctions.maybeJsonB("""[{"id":"first","values":["a","b","c"]},""" + + """{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]"""), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE) + QueryUtils.orderBy((Field.named("id") :: Nil).asJava), Nil, + Results.jsonFromData), + "A multiple document list was not represented correctly") + + def writeJsonArrayEmpty(db: ThrowawayDatabase): Unit = + assertEquals(0L, db.conn.countAll(TEST_TABLE), "The test table should be empty") + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), Nil, writer, Results.jsonFromData) + assertEquals("[]", output.toString, "An empty list was not represented correctly") + + def writeJsonArraySingle(db: ThrowawayDatabase): Unit = + db.conn.insert(TEST_TABLE, ArrayDocument("one", "2" :: "3" :: Nil)) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE), Nil, writer, Results.jsonFromData) + assertEquals(JsonFunctions.maybeJsonB("""[{"id":"one","values":["2","3"]}]"""), output.toString, + "A single document list was not represented correctly") + + def writeJsonArrayMany(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeCustomJsonArray(FindQuery.all(TEST_TABLE) + QueryUtils.orderBy((Field.named("id") :: Nil).asJava), Nil, + writer, Results.jsonFromData) + assertEquals(JsonFunctions.maybeJsonB("""[{"id":"first","values":["a","b","c"]},""" + + """{"id":"second","values":["c","d","e"]},{"id":"third","values":["x","y","z"]}]"""), + output.toString, "A multiple document list was not represented correctly") + + def singleNone(db: ThrowawayDatabase): Unit = + assertTrue(db.conn.customSingle[JsonDocument](FindQuery.all(TEST_TABLE), Results.fromData).isEmpty, + "There should not have been a document returned") + + def singleOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertTrue(db.conn.customSingle[JsonDocument](FindQuery.all(TEST_TABLE), Results.fromData).isDefined, + "There should have been a document returned") + + def jsonSingleNone(db: ThrowawayDatabase): Unit = + assertEquals("{}", db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), Nil, Results.jsonFromData), + "An empty document was not represented correctly") + + def jsonSingleOne(db: ThrowawayDatabase): Unit = + db.conn.insert(TEST_TABLE, ArrayDocument("me", "myself" :: "i" :: Nil)) + assertEquals(JsonFunctions.maybeJsonB("{\"id\":\"me\",\"values\":[\"myself\",\"i\"]}"), + db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), Nil, Results.jsonFromData), + "A single document was not represented correctly") + + def nonQueryChanges(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.customScalar[Long](CountQuery.all(TEST_TABLE), Results.toCount), + "There should have been 5 documents in the table") + db.conn.customNonQuery(s"DELETE FROM $TEST_TABLE") + assertEquals(0L, db.conn.customScalar[Long](CountQuery.all(TEST_TABLE), Results.toCount), + "There should have been no documents in the table") + + def nonQueryNoChanges(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.customScalar[Long](CountQuery.all(TEST_TABLE), Results.toCount), + "There should have been 5 documents in the table") + db.conn.customNonQuery(DeleteQuery.byId(TEST_TABLE, "eighty-two"), + Parameter(":id", ParameterType.STRING, "eighty-two") :: Nil) + assertEquals(5L, db.conn.customScalar[Long](CountQuery.all(TEST_TABLE), Results.toCount), + "There should still have been 5 documents in the table") + + def scalar(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(3L, db.conn.customScalar[Long](s"SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", Results.toCount), + "The number 3 should have been returned") diff --git a/src/scala/src/test/scala/integration/DefinitionFunctions.scala b/src/scala/src/test/scala/integration/DefinitionFunctions.scala new file mode 100644 index 0000000..80edff0 --- /dev/null +++ b/src/scala/src/test/scala/integration/DefinitionFunctions.scala @@ -0,0 +1,36 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +object DefinitionFunctions: + + def ensureTable(db: ThrowawayDatabase): Unit = + assertFalse(db.dbObjectExists("ensured"), "The 'ensured' table should not exist") + assertFalse(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should not exist") + db.conn.ensureTable("ensured") + assertTrue(db.dbObjectExists("ensured"), "The 'ensured' table should exist") + assertTrue(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should now exist") + + def ensureFieldIndex(db: ThrowawayDatabase): Unit = + assertFalse(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should not exist") + db.conn.ensureFieldIndex(TEST_TABLE, "test", "id" :: "category" :: Nil) + assertTrue(db.dbObjectExists(s"idx_${TEST_TABLE}_test"), "The test index should now exist") + + def ensureDocumentIndexFull(db: ThrowawayDatabase): Unit = + assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist") + db.conn.ensureTable("doc_table") + assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist") + assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist") + db.conn.ensureDocumentIndex("doc_table", DocumentIndex.FULL) + assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist") + + def ensureDocumentIndexOptimized(db: ThrowawayDatabase): Unit = + assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist") + db.conn.ensureTable("doc_table") + assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist") + assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist") + db.conn.ensureDocumentIndex("doc_table", DocumentIndex.OPTIMIZED) + assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist") diff --git a/src/scala/src/test/scala/integration/DeleteFunctions.scala b/src/scala/src/test/scala/integration/DeleteFunctions.scala new file mode 100644 index 0000000..faf1281 --- /dev/null +++ b/src/scala/src/test/scala/integration/DeleteFunctions.scala @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +object DeleteFunctions: + + def byIdMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteById(TEST_TABLE, "four") + assertEquals(4L, db.conn.countAll(TEST_TABLE), "There should now be 4 documents in the table") + + def byIdNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteById(TEST_TABLE, "negative four") + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + + def byFieldsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByFields(TEST_TABLE, Field.notEqual("value", "purple") :: Nil) + assertEquals(2L, db.conn.countAll(TEST_TABLE), "There should now be 2 documents in the table") + + def byFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByFields(TEST_TABLE, Field.equal("value", "crimson") :: Nil) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + + def byContainsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByContains(TEST_TABLE, Map.Map1("value", "purple")) + assertEquals(3L, db.conn.countAll(TEST_TABLE), "There should now be 3 documents in the table") + + def byContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByContains(TEST_TABLE, Map.Map1("target", "acquired")) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") + + def byJsonPathMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByJsonPath(TEST_TABLE, "$.value ? (@ == \"purple\")") + assertEquals(3L, db.conn.countAll(TEST_TABLE), "There should now be 3 documents in the table") + + def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should be 5 documents in the table") + db.conn.deleteByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)") + assertEquals(5L, db.conn.countAll(TEST_TABLE), "There should still be 5 documents in the table") diff --git a/src/scala/src/test/scala/integration/DocumentFunctions.scala b/src/scala/src/test/scala/integration/DocumentFunctions.scala new file mode 100644 index 0000000..33387e3 --- /dev/null +++ b/src/scala/src/test/scala/integration/DocumentFunctions.scala @@ -0,0 +1,108 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{AutoId, Configuration, DocumentException, Field} +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +object DocumentFunctions: + + import org.junit.jupiter.api.Assertions.assertThrows + + def insertDefault(db: ThrowawayDatabase): Unit = + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) + db.conn.insert(TEST_TABLE, doc) + val after = db.conn.findAll[JsonDocument](TEST_TABLE) + assertEquals(1, after.size, "There should be one document in the table") + assertEquals(doc, after.head, "The document should be what was inserted") + + def insertDupe(db: ThrowawayDatabase): Unit = + db.conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) + assertThrows(classOf[DocumentException], () => db.conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)), + "Inserting a document with a duplicate key should have thrown an exception") + + def insertNumAutoId(db: ThrowawayDatabase): Unit = + try + Configuration.autoIdStrategy = AutoId.NUMBER + Configuration.idField = "key" + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, NumIdDocument(0, "one")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "two")) + db.conn.insert(TEST_TABLE, NumIdDocument(77, "three")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "four")) + + val after = db.conn.findAll[NumIdDocument](TEST_TABLE, Field.named("key") :: Nil) + assertEquals(4, after.size, "There should have been 4 documents returned") + assertEquals("1|2|77|78", after.fold("") { (acc, item) => s"$acc|$item" }, "The IDs were not generated correctly") + finally + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idField = "id" + + def insertUUIDAutoId(db: ThrowawayDatabase): Unit = + try + Configuration.autoIdStrategy = AutoId.UUID + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, JsonDocument("")) + + val after = db.conn.findAll[JsonDocument](TEST_TABLE) + assertEquals(1, after.size, "There should have been 1 document returned") + assertEquals(32, after.head.id.length, "The ID was not generated correctly") + finally + Configuration.autoIdStrategy = AutoId.DISABLED + + def insertStringAutoId(db: ThrowawayDatabase): Unit = + try + Configuration.autoIdStrategy = AutoId.RANDOM_STRING + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, JsonDocument("")) + + Configuration.idStringLength = 21 + db.conn.insert(TEST_TABLE, JsonDocument("")) + + val after = db.conn.findAll[JsonDocument](TEST_TABLE) + assertEquals(2, after.size, "There should have been 2 documents returned") + assertEquals(16, after.head.id.length, "The first document's ID was not generated correctly") + assertEquals(21, after(1).id.length, "The second document's ID was not generated correctly") + finally + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idStringLength = 16 + + def saveMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + db.conn.save(TEST_TABLE, JsonDocument("two", numValue = 44)) + val tryDoc = db.conn.findById[String, JsonDocument](TEST_TABLE, "two") + assertTrue(tryDoc.isDefined, "There should have been a document returned") + val doc = tryDoc.get + assertEquals("two", doc.id, "An incorrect document was returned") + assertEquals("", doc.value, "The \"value\" field was not updated") + assertEquals(44, doc.numValue, "The \"numValue\" field was not updated") + assertNull(doc.sub, "The \"sub\" field was not updated") + + def saveNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + db.conn.save(TEST_TABLE, JsonDocument("test", sub = SubDocument("a", "b"))) + assertTrue(db.conn.findById[String, JsonDocument](TEST_TABLE, "test").isDefined, + "The test document should have been saved") + + def updateMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + db.conn.update(TEST_TABLE, "one", JsonDocument("one", "howdy", 8, SubDocument("y", "z"))) + val tryDoc = db.conn.findById[String, JsonDocument](TEST_TABLE, "one") + assertTrue(tryDoc.isDefined, "There should have been a document returned") + val doc = tryDoc.get + assertEquals("one", doc.id, "An incorrect document was returned") + assertEquals("howdy", doc.value, "The \"value\" field was not updated") + assertEquals(8, doc.numValue, "The \"numValue\" field was not updated") + assertNotNull(doc.sub, "The sub-document should not be null") + assertEquals("y", doc.sub.foo, "The sub-document \"foo\" field was not updated") + assertEquals("z", doc.sub.bar, "The sub-document \"bar\" field was not updated") + + def updateNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsById(TEST_TABLE, "two-hundred")) + db.conn.update(TEST_TABLE, "two-hundred", JsonDocument("two-hundred", numValue = 200)) + assertFalse(db.conn.existsById(TEST_TABLE, "two-hundred")) diff --git a/src/scala/src/test/scala/integration/ExistsFunctions.scala b/src/scala/src/test/scala/integration/ExistsFunctions.scala new file mode 100644 index 0000000..3f18932 --- /dev/null +++ b/src/scala/src/test/scala/integration/ExistsFunctions.scala @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +object ExistsFunctions: + + def byIdMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertTrue(db.conn.existsById(TEST_TABLE, "three"), "The document with ID \"three\" should exist") + + def byIdNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsById(TEST_TABLE, "seven"), "The document with ID \"seven\" should not exist") + + def byFieldsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertTrue(db.conn.existsByFields(TEST_TABLE, Field.equal("numValue", 10) :: Nil), + "Matching documents should have been found") + + def byFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, Field.equal("nothing", "none") :: Nil), + "No matching documents should have been found") + + def byContainsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertTrue(db.conn.existsByContains(TEST_TABLE, Map.Map1("value", "purple")), + "Matching documents should have been found") + + def byContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsByContains(TEST_TABLE, Map.Map1("value", "violet")), + "Matching documents should not have been found") + + def byJsonPathMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertTrue(db.conn.existsByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)"), + "Matching documents should have been found") + + def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10.1)"), + "Matching documents should not have been found") diff --git a/src/scala/src/test/scala/integration/FindFunctions.scala b/src/scala/src/test/scala/integration/FindFunctions.scala new file mode 100644 index 0000000..78e0d37 --- /dev/null +++ b/src/scala/src/test/scala/integration/FindFunctions.scala @@ -0,0 +1,216 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{Configuration, Field} +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +object FindFunctions: + + /** Generate IDs as a pipe-delimited string */ + private def docIds(docs: List[JsonDocument]) = + docs.map(_.id).reduce((ids, docId) => s"$ids|$docId") + + def allDefault(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5, db.conn.findAll[JsonDocument](TEST_TABLE).size, "There should have been 5 documents returned") + + def allAscending(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findAll[JsonDocument](TEST_TABLE, Field.named("id") :: Nil) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals("five|four|one|three|two", docIds(docs), "The documents were not ordered correctly") + + def allDescending(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findAll[JsonDocument](TEST_TABLE, Field.named("id DESC") :: Nil) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals("two|three|one|four|five", docIds(docs), "The documents were not ordered correctly") + + def allNumOrder(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findAll[JsonDocument](TEST_TABLE, + Field.named("sub.foo NULLS LAST") :: Field.named("n:numValue") :: Nil) + assertEquals(5, docs.size, "There should have been 5 documents returned") + assertEquals("two|four|one|three|five", docIds(docs), "The documents were not ordered correctly") + + def allEmpty(db: ThrowawayDatabase): Unit = + assertEquals(0, db.conn.findAll[JsonDocument](TEST_TABLE).size, "There should have been no documents returned") + + def byIdString(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findById[String, JsonDocument](TEST_TABLE, "two") + assertTrue(doc.isDefined, "The document should have been returned") + assertEquals("two", doc.get.id, "An incorrect document was returned") + + def byIdNumber(db: ThrowawayDatabase): Unit = + Configuration.idField = "key" + try + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + val doc = db.conn.findById[Int, NumIdDocument](TEST_TABLE, 18) + assertTrue(doc.isDefined, "The document should have been returned") + finally + Configuration.idField = "id" + + def byIdNotFound(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.findById[String, JsonDocument](TEST_TABLE, "x").isDefined, + "There should have been no document returned") + + def byFieldsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findByFields[JsonDocument](TEST_TABLE, + Field.any("value", ("blue" :: "purple" :: Nil).asJava) :: Nil, orderBy = Field.exists("sub") :: Nil) + assertEquals(1, docs.size, "There should have been a document returned") + assertEquals("four", docs.head.id, "The incorrect document was returned") + + def byFieldsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findByFields[JsonDocument](TEST_TABLE, Field.equal("value", "purple") :: Nil, + orderBy = Field.named("id") :: Nil) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("five|four", docIds(docs), "The documents were not ordered correctly") + + def byFieldsMatchNumIn(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findByFields[JsonDocument](TEST_TABLE, + Field.any("numValue", (2 :: 4 :: 6 :: 8 :: Nil).asJava) :: Nil) + assertEquals(1, docs.size, "There should have been a document returned") + assertEquals("three", docs.head.id, "The incorrect document was returned") + + def byFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(0, db.conn.findByFields[JsonDocument](TEST_TABLE, Field.greater("numValue", 100) :: Nil).size, + "There should have been no documents returned") + + def byFieldsMatchInArray(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + val docs = db.conn.findByFields[ArrayDocument](TEST_TABLE, + Field.inArray("values", TEST_TABLE, ("c" :: Nil).asJava) :: Nil) + assertEquals(2, docs.size, "There should have been two documents returned") + assertTrue(("first" :: "second" :: Nil).contains(docs.head.id), + s"An incorrect document was returned (${docs.head.id}") + assertTrue(("first" :: "second" :: Nil).contains(docs(1).id), s"An incorrect document was returned (${docs(1).id})") + + def byFieldsNoMatchInArray(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + assertEquals(0, + db.conn.findByFields[ArrayDocument](TEST_TABLE, + Field.inArray("values", TEST_TABLE, ("j" :: Nil).asJava) :: Nil).size, + "There should have been no documents returned") + + def byContainsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findByContains[JsonDocument, Map.Map1[String, String]](TEST_TABLE, Map.Map1("value", "purple")) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertTrue(("four" :: "five" :: Nil).contains(docs.head.id), + s"An incorrect document was returned (${docs.head.id})") + assertTrue(("four" :: "five" :: Nil).contains(docs(1).id), s"An incorrect document was returned (${docs(1).id})") + + def byContainsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findByContains[JsonDocument, Map.Map1[String, Map.Map1[String, String]]](TEST_TABLE, + Map.Map1("sub", Map.Map1("foo", "green")), Field.named("value") :: Nil) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("two|four", docIds(docs), "The documents were not ordered correctly") + + def byContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(0, + db.conn.findByContains[JsonDocument, Map.Map1[String, String]](TEST_TABLE, Map.Map1("value", "indigo")).size, + "There should have been no documents returned") + + def byJsonPathMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findByJsonPath[JsonDocument](TEST_TABLE, "$.numValue ? (@ > 10)") + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertTrue(("four" :: "five" :: Nil).contains(docs.head.id), + s"An incorrect document was returned (${docs.head.id})") + assertTrue(("four" :: "five" :: Nil).contains(docs(1).id), s"An incorrect document was returned (${docs(1).id})") + + def byJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val docs = db.conn.findByJsonPath[JsonDocument](TEST_TABLE, "$.numValue ? (@ > 10)", Field.named("id") :: Nil) + assertEquals(2, docs.size, "There should have been 2 documents returned") + assertEquals("five|four", docIds(docs), "The documents were not ordered correctly") + + def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(0, db.conn.findByJsonPath[JsonDocument](TEST_TABLE, "$.numValue ? (@ > 100)").size, + "There should have been no documents returned") + + def firstByFieldsMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByFields[JsonDocument](TEST_TABLE, Field.equal("value", "another") :: Nil) + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("two", doc.get.id, "The incorrect document was returned") + + def firstByFieldsMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByFields[JsonDocument](TEST_TABLE, Field.equal("sub.foo", "green") :: Nil) + assertTrue(doc.isDefined, "There should have been a document returned") + assertTrue(("two" :: "four" :: Nil).contains(doc.get.id), s"An incorrect document was returned (${doc.get.id})") + + def firstByFieldsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByFields[JsonDocument](TEST_TABLE, Field.equal("sub.foo", "green") :: Nil, + orderBy = Field.named("n:numValue DESC") :: Nil) + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("four", doc.get.id, "An incorrect document was returned") + + def firstByFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.findFirstByFields[JsonDocument](TEST_TABLE, Field.equal("value", "absent") :: Nil).isDefined, + "There should have been no document returned") + + def firstByContainsMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByContains[JsonDocument, Map.Map1[String, String]](TEST_TABLE, + Map.Map1("value", "FIRST!")) + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("one", doc.get.id, "An incorrect document was returned") + + def firstByContainsMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByContains[JsonDocument, Map.Map1[String, String]](TEST_TABLE, + Map.Map1("value", "purple")) + assertTrue(doc.isDefined, "There should have been a document returned") + assertTrue(("four" :: "five" :: Nil).contains(doc.get.id), s"An incorrect document was returned (${doc.get.id})") + + def firstByContainsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByContains[JsonDocument, Map.Map1[String, String]](TEST_TABLE, + Map.Map1("value", "purple"), Field.named("sub.bar NULLS FIRST") :: Nil) + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("five", doc.get.id, "An incorrect document was returned") + + def firstByContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.findFirstByContains[JsonDocument, Map.Map1[String, String]](TEST_TABLE, + Map.Map1("value", "indigo")).isDefined, "There should have been no document returned") + + def firstByJsonPathMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath[JsonDocument](TEST_TABLE, "$.numValue ? (@ == 10)") + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("two", doc.get.id, "An incorrect document was returned") + + def firstByJsonPathMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath[JsonDocument](TEST_TABLE, "$.numValue ? (@ > 10)") + assertTrue(doc.isDefined, "There should have been a document returned") + assertTrue(("four" :: "five" :: Nil).contains(doc.get.id), s"An incorrect document was returned (${doc.get.id})") + + def firstByJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val doc = db.conn.findFirstByJsonPath[JsonDocument](TEST_TABLE, "$.numValue ? (@ > 10)", + Field.named("id DESC") :: Nil) + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("four", doc.get.id, "An incorrect document was returned") + + def firstByJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.findFirstByJsonPath[JsonDocument](TEST_TABLE, "$.numValue ? (@ > 100)").isDefined, + "There should have been no document returned") diff --git a/src/scala/src/test/scala/integration/JacksonDocumentSerializer.scala b/src/scala/src/test/scala/integration/JacksonDocumentSerializer.scala new file mode 100644 index 0000000..6cac9df --- /dev/null +++ b/src/scala/src/test/scala/integration/JacksonDocumentSerializer.scala @@ -0,0 +1,17 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import solutions.bitbadger.documents.DocumentSerializer +import com.fasterxml.jackson.databind.ObjectMapper + +/** + * A JSON serializer using Jackson's default options + */ +class JacksonDocumentSerializer extends DocumentSerializer: + + private val mapper = ObjectMapper() + + override def serialize[TDoc](document: TDoc): String = + mapper.writeValueAsString(document) + + override def deserialize[TDoc](json: String, clazz: Class[TDoc]): TDoc = + mapper.readValue(json, clazz) diff --git a/src/scala/src/test/scala/integration/JsonDocument.scala b/src/scala/src/test/scala/integration/JsonDocument.scala new file mode 100644 index 0000000..b5ef2e7 --- /dev/null +++ b/src/scala/src/test/scala/integration/JsonDocument.scala @@ -0,0 +1,34 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import solutions.bitbadger.documents.scala.extensions.insert +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +class JsonDocument(val id: String = "", val value: String = "", val numValue: Int = 0, val sub: SubDocument = null) + +object JsonDocument: + + /** Documents to use for testing */ + private val testDocuments = List( + JsonDocument("one", "FIRST!", 0, null), + JsonDocument("two", "another", 10, SubDocument("green", "blue")), + JsonDocument("three", "", 4, null), + JsonDocument("four", "purple", 17, SubDocument("green", "red")), + JsonDocument("five", "purple", 18, null)) + + def load(db: ThrowawayDatabase, tableName: String = TEST_TABLE): Unit = + testDocuments.foreach { it => db.conn.insert(tableName, it) } + + /** Document ID `one` as a JSON string */ + val one = """{"id":"one","value":"FIRST!","numValue":0,"sub":null}""" + + /** Document ID `two` as a JSON string */ + val two = """{"id":"two","value":"another","numValue":10,"sub":{"foo":"green","bar":"blue"}}""" + + /** Document ID `three` as a JSON string */ + val three = """{"id":"three","value":"","numValue":4,"sub":null}""" + + /** Document ID `four` as a JSON string */ + val four = """{"id":"four","value":"purple","numValue":17,"sub":{"foo":"green","bar":"red"}}""" + + /** Document ID `five` as a JSON string */ + val five = """{"id":"five","value":"purple","numValue":18,"sub":null}""" diff --git a/src/scala/src/test/scala/integration/JsonFunctions.scala b/src/scala/src/test/scala/integration/JsonFunctions.scala new file mode 100644 index 0000000..7a4c3e8 --- /dev/null +++ b/src/scala/src/test/scala/integration/JsonFunctions.scala @@ -0,0 +1,572 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{Configuration, Dialect, Field, FieldMatch} +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +import java.io.{PrintWriter, StringWriter} +import scala.jdk.CollectionConverters.* + +/** + * Tests for the JSON-returning functions + * + * NOTE: PostgreSQL JSONB columns do not preserve the original JSON with which a document was stored. These tests are + * the most complex within the library, as they have split testing based on the backing data store. The PostgreSQL tests + * check IDs (and, in the case of ordered queries, which ones occur before which others) vs. the entire JSON string. + * Meanwhile, SQLite stores JSON as text, and will return exactly the JSON it was given when it was originally written. + * These tests can ensure the expected round-trip of the entire JSON string. + */ +object JsonFunctions: + + /** + * PostgreSQL, when returning JSONB as a string, has spaces after commas and colons delineating fields and values. + * This function will do a crude string replacement to match the target string based on the dialect being tested. + * + * @param json The JSON which should be returned + * @return The actual expected JSON based on the database being tested + */ + def maybeJsonB(json: String): String = + Configuration.dialect() match + case Dialect.SQLITE => json + case Dialect.POSTGRESQL => json.replace("\":", "\": ").replace(",\"", ", \"") + + /** + * Create a snippet of JSON to find a document ID + * + * @param id The ID of the document + * @return A connection-aware ID to check for presence and positioning + */ + private def docId(id: String): String = + maybeJsonB(s"""{"id":"$id"""") + + private def checkAllDefault(json: String): Unit = + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + Configuration.dialect() match + case Dialect.SQLITE => + assertTrue(json.contains(JsonDocument.one), s"Document 'one' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.two), s"Document 'two' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.three), s"Document 'three' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.four), s"Document 'four' not found in JSON ($json)") + assertTrue(json.contains(JsonDocument.five), s"Document 'five' not found in JSON ($json)") + case Dialect.POSTGRESQL => + assertTrue(json.contains(docId("one")), s"Document 'one' not found in JSON ($json)") + assertTrue(json.contains(docId("two")), s"Document 'two' not found in JSON ($json)") + assertTrue(json.contains(docId("three")), s"Document 'three' not found in JSON ($json)") + assertTrue(json.contains(docId("four")), s"Document 'four' not found in JSON ($json)") + assertTrue(json.contains(docId("five")), s"Document 'five' not found in JSON ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def allDefault(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkAllDefault(db.conn.jsonAll(TEST_TABLE)) + + def writeAllDefault(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonAll(TEST_TABLE, writer) + checkAllDefault(output.toString) + + private def checkAllEmpty(json: String): Unit = + assertEquals("[]", json, "There should have been no documents returned") + + def allEmpty(db: ThrowawayDatabase): Unit = + checkAllEmpty(db.conn.jsonAll(TEST_TABLE)) + + def writeAllEmpty(db: ThrowawayDatabase): Unit = + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonAll(TEST_TABLE, writer) + checkAllEmpty(output.toString) + + private def checkByIdString(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => assertEquals(JsonDocument.two, json, "An incorrect document was returned") + case Dialect.POSTGRESQL => assertTrue(json.contains(docId("two")), s"An incorrect document was returned ($json)") + + def byIdString(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByIdString(db.conn.jsonById(TEST_TABLE, "two")) + + def writeByIdString(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, "two") + checkByIdString(output.toString) + + private def checkByIdNumber(json: String): Unit = + assertEquals(maybeJsonB("""{"key":18,"text":"howdy"}"""), json, "The document should have been found by numeric ID") + + def byIdNumber(db: ThrowawayDatabase): Unit = + Configuration.idField = "key" + try + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + checkByIdNumber(db.conn.jsonById(TEST_TABLE, 18)) + finally + Configuration.idField = "id" + + def writeByIdNumber(db: ThrowawayDatabase): Unit = + Configuration.idField = "key" + try + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, 18) + checkByIdNumber(output.toString) + finally + Configuration.idField = "id" + + private def checkByIdNotFound(json: String): Unit = + assertEquals("{}", json, "There should have been no document returned") + + def byIdNotFound(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByIdNotFound(db.conn.jsonById(TEST_TABLE, "x")) + + def writeByIdNotFound(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonById(TEST_TABLE, writer, "x") + checkByIdNotFound(output.toString) + + private def checkByFieldsMatch(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => + assertEquals(s"[${JsonDocument.four}]", json, "The incorrect document was returned") + case Dialect.POSTGRESQL => + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + assertTrue(json.contains(docId("four")), s"The incorrect document was returned ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def byFieldsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByFieldsMatch(db.conn.jsonByFields(TEST_TABLE, + Field.any("value", ("blue" :: "purple" :: Nil).asJava) :: Field.exists("sub") :: Nil, Some(FieldMatch.ALL))) + + def writeByFieldsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, + Field.any("value", ("blue" :: "purple" :: Nil).asJava) :: Field.exists("sub") :: Nil, Some(FieldMatch.ALL)) + checkByFieldsMatch(output.toString) + + private def checkByFieldsMatchOrdered(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => + assertEquals(s"[${JsonDocument.five},${JsonDocument.four}]", json, "The documents were not ordered correctly") + case Dialect.POSTGRESQL => + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, s"Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, s"Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, s"Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def byFieldsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByFieldsMatchOrdered(db.conn.jsonByFields(TEST_TABLE, Field.equal("value", "purple") :: Nil, None, + Field.named("id") :: Nil)) + + def writeByFieldsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, Field.equal("value", "purple") :: Nil, None, Field.named("id") :: Nil) + checkByFieldsMatchOrdered(output.toString) + + private def checkByFieldsMatchNumIn(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => + assertEquals(s"[${JsonDocument.three}]", json, "The incorrect document was returned") + case Dialect.POSTGRESQL => + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + assertTrue(json.contains(docId("three")), s"The incorrect document was returned ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def byFieldsMatchNumIn(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByFieldsMatchNumIn(db.conn.jsonByFields(TEST_TABLE, + Field.any("numValue", (2 :: 4 :: 6 :: 8 :: Nil).asJava) :: Nil)) + + def writeByFieldsMatchNumIn(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, Field.any("numValue", (2 :: 4 :: 6 :: 8 :: Nil).asJava) :: Nil) + checkByFieldsMatchNumIn(output.toString) + + private def checkByFieldsNoMatch(json: String): Unit = + assertEquals("[]", json, "There should have been no documents returned") + + def byFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByFieldsNoMatch(db.conn.jsonByFields(TEST_TABLE, Field.greater("numValue", 100) :: Nil)) + + def writeByFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, Field.greater("numValue", 100) :: Nil) + checkByFieldsNoMatch(output.toString) + + private def checkByFieldsMatchInArray(json: String): Unit = + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + assertTrue(json.contains(docId("first")), s"The 'first' document was not found ($json)") + assertTrue(json.contains(docId("second")), s"The 'second' document was not found ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def byFieldsMatchInArray(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + checkByFieldsMatchInArray(db.conn.jsonByFields(TEST_TABLE, + Field.inArray("values", TEST_TABLE, ("c" :: Nil).asJava) :: Nil)) + + def writeByFieldsMatchInArray(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, Field.inArray("values", TEST_TABLE, ("c" :: Nil).asJava) :: Nil) + checkByFieldsMatchInArray(output.toString) + + private def checkByFieldsNoMatchInArray(json: String): Unit = + assertEquals("[]", json, "There should have been no documents returned") + + def byFieldsNoMatchInArray(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + checkByFieldsNoMatchInArray(db.conn.jsonByFields(TEST_TABLE, + Field.inArray("values", TEST_TABLE, ("j" :: Nil).asJava) :: Nil)) + + def writeByFieldsNoMatchInArray(db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByFields(TEST_TABLE, writer, Field.inArray("values", TEST_TABLE, ("j" :: Nil).asJava) :: Nil) + checkByFieldsNoMatchInArray(output.toString) + + private def checkByContainsMatch(json: String): Unit = + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + Configuration.dialect() match + case Dialect.SQLITE => + assertTrue(json.contains(JsonDocument.four), s"Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), s"Document 'five' not found ($json)") + case Dialect.POSTGRESQL => + assertTrue(json.contains(docId("four")), s"Document 'four' not found ($json)") + assertTrue(json.contains(docId("five")), s"Document 'five' not found ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def byContainsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByContainsMatch(db.conn.jsonByContains(TEST_TABLE, Map.Map1("value", "purple"))) + + def writeByContainsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains(TEST_TABLE, writer, Map.Map1("value", "purple")) + checkByContainsMatch(output.toString) + + private def checkByContainsMatchOrdered(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => + assertEquals(s"[${JsonDocument.two},${JsonDocument.four}]", json, "The documents were not ordered correctly") + case Dialect.POSTGRESQL => + val twoIdx = json.indexOf(docId("two")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + assertTrue(twoIdx >= 0, s"Document 'two' not found ($json)") + assertTrue(fourIdx >= 0, s"Document 'four' not found ($json)") + assertTrue(twoIdx < fourIdx, s"Document 'two' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def byContainsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByContainsMatchOrdered(db.conn.jsonByContains(TEST_TABLE, Map.Map1("sub", Map.Map1("foo", "green")), + Field.named("value") :: Nil)) + + def writeByContainsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains(TEST_TABLE, writer, Map.Map1("sub", Map.Map1("foo", "green")), + Field.named("value") :: Nil) + checkByContainsMatchOrdered(output.toString) + + private def checkByContainsNoMatch(json: String): Unit = + assertEquals("[]", json, "There should have been no documents returned") + + def byContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByContainsNoMatch(db.conn.jsonByContains(TEST_TABLE, Map.Map1("value", "indigo"))) + + def writeByContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByContains(TEST_TABLE, writer, Map.Map1("value", "indigo")) + checkByContainsNoMatch(output.toString) + + private def checkByJsonPathMatch(json: String): Unit = + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + Configuration.dialect() match + case Dialect.SQLITE => + assertTrue(json.contains(JsonDocument.four), s"Document 'four' not found ($json)") + assertTrue(json.contains(JsonDocument.five), s"Document 'five' not found ($json)") + case Dialect.POSTGRESQL => + assertTrue(json.contains(docId("four")), s"Document 'four' not found ($json)") + assertTrue(json.contains(docId("five")), s"Document 'five' not found ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def byJsonPathMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByJsonPathMatch(db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)")) + + def writeByJsonPathMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)") + checkByJsonPathMatch(output.toString) + + private def checkByJsonPathMatchOrdered(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => + assertEquals(s"[${JsonDocument.five},${JsonDocument.four}]", json, "The documents were not ordered correctly") + case Dialect.POSTGRESQL => + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") + assertTrue(fiveIdx >= 0, s"Document 'five' not found ($json)") + assertTrue(fourIdx >= 0, s"Document 'four' not found ($json)") + assertTrue(fiveIdx < fourIdx, s"Document 'five' should have been before 'four' ($json)") + assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") + + def byJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByJsonPathMatchOrdered(db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", Field.named("id") :: Nil)) + + def writeByJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)", Field.named("id") :: Nil) + checkByJsonPathMatchOrdered(output.toString) + + private def checkByJsonPathNoMatch(json: String): Unit = + assertEquals("[]", json, "There should have been no documents returned") + + def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkByJsonPathNoMatch(db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)")) + + def writeByJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 100)") + checkByJsonPathNoMatch(output.toString) + + private def checkFirstByFieldsMatchOne(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => assertEquals(JsonDocument.two, json, "The incorrect document was returned") + case Dialect.POSTGRESQL => assertTrue(json.contains(docId("two")), s"The incorrect document was returned ($json)") + + def firstByFieldsMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByFieldsMatchOne(db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("value", "another") :: Nil)) + + def writeFirstByFieldsMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, Field.equal("value", "another") :: Nil) + checkFirstByFieldsMatchOne(output.toString) + + private def checkFirstByFieldsMatchMany(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => + assertTrue(json.contains(JsonDocument.two) || json.contains(JsonDocument.four), + s"Expected document 'two' or 'four' ($json)") + case Dialect.POSTGRESQL => + assertTrue(json.contains(docId("two")) || json.contains(docId("four")), + s"Expected document 'two' or 'four' ($json)") + + def firstByFieldsMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByFieldsMatchMany(db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("sub.foo", "green") :: Nil)) + + def writeFirstByFieldsMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, Field.equal("sub.foo", "green") :: Nil) + checkFirstByFieldsMatchMany(output.toString) + + private def checkFirstByFieldsMatchOrdered(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => assertEquals(JsonDocument.four, json, "An incorrect document was returned") + case Dialect.POSTGRESQL => assertTrue(json.contains(docId("four")), s"An incorrect document was returned ($json)") + + def firstByFieldsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByFieldsMatchOrdered(db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("sub.foo", "green") :: Nil, None, + Field.named("n:numValue DESC") :: Nil)) + + def writeFirstByFieldsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, Field.equal("sub.foo", "green") :: Nil, None, + Field.named("n:numValue DESC") :: Nil) + + private def checkFirstByFieldsNoMatch(json: String): Unit = + assertEquals("{}", json, "There should have been no document returned") + + def firstByFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByFieldsNoMatch(db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("value", "absent") :: Nil)) + + def writeFirstByFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByFields(TEST_TABLE, writer, Field.equal("value", "absent") :: Nil) + checkFirstByFieldsNoMatch(output.toString) + + private def checkFirstByContainsMatchOne(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => assertEquals(JsonDocument.one, json, "An incorrect document was returned") + case Dialect.POSTGRESQL => assertTrue(json.contains(docId("one")), s"An incorrect document was returned ($json)") + + def firstByContainsMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByContainsMatchOne(db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "FIRST!"))) + + def writeFirstByContainsMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, Map.Map1("value", "FIRST!")) + checkFirstByContainsMatchOne(output.toString) + + private def checkFirstByContainsMatchMany(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => + assertTrue(json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + s"Expected document 'four' or 'five' ($json)") + case Dialect.POSTGRESQL => + assertTrue(json.contains(docId("four")) || json.contains(docId("five")), + s"Expected document 'four' or 'five' ($json)") + + def firstByContainsMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByContainsMatchMany(db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "purple"))) + + def writeFirstByContainsMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, Map.Map1("value", "purple")) + checkFirstByContainsMatchMany(output.toString) + + private def checkFirstByContainsMatchOrdered(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => assertEquals(JsonDocument.five, json, "An incorrect document was returned") + case Dialect.POSTGRESQL => assertTrue(json.contains(docId("five")), s"An incorrect document was returned ($json)") + + def firstByContainsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByContainsMatchOrdered(db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "purple"), + Field.named("sub.bar NULLS FIRST") :: Nil)) + + def writeFirstByContainsMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, Map.Map1("value", "purple"), + Field.named("sub.bar NULLS FIRST") :: Nil) + checkFirstByContainsMatchOrdered(output.toString) + + private def checkFirstByContainsNoMatch(json: String): Unit = + assertEquals("{}", json, "There should have been no document returned") + + def firstByContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByContainsNoMatch(db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "indigo"))) + + def writeFirstByContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByContains(TEST_TABLE, writer, Map.Map1("value", "indigo")) + checkFirstByContainsNoMatch(output.toString) + + private def checkFirstByJsonPathMatchOne(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => assertEquals(JsonDocument.two, json, "An incorrect document was returned") + case Dialect.POSTGRESQL => assertTrue(json.contains(docId("two")), s"An incorrect document was returned ($json)") + + def firstByJsonPathMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByJsonPathMatchOne(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)")) + + def writeFirstByJsonPathMatchOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ == 10)") + checkFirstByJsonPathMatchOne(output.toString) + + private def checkFirstByJsonPathMatchMany(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => + assertTrue(json.contains(JsonDocument.four) || json.contains(JsonDocument.five), + s"Expected document 'four' or 'five' ($json)") + case Dialect.POSTGRESQL => + assertTrue(json.contains(docId("four")) || json.contains(docId("five")), + s"Expected document 'four' or 'five' ($json)") + + def firstByJsonPathMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByJsonPathMatchMany(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)")) + + def writeFirstByJsonPathMatchMany(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)") + checkFirstByJsonPathMatchMany(output.toString) + + private def checkFirstByJsonPathMatchOrdered(json: String): Unit = + Configuration.dialect() match + case Dialect.SQLITE => assertEquals(JsonDocument.four, json, "An incorrect document was returned") + case Dialect.POSTGRESQL => assertTrue(json.contains(docId("four")), s"An incorrect document was returned ($json)") + + def firstByJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByJsonPathMatchOrdered(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", + Field.named("id DESC") :: Nil)) + + def writeFirstByJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 10)", Field.named("id DESC") :: Nil) + checkFirstByJsonPathMatchOrdered(output.toString) + + private def checkFirstByJsonPathNoMatch(json: String): Unit = + assertEquals("{}", json, "There should have been no document returned") + + def firstByJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + checkFirstByJsonPathNoMatch(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)")) + + def writeFirstByJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val output = StringWriter() + val writer = PrintWriter(output) + db.conn.writeJsonFirstByJsonPath(TEST_TABLE, writer, "$.numValue ? (@ > 100)") + checkFirstByJsonPathNoMatch(output.toString) diff --git a/src/scala/src/test/scala/integration/NumIdDocument.scala b/src/scala/src/test/scala/integration/NumIdDocument.scala new file mode 100644 index 0000000..e426246 --- /dev/null +++ b/src/scala/src/test/scala/integration/NumIdDocument.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests.integration + +class NumIdDocument(val key: Int = 0, val text: String = "") diff --git a/src/scala/src/test/scala/integration/PatchFunctions.scala b/src/scala/src/test/scala/integration/PatchFunctions.scala new file mode 100644 index 0000000..b999c30 --- /dev/null +++ b/src/scala/src/test/scala/integration/PatchFunctions.scala @@ -0,0 +1,65 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +object PatchFunctions: + + def byIdMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + db.conn.patchById(TEST_TABLE, "one", Map.Map1("numValue", 44)) + val doc = db.conn.findById[String, JsonDocument](TEST_TABLE, "one") + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("one", doc.get.id, "An incorrect document was returned") + assertEquals(44, doc.get.numValue, "The document was not patched") + + def byIdNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsById(TEST_TABLE, "forty-seven"), "Document with ID \"forty-seven\" should not exist") + db.conn.patchById(TEST_TABLE, "forty-seven", Map.Map1("foo", "green")) // no exception = pass + + def byFieldsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + db.conn.patchByFields(TEST_TABLE, Field.equal("value", "purple") :: Nil, Map.Map1("numValue", 77)) + assertEquals(2L, db.conn.countByFields(TEST_TABLE, Field.equal("numValue", 77) :: Nil), + "There should have been 2 documents with numeric value 77") + + def byFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val fields = Field.equal("value", "burgundy") :: Nil + assertFalse(db.conn.existsByFields(TEST_TABLE, fields), "There should be no documents with value of \"burgundy\"") + db.conn.patchByFields(TEST_TABLE, fields, Map.Map1("foo", "green")) // no exception = pass + + def byContainsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val contains = Map.Map1("value", "another") + db.conn.patchByContains(TEST_TABLE, contains, Map.Map1("numValue", 12)) + val doc = db.conn.findFirstByContains[JsonDocument, Map.Map1[String, String]](TEST_TABLE, contains) + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("two", doc.get.id, "The incorrect document was returned") + assertEquals(12, doc.get.numValue, "The document was not updated") + + def byContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val contains = Map.Map1("value", "updated") + assertFalse(db.conn.existsByContains(TEST_TABLE, contains), "There should be no matching documents") + db.conn.patchByContains(TEST_TABLE, contains, Map.Map1("sub.foo", "green")) // no exception = pass + + def byJsonPathMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val path = "$.numValue ? (@ > 10)" + db.conn.patchByJsonPath(TEST_TABLE, path, Map.Map1("value", "blue")) + val docs = db.conn.findByJsonPath[JsonDocument](TEST_TABLE, path) + assertEquals(2, docs.size, "There should have been two documents returned") + docs.foreach { doc => + assertTrue(("four" :: "five" :: Nil).contains(doc.id), s"An incorrect document was returned (${doc.id})") + assertEquals("blue", doc.value, s"The value for ID ${doc.id} was incorrect") + } + + def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val path = "$.numValue ? (@ > 100)" + assertFalse(db.conn.existsByJsonPath(TEST_TABLE, path), "There should be no documents with numeric values over 100") + db.conn.patchByJsonPath(TEST_TABLE, path, Map.Map1("value", "blue")) // no exception = pass diff --git a/src/scala/src/test/scala/integration/PgDB.scala b/src/scala/src/test/scala/integration/PgDB.scala new file mode 100644 index 0000000..27c1e41 --- /dev/null +++ b/src/scala/src/test/scala/integration/PgDB.scala @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import solutions.bitbadger.documents.{Configuration, Parameter, ParameterType} +import solutions.bitbadger.documents.scala.Results +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +import java.sql.Connection +import scala.util.Using + +/** + * A wrapper for a throwaway PostgreSQL database + */ +class PgDB extends ThrowawayDatabase: + + Configuration.setConnectionString(PgDB.connString("postgres")) + Using(Configuration.dbConn()) { conn => conn.customNonQuery(s"CREATE DATABASE $dbName") } + + Configuration.setConnectionString(PgDB.connString(dbName)) + + override val conn: Connection = Configuration.dbConn() + + conn.ensureTable(TEST_TABLE) + + override def close(): Unit = + conn.close() + Configuration.setConnectionString(PgDB.connString("postgres")) + Using(Configuration.dbConn()) { conn => conn.customNonQuery(s"DROP DATABASE $dbName") } + Configuration.setConnectionString(null) + + override def dbObjectExists(name: String): Boolean = + conn.customScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = :name) AS it", + Parameter(":name", ParameterType.STRING, name) :: Nil, Results.toExists) + + +object PgDB: + + /** + * Create a connection string for the given database + * + * @param database The database to which the library should connect + * @return The connection string for the database + */ + private def connString(database: String): String = + s"jdbc:postgresql://localhost/$database?user=postgres&password=postgres" diff --git a/src/scala/src/test/scala/integration/PostgreSQLCountIT.scala b/src/scala/src/test/scala/integration/PostgreSQLCountIT.scala new file mode 100644 index 0000000..fcb84e1 --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLCountIT.scala @@ -0,0 +1,43 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +@DisplayName("Scala | PostgreSQL: Count") +class PostgreSQLCountIT: + + @Test + @DisplayName("all counts all documents") + def all(): Unit = + Using(PgDB()) { db => CountFunctions.all(db) } + + @Test + @DisplayName("byFields counts documents by a numeric value") + def byFieldsNumeric(): Unit = + Using(PgDB()) { db => CountFunctions.byFieldsNumeric(db) } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + def byFieldsAlpha(): Unit = + Using(PgDB()) { db => CountFunctions.byFieldsAlpha(db) } + + @Test + @DisplayName("byContains counts documents when matches are found") + def byContainsMatch(): Unit = + Using(PgDB()) { db => CountFunctions.byContainsMatch(db) } + + @Test + @DisplayName("byContains counts documents when no matches are found") + def byContainsNoMatch(): Unit = + Using(PgDB()) { db => CountFunctions.byContainsNoMatch(db) } + + @Test + @DisplayName("byJsonPath counts documents when matches are found") + def byJsonPathMatch(): Unit = + Using(PgDB()) { db => CountFunctions.byJsonPathMatch(db) } + + @Test + @DisplayName("byJsonPath counts documents when no matches are found") + def byJsonPathNoMatch(): Unit = + Using(PgDB()) { db => CountFunctions.byJsonPathNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala new file mode 100644 index 0000000..e1a6cc1 --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala @@ -0,0 +1,83 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +@DisplayName("Scala | PostgreSQL: Custom") +class PostgreSQLCustomIT: + + @Test + @DisplayName("list succeeds with empty list") + def listEmpty(): Unit = + Using(PgDB()) { db => CustomFunctions.listEmpty(db) } + + @Test + @DisplayName("list succeeds with a non-empty list") + def listAll(): Unit = + Using(PgDB()) { db => CustomFunctions.listAll(db) } + + @Test + @DisplayName("jsonArray succeeds with empty array") + def jsonArrayEmpty(): Unit = + Using(PgDB()) { db => CustomFunctions.jsonArrayEmpty(db) } + + @Test + @DisplayName("jsonArray succeeds with a single-item array") + def jsonArraySingle(): Unit = + Using(PgDB()) { db => CustomFunctions.jsonArraySingle(db) } + + @Test + @DisplayName("jsonArray succeeds with a multi-item array") + def jsonArrayMany(): Unit = + Using(PgDB()) { db => CustomFunctions.jsonArrayMany(db) } + + @Test + @DisplayName("writeJsonArray succeeds with empty array") + def writeJsonArrayEmpty(): Unit = + Using(PgDB()) { db => CustomFunctions.writeJsonArrayEmpty(db) } + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + def writeJsonArraySingle(): Unit = + Using(PgDB()) { db => CustomFunctions.writeJsonArraySingle(db) } + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + def writeJsonArrayMany(): Unit = + Using(PgDB()) { db => CustomFunctions.writeJsonArrayMany(db) } + + @Test + @DisplayName("single succeeds when document not found") + def singleNone(): Unit = + Using(PgDB()) { db => CustomFunctions.singleNone(db) } + + @Test + @DisplayName("single succeeds when a document is found") + def singleOne(): Unit = + Using(PgDB()) { db => CustomFunctions.singleOne(db) } + + @Test + @DisplayName("jsonSingle succeeds when document not found") + def jsonSingleNone(): Unit = + Using(PgDB()) { db => CustomFunctions.jsonSingleNone(db) } + + @Test + @DisplayName("jsonSingle succeeds when a document is found") + def jsonSingleOne(): Unit = + Using(PgDB()) { db => CustomFunctions.jsonSingleOne(db) } + + @Test + @DisplayName("nonQuery makes changes") + def nonQueryChanges(): Unit = + Using(PgDB()) { db => CustomFunctions.nonQueryChanges(db) } + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + def nonQueryNoChanges(): Unit = + Using(PgDB()) { db => CustomFunctions.nonQueryNoChanges(db) } + + @Test + @DisplayName("scalar succeeds") + def scalar(): Unit = + Using(PgDB()) { db => CustomFunctions.scalar(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLDefinitionIT.scala b/src/scala/src/test/scala/integration/PostgreSQLDefinitionIT.scala new file mode 100644 index 0000000..abdf325 --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLDefinitionIT.scala @@ -0,0 +1,31 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("Scala | PostgreSQL: Definition") +class PostgreSQLDefinitionIT: + + @Test + @DisplayName("ensureTable creates table and index") + def ensureTable(): Unit = + Using(PgDB()) { db => DefinitionFunctions.ensureTable(db) } + + @Test + @DisplayName("ensureFieldIndex creates an index") + def ensureFieldIndex(): Unit = + Using(PgDB()) { db => DefinitionFunctions.ensureFieldIndex(db) } + + @Test + @DisplayName("ensureDocumentIndex creates a full index") + def ensureDocumentIndexFull(): Unit = + Using(PgDB()) { db => DefinitionFunctions.ensureDocumentIndexFull(db) } + + @Test + @DisplayName("ensureDocumentIndex creates an optimized index") + def ensureDocumentIndexOptimized(): Unit = + Using(PgDB()) { db => DefinitionFunctions.ensureDocumentIndexOptimized(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLDeleteIT.scala b/src/scala/src/test/scala/integration/PostgreSQLDeleteIT.scala new file mode 100644 index 0000000..df349b0 --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLDeleteIT.scala @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName("Scala | PostgreSQL: Delete") +class PostgreSQLDeleteIT: + + @Test + @DisplayName("byId deletes a matching ID") + def byIdMatch(): Unit = + Using(PgDB()) { db => DeleteFunctions.byIdMatch(db) } + + @Test + @DisplayName("byId succeeds when no ID matches") + def byIdNoMatch(): Unit = + Using(PgDB()) { db => DeleteFunctions.byIdNoMatch(db) } + + @Test + @DisplayName("byFields deletes matching documents") + def byFieldsMatch(): Unit = + Using(PgDB()) { db => DeleteFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields succeeds when no documents match") + def byFieldsNoMatch(): Unit = + Using(PgDB()) { db => DeleteFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byContains deletes matching documents") + def byContainsMatch(): Unit = + Using(PgDB()) { db => DeleteFunctions.byContainsMatch(db) } + + @Test + @DisplayName("byContains succeeds when no documents match") + def byContainsNoMatch(): Unit = + Using(PgDB()) { db => DeleteFunctions.byContainsNoMatch(db) } + + @Test + @DisplayName("byJsonPath deletes matching documents") + def byJsonPathMatch(): Unit = + Using(PgDB()) { db => DeleteFunctions.byJsonPathMatch(db) } + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + def byJsonPathNoMatch(): Unit = + Using(PgDB()) { db => DeleteFunctions.byJsonPathNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLDocumentIT.scala b/src/scala/src/test/scala/integration/PostgreSQLDocumentIT.scala new file mode 100644 index 0000000..77ebe77 --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLDocumentIT.scala @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("Scala | PostgreSQL: Document") +class PostgreSQLDocumentIT: + + @Test + @DisplayName("insert works with default values") + def insertDefault(): Unit = + Using(PgDB()) { db => DocumentFunctions.insertDefault(db) } + + @Test + @DisplayName("insert fails with duplicate key") + def insertDupe(): Unit = + Using(PgDB()) { db => DocumentFunctions.insertDupe(db) } + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + def insertNumAutoId(): Unit = + Using(PgDB()) { db => DocumentFunctions.insertNumAutoId(db) } + + @Test + @DisplayName("insert succeeds with UUID auto ID") + def insertUUIDAutoId(): Unit = + Using(PgDB()) { db => DocumentFunctions.insertUUIDAutoId(db) } + + @Test + @DisplayName("insert succeeds with random string auto ID") + def insertStringAutoId(): Unit = + Using(PgDB()) { db => DocumentFunctions.insertStringAutoId(db) } + + @Test + @DisplayName("save updates an existing document") + def saveMatch(): Unit = + Using(PgDB()) { db => DocumentFunctions.saveMatch(db) } + + @Test + @DisplayName("save inserts a new document") + def saveNoMatch(): Unit = + Using(PgDB()) { db => DocumentFunctions.saveNoMatch(db) } + + @Test + @DisplayName("update replaces an existing document") + def updateMatch(): Unit = + Using(PgDB()) { db => DocumentFunctions.updateMatch(db) } + + @Test + @DisplayName("update succeeds when no document exists") + def updateNoMatch(): Unit = + Using(PgDB()) { db => DocumentFunctions.updateNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLExistsIT.scala b/src/scala/src/test/scala/integration/PostgreSQLExistsIT.scala new file mode 100644 index 0000000..ea04640 --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLExistsIT.scala @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("Scala | PostgreSQL: Exists") +class PostgreSQLExistsIT: + + @Test + @DisplayName("byId returns true when a document matches the ID") + def byIdMatch(): Unit = + Using(PgDB()) { db => ExistsFunctions.byIdMatch(db) } + + @Test + @DisplayName("byId returns false when no document matches the ID") + def byIdNoMatch(): Unit = + Using(PgDB()) { db => ExistsFunctions.byIdNoMatch(db) } + + @Test + @DisplayName("byFields returns true when documents match") + def byFieldsMatch(): Unit = + Using(PgDB()) { db => ExistsFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields returns false when no documents match") + def byFieldsNoMatch(): Unit = + Using(PgDB()) { db => ExistsFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byContains returns true when documents match") + def byContainsMatch(): Unit = + Using(PgDB()) { db => ExistsFunctions.byContainsMatch(db) } + + @Test + @DisplayName("byContains returns false when no documents match") + def byContainsNoMatch(): Unit = + Using(PgDB()) { db => ExistsFunctions.byContainsNoMatch(db) } + + @Test + @DisplayName("byJsonPath returns true when documents match") + def byJsonPathMatch(): Unit = + Using(PgDB()) { db => ExistsFunctions.byJsonPathMatch(db) } + + @Test + @DisplayName("byJsonPath returns false when no documents match") + def byJsonPathNoMatch(): Unit = + Using(PgDB()) { db => ExistsFunctions.byJsonPathNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLFindIT.scala b/src/scala/src/test/scala/integration/PostgreSQLFindIT.scala new file mode 100644 index 0000000..e7f85be --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLFindIT.scala @@ -0,0 +1,171 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("Scala | PostgreSQL: Find") +class PostgreSQLFindIT: + + @Test + @DisplayName("all retrieves all documents") + def allDefault(): Unit = + Using(PgDB()) { db => FindFunctions.allDefault(db) } + + @Test + @DisplayName("all sorts data ascending") + def allAscending(): Unit = + Using(PgDB()) { db => FindFunctions.allAscending(db) } + + @Test + @DisplayName("all sorts data descending") + def allDescending(): Unit = + Using(PgDB()) { db => FindFunctions.allDescending(db) } + + @Test + @DisplayName("all sorts data numerically") + def allNumOrder(): Unit = + Using(PgDB()) { db => FindFunctions.allNumOrder(db) } + + @Test + @DisplayName("all succeeds with an empty table") + def allEmpty(): Unit = + Using(PgDB()) { db => FindFunctions.allEmpty(db) } + + @Test + @DisplayName("byId retrieves a document via a string ID") + def byIdString(): Unit = + Using(PgDB()) { db => FindFunctions.byIdString(db) } + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + def byIdNumber(): Unit = + Using(PgDB()) { db => FindFunctions.byIdNumber(db) } + + @Test + @DisplayName("byId returns null when a matching ID is not found") + def byIdNotFound(): Unit = + Using(PgDB()) { db => FindFunctions.byIdNotFound(db) } + + @Test + @DisplayName("byFields retrieves matching documents") + def byFieldsMatch(): Unit = + Using(PgDB()) { db => FindFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields retrieves ordered matching documents") + def byFieldsMatchOrdered(): Unit = + Using(PgDB()) { db => FindFunctions.byFieldsMatchOrdered(db) } + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + def byFieldsMatchNumIn(): Unit = + Using(PgDB()) { db => FindFunctions.byFieldsMatchNumIn(db) } + + @Test + @DisplayName("byFields succeeds when no documents match") + def byFieldsNoMatch(): Unit = + Using(PgDB()) { db => FindFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + def byFieldsMatchInArray(): Unit = + Using(PgDB()) { db => FindFunctions.byFieldsMatchInArray(db) } + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + def byFieldsNoMatchInArray(): Unit = + Using(PgDB()) { db => FindFunctions.byFieldsNoMatchInArray(db) } + + @Test + @DisplayName("byContains retrieves matching documents") + def byContainsMatch(): Unit = + Using(PgDB()) { db => FindFunctions.byContainsMatch(db) } + + @Test + @DisplayName("byContains retrieves ordered matching documents") + def byContainsMatchOrdered(): Unit = + Using(PgDB()) { db => FindFunctions.byContainsMatchOrdered(db) } + + @Test + @DisplayName("byContains succeeds when no documents match") + def byContainsNoMatch(): Unit = + Using(PgDB()) { db => FindFunctions.byContainsNoMatch(db) } + + @Test + @DisplayName("byJsonPath retrieves matching documents") + def byJsonPathMatch(): Unit = + Using(PgDB()) { db => FindFunctions.byJsonPathMatch(db) } + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + def byJsonPathMatchOrdered(): Unit = + Using(PgDB()) { db => FindFunctions.byJsonPathMatchOrdered(db) } + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + def byJsonPathNoMatch(): Unit = + Using(PgDB()) { db => FindFunctions.byJsonPathNoMatch(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document") + def firstByFieldsMatchOne(): Unit = + Using(PgDB()) { db => FindFunctions.firstByFieldsMatchOne(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + def firstByFieldsMatchMany(): Unit = + Using(PgDB()) { db => FindFunctions.firstByFieldsMatchMany(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + def firstByFieldsMatchOrdered(): Unit = + Using(PgDB()) { db => FindFunctions.firstByFieldsMatchOrdered(db) } + + @Test + @DisplayName("firstByFields returns null when no document matches") + def firstByFieldsNoMatch(): Unit = + Using(PgDB()) { db => FindFunctions.firstByFieldsNoMatch(db) } + + @Test + @DisplayName("firstByContains retrieves a matching document") + def firstByContainsMatchOne(): Unit = + Using(PgDB()) { db => FindFunctions.firstByContainsMatchOne(db) } + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + def firstByContainsMatchMany(): Unit = + Using(PgDB()) { db => FindFunctions.firstByContainsMatchMany(db) } + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + def firstByContainsMatchOrdered(): Unit = + Using(PgDB()) { db => FindFunctions.firstByContainsMatchOrdered(db) } + + @Test + @DisplayName("firstByContains returns null when no document matches") + def firstByContainsNoMatch(): Unit = + Using(PgDB()) { db => FindFunctions.firstByContainsNoMatch(db) } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + def firstByJsonPathMatchOne(): Unit = + Using(PgDB()) { db => FindFunctions.firstByJsonPathMatchOne(db) } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + def firstByJsonPathMatchMany(): Unit = + Using(PgDB()) { db => FindFunctions.firstByJsonPathMatchMany(db) } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + def firstByJsonPathMatchOrdered(): Unit = + Using(PgDB()) { db => FindFunctions.firstByJsonPathMatchOrdered(db) } + + @Test + @DisplayName("firstByJsonPath returns null when no document matches") + def firstByJsonPathNoMatch(): Unit = + Using(PgDB()) { db => FindFunctions.firstByJsonPathNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala b/src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala new file mode 100644 index 0000000..553bbde --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala @@ -0,0 +1,301 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * PostgreSQL integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName("Scala | PostgreSQL: Json") +class PostgreSQLJsonIT: + + @Test + @DisplayName("all retrieves all documents") + def allDefault(): Unit = + Using(PgDB()) { db => JsonFunctions.allDefault(db) } + + @Test + @DisplayName("all succeeds with an empty table") + def allEmpty(): Unit = + Using(PgDB()) { db => JsonFunctions.allEmpty(db) } + + @Test + @DisplayName("byId retrieves a document via a string ID") + def byIdString(): Unit = + Using(PgDB()) { db => JsonFunctions.byIdString(db) } + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + def byIdNumber(): Unit = + Using(PgDB()) { db => JsonFunctions.byIdNumber(db) } + + @Test + @DisplayName("byId returns null when a matching ID is not found") + def byIdNotFound(): Unit = + Using(PgDB()) { db => JsonFunctions.byIdNotFound(db) } + + @Test + @DisplayName("byFields retrieves matching documents") + def byFieldsMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields retrieves ordered matching documents") + def byFieldsMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.byFieldsMatchOrdered(db) } + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + def byFieldsMatchNumIn(): Unit = + Using(PgDB()) { db => JsonFunctions.byFieldsMatchNumIn(db) } + + @Test + @DisplayName("byFields succeeds when no documents match") + def byFieldsNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + def byFieldsMatchInArray(): Unit = + Using(PgDB()) { db => JsonFunctions.byFieldsMatchInArray(db) } + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + def byFieldsNoMatchInArray(): Unit = + Using(PgDB()) { db => JsonFunctions.byFieldsNoMatchInArray(db) } + + @Test + @DisplayName("byContains retrieves matching documents") + def byContainsMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.byContainsMatch(db) } + + @Test + @DisplayName("byContains retrieves ordered matching documents") + def byContainsMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.byContainsMatchOrdered(db) } + + @Test + @DisplayName("byContains succeeds when no documents match") + def byContainsNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.byContainsNoMatch(db) } + + @Test + @DisplayName("byJsonPath retrieves matching documents") + def byJsonPathMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.byJsonPathMatch(db) } + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + def byJsonPathMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.byJsonPathMatchOrdered(db) } + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + def byJsonPathNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.byJsonPathNoMatch(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document") + def firstByFieldsMatchOne(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByFieldsMatchOne(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + def firstByFieldsMatchMany(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByFieldsMatchMany(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + def firstByFieldsMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByFieldsMatchOrdered(db) } + + @Test + @DisplayName("firstByFields returns null when no document matches") + def firstByFieldsNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByFieldsNoMatch(db) } + + @Test + @DisplayName("firstByContains retrieves a matching document") + def firstByContainsMatchOne(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByContainsMatchOne(db) } + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + def firstByContainsMatchMany(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByContainsMatchMany(db) } + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + def firstByContainsMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByContainsMatchOrdered(db) } + + @Test + @DisplayName("firstByContains returns null when no document matches") + def firstByContainsNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByContainsNoMatch(db) } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + def firstByJsonPathMatchOne(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByJsonPathMatchOne(db) } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + def firstByJsonPathMatchMany(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByJsonPathMatchMany(db) } + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + def firstByJsonPathMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByJsonPathMatchOrdered(db) } + + @Test + @DisplayName("firstByJsonPath returns null when no document matches") + def firstByJsonPathNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.firstByJsonPathNoMatch(db) } + + @Test + @DisplayName("writeAll retrieves all documents") + def writeAllDefault(): Unit = + Using(PgDB()) { db => JsonFunctions.writeAllDefault(db) } + + @Test + @DisplayName("writeAll succeeds with an empty table") + def writeAllEmpty(): Unit = + Using(PgDB()) { db => JsonFunctions.writeAllEmpty(db) } + + @Test + @DisplayName("writeById retrieves a document via a string ID") + def writeByIdString(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByIdString(db) } + + @Test + @DisplayName("writeById retrieves a document via a numeric ID") + def writeByIdNumber(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByIdNumber(db) } + + @Test + @DisplayName("writeById returns null when a matching ID is not found") + def writeByIdNotFound(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByIdNotFound(db) } + + @Test + @DisplayName("writeByFields retrieves matching documents") + def writeByFieldsMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByFieldsMatch(db) } + + @Test + @DisplayName("writeByFields retrieves ordered matching documents") + def writeByFieldsMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByFieldsMatchOrdered(db) } + + @Test + @DisplayName("writeByFields retrieves matching documents with a numeric IN clause") + def writeByFieldsMatchNumIn(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByFieldsMatchNumIn(db) } + + @Test + @DisplayName("writeByFields succeeds when no documents match") + def writeByFieldsNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByFieldsNoMatch(db) } + + @Test + @DisplayName("writeByFields retrieves matching documents with an IN_ARRAY comparison") + def writeByFieldsMatchInArray(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByFieldsMatchInArray(db) } + + @Test + @DisplayName("writeByFields succeeds when no documents match an IN_ARRAY comparison") + def writeByFieldsNoMatchInArray(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByFieldsNoMatchInArray(db) } + + @Test + @DisplayName("writeByContains retrieves matching documents") + def writeByContainsMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByContainsMatch(db) } + + @Test + @DisplayName("writeByContains retrieves ordered matching documents") + def writeByContainsMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByContainsMatchOrdered(db) } + + @Test + @DisplayName("writeByContains succeeds when no documents match") + def writeByContainsNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByContainsNoMatch(db) } + + @Test + @DisplayName("writeByJsonPath retrieves matching documents") + def writeByJsonPathMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByJsonPathMatch(db) } + + @Test + @DisplayName("writeByJsonPath retrieves ordered matching documents") + def writeByJsonPathMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByJsonPathMatchOrdered(db) } + + @Test + @DisplayName("writeByJsonPath succeeds when no documents match") + def writeByJsonPathNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeByJsonPathNoMatch(db) } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document") + def writeFirstByFieldsMatchOne(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByFieldsMatchOne(db) } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many") + def writeFirstByFieldsMatchMany(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByFieldsMatchMany(db) } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many (ordered)") + def writeFirstByFieldsMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByFieldsMatchOrdered(db) } + + @Test + @DisplayName("writeFirstByFields returns null when no document matches") + def writeFirstByFieldsNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByFieldsNoMatch(db) } + + @Test + @DisplayName("writeFirstByContains retrieves a matching document") + def writeFirstByContainsMatchOne(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByContainsMatchOne(db) } + + @Test + @DisplayName("writeFirstByContains retrieves a matching document among many") + def writeFirstByContainsMatchMany(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByContainsMatchMany(db) } + + @Test + @DisplayName("writeFirstByContains retrieves a matching document among many (ordered)") + def writeFirstByContainsMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByContainsMatchOrdered(db) } + + @Test + @DisplayName("writeFirstByContains returns null when no document matches") + def writeFirstByContainsNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByContainsNoMatch(db) } + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document") + def writeFirstByJsonPathMatchOne(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByJsonPathMatchOne(db) } + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document among many") + def writeFirstByJsonPathMatchMany(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByJsonPathMatchMany(db) } + + @Test + @DisplayName("writeFirstByJsonPath retrieves a matching document among many (ordered)") + def writeFirstByJsonPathMatchOrdered(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByJsonPathMatchOrdered(db) } + + @Test + @DisplayName("writeFirstByJsonPath returns null when no document matches") + def writeFirstByJsonPathNoMatch(): Unit = + Using(PgDB()) { db => JsonFunctions.writeFirstByJsonPathNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLPatchIT.scala b/src/scala/src/test/scala/integration/PostgreSQLPatchIT.scala new file mode 100644 index 0000000..06e9410 --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLPatchIT.scala @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("Scala | PostgreSQL: Patch") +class PostgreSQLPatchIT: + + @Test + @DisplayName("byId patches an existing document") + def byIdMatch(): Unit = + Using(PgDB()) { db => PatchFunctions. byIdMatch(db) } + + @Test + @DisplayName("byId succeeds for a non-existent document") + def byIdNoMatch(): Unit = + Using(PgDB()) { db => PatchFunctions. byIdNoMatch(db) } + + @Test + @DisplayName("byFields patches matching document") + def byFieldsMatch(): Unit = + Using(PgDB()) { db => PatchFunctions. byFieldsMatch(db) } + + @Test + @DisplayName("byFields succeeds when no documents match") + def byFieldsNoMatch(): Unit = + Using(PgDB()) { db => PatchFunctions. byFieldsNoMatch(db) } + + @Test + @DisplayName("byContains patches matching document") + def byContainsMatch(): Unit = + Using(PgDB()) { db => PatchFunctions. byContainsMatch(db) } + + @Test + @DisplayName("byContains succeeds when no documents match") + def byContainsNoMatch(): Unit = + Using(PgDB()) { db => PatchFunctions. byContainsNoMatch(db) } + + @Test + @DisplayName("byJsonPath patches matching document") + def byJsonPathMatch(): Unit = + Using(PgDB()) { db => PatchFunctions. byJsonPathMatch(db) } + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + def byJsonPathNoMatch(): Unit = + Using(PgDB()) { db => PatchFunctions. byJsonPathNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/PostgreSQLRemoveFieldsIT.scala b/src/scala/src/test/scala/integration/PostgreSQLRemoveFieldsIT.scala new file mode 100644 index 0000000..ffbc6c2 --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLRemoveFieldsIT.scala @@ -0,0 +1,71 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("Scala | PostgreSQL: RemoveFields") +class PostgreSQLRemoveFieldsIT: + + @Test + @DisplayName("byId removes fields from an existing document") + def byIdMatchFields(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byIdMatchFields(db) } + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + def byIdMatchNoFields(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byIdMatchNoFields(db) } + + @Test + @DisplayName("byId succeeds when no document exists") + def byIdNoMatch(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byIdNoMatch(db) } + + @Test + @DisplayName("byFields removes fields from matching documents") + def byFieldsMatchFields(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byFieldsMatchFields(db) } + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + def byFieldsMatchNoFields(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byFieldsMatchNoFields(db) } + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + def byFieldsNoMatch(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byFieldsNoMatch(db) } + + @Test + @DisplayName("byContains removes fields from matching documents") + def byContainsMatchFields(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byContainsMatchFields(db) } + + @Test + @DisplayName("byContains succeeds when fields do not exist on matching documents") + def byContainsMatchNoFields(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byContainsMatchNoFields(db) } + + @Test + @DisplayName("byContains succeeds when no matching documents exist") + def byContainsNoMatch(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byContainsNoMatch(db) } + + @Test + @DisplayName("byJsonPath removes fields from matching documents") + def byJsonPathMatchFields(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byJsonPathMatchFields(db) } + + @Test + @DisplayName("byJsonPath succeeds when fields do not exist on matching documents") + def byJsonPathMatchNoFields(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byJsonPathMatchNoFields(db) } + + @Test + @DisplayName("byJsonPath succeeds when no matching documents exist") + def byJsonPathNoMatch(): Unit = + Using(PgDB()) { db => RemoveFieldsFunctions. byJsonPathNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/RemoveFieldsFunctions.scala b/src/scala/src/test/scala/integration/RemoveFieldsFunctions.scala new file mode 100644 index 0000000..1531355 --- /dev/null +++ b/src/scala/src/test/scala/integration/RemoveFieldsFunctions.scala @@ -0,0 +1,92 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +object RemoveFieldsFunctions: + + def byIdMatchFields(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + db.conn.removeFieldsById(TEST_TABLE, "two", "sub" :: "value" :: Nil) + val doc = db.conn.findById[String, JsonDocument](TEST_TABLE, "two") + assertTrue(doc.isDefined, "There should have been a document returned") + assertEquals("", doc.get.value, "The value should have been empty") + assertNull(doc.get.sub, "The sub-document should have been removed") + + def byIdMatchNoFields(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, Field.exists("a_field_that_does_not_exist") :: Nil)) + db.conn.removeFieldsById(TEST_TABLE, "one", "a_field_that_does_not_exist" :: Nil) // no exception = pass + + def byIdNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsById(TEST_TABLE, "fifty")) + db.conn.removeFieldsById(TEST_TABLE, "fifty", "sub" :: Nil) // no exception = pass + + def byFieldsMatchFields(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val fields = Field.equal("numValue", 17) :: Nil + db.conn.removeFieldsByFields(TEST_TABLE, fields, "sub" :: Nil) + val doc = db.conn.findFirstByFields[JsonDocument](TEST_TABLE, fields) + assertTrue(doc.isDefined, "The document should have been returned") + assertEquals("four", doc.get.id, "An incorrect document was returned") + assertNull(doc.get.sub, "The sub-document should have been removed") + + def byFieldsMatchNoFields(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, Field.exists("nada") :: Nil)) + db.conn.removeFieldsByFields(TEST_TABLE, Field.equal("numValue", 17) :: Nil, "nada" :: Nil) // no exn = pass + + def byFieldsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val fields = Field.notEqual("missing", "nope") :: Nil + assertFalse(db.conn.existsByFields(TEST_TABLE, fields)) + db.conn.removeFieldsByFields(TEST_TABLE, fields, "value" :: Nil) // no exception = pass + + def byContainsMatchFields(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val criteria = Map.Map1("sub", Map.Map1("foo", "green")) + db.conn.removeFieldsByContains(TEST_TABLE, criteria, "value" :: Nil) + val docs = db.conn.findByContains[JsonDocument, Map.Map1[String, Map.Map1[String, String]]](TEST_TABLE, criteria) + assertEquals(2, docs.size, "There should have been 2 documents returned") + docs.foreach { doc => + assertTrue(("two" :: "four" :: Nil).contains(doc.id), s"An incorrect document was returned (${doc.id})") + assertEquals("", doc.value, "The value should have been empty") + } + + def byContainsMatchNoFields(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, Field.exists("invalid_field") :: Nil)) + db.conn.removeFieldsByContains(TEST_TABLE, Map.Map1("sub", Map.Map1("foo", "green")), "invalid_field" :: Nil) + // no exception = pass + + def byContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val contains = Map.Map1("value", "substantial") + assertFalse(db.conn.existsByContains(TEST_TABLE, contains)) + db.conn.removeFieldsByContains(TEST_TABLE, contains, "numValue" :: Nil) + + def byJsonPathMatchFields(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val path = "$.value ? (@ == \"purple\")" + db.conn.removeFieldsByJsonPath(TEST_TABLE, path, "sub" :: Nil) + val docs = db.conn.findByJsonPath[JsonDocument](TEST_TABLE, path) + assertEquals(2, docs.size, "There should have been 2 documents returned") + docs.foreach { doc => + assertTrue(("four" :: "five" :: Nil).contains(doc.id), s"An incorrect document was returned (${doc.id})") + assertNull(doc.sub, "The sub-document should have been removed") + } + + def byJsonPathMatchNoFields(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertFalse(db.conn.existsByFields(TEST_TABLE, Field.exists("submarine") :: Nil)) + db.conn.removeFieldsByJsonPath(TEST_TABLE, "$.value ? (@ == \"purple\")", "submarine" :: Nil) // no exn = pass + + def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val path = "$.value ? (@ == \"mauve\")" + assertFalse(db.conn.existsByJsonPath(TEST_TABLE, path)) + db.conn.removeFieldsByJsonPath(TEST_TABLE, path, "value" :: Nil) // no exception = pass + \ No newline at end of file diff --git a/src/scala/src/test/scala/integration/SQLiteCountIT.scala b/src/scala/src/test/scala/integration/SQLiteCountIT.scala new file mode 100644 index 0000000..eba322d --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteCountIT.scala @@ -0,0 +1,35 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +@DisplayName("Scala | SQLite: Count") +class SQLiteCountIT: + + @Test + @DisplayName("all counts all documents") + def all(): Unit = + Using(SQLiteDB()) { db => CountFunctions.all(db) } + + @Test + @DisplayName("byFields counts documents by a numeric value") + def byFieldsNumeric(): Unit = + Using(SQLiteDB()) { db => CountFunctions.byFieldsNumeric(db) } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + def byFieldsAlpha(): Unit = + Using(SQLiteDB()) { db => CountFunctions.byFieldsAlpha(db) } + + @Test + @DisplayName("byContains fails") + def byContainsMatch(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => CountFunctions.byContainsMatch(db)) } + + @Test + @DisplayName("byJsonPath fails") + def byJsonPathMatch(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => CountFunctions.byJsonPathMatch(db)) } diff --git a/src/scala/src/test/scala/integration/SQLiteCustomIT.scala b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala new file mode 100644 index 0000000..e93bdb3 --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala @@ -0,0 +1,83 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +@DisplayName("Scala | SQLite: Custom") +class SQLiteCustomIT: + + @Test + @DisplayName("list succeeds with empty list") + def listEmpty(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.listEmpty(db) } + + @Test + @DisplayName("list succeeds with a non-empty list") + def listAll(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.listAll(db) } + + @Test + @DisplayName("jsonArray succeeds with empty array") + def jsonArrayEmpty(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.jsonArrayEmpty(db) } + + @Test + @DisplayName("jsonArray succeeds with a single-item array") + def jsonArraySingle(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.jsonArraySingle(db) } + + @Test + @DisplayName("jsonArray succeeds with a multi-item array") + def jsonArrayMany(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.jsonArrayMany(db) } + + @Test + @DisplayName("writeJsonArray succeeds with empty array") + def writeJsonArrayEmpty(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.writeJsonArrayEmpty(db) } + + @Test + @DisplayName("writeJsonArray succeeds with a single-item array") + def writeJsonArraySingle(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.writeJsonArraySingle(db) } + + @Test + @DisplayName("writeJsonArray succeeds with a multi-item array") + def writeJsonArrayMany(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.writeJsonArrayMany(db) } + + @Test + @DisplayName("single succeeds when document not found") + def singleNone(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.singleNone(db) } + + @Test + @DisplayName("single succeeds when a document is found") + def singleOne(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.singleOne(db) } + + @Test + @DisplayName("jsonSingle succeeds when document not found") + def jsonSingleNone(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.jsonSingleNone(db) } + + @Test + @DisplayName("jsonSingle succeeds when a document is found") + def jsonSingleOne(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.jsonSingleOne(db) } + + @Test + @DisplayName("nonQuery makes changes") + def nonQueryChanges(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.nonQueryChanges(db) } + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + def nonQueryNoChanges(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.nonQueryNoChanges(db) } + + @Test + @DisplayName("scalar succeeds") + def scalar(): Unit = + Using(SQLiteDB()) { db => CustomFunctions.scalar(db) } diff --git a/src/scala/src/test/scala/integration/SQLiteDB.scala b/src/scala/src/test/scala/integration/SQLiteDB.scala new file mode 100644 index 0000000..5a90a0e --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteDB.scala @@ -0,0 +1,30 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import solutions.bitbadger.documents.{Configuration, Parameter, ParameterType} +import solutions.bitbadger.documents.scala.Results +import solutions.bitbadger.documents.scala.extensions.* +import solutions.bitbadger.documents.scala.tests.TEST_TABLE + +import java.io.File +import java.sql.Connection + +/** + * A wrapper for a throwaway SQLite database + */ +class SQLiteDB extends ThrowawayDatabase: + + Configuration.setConnectionString(s"jdbc:sqlite:$dbName.db") + + override val conn: Connection = Configuration.dbConn() + + conn.ensureTable(TEST_TABLE) + + override def close(): Unit = + conn.close() + File(s"$dbName.db").delete() + Configuration.setConnectionString(null) + + override def dbObjectExists(name: String): Boolean = + conn.customScalar[Boolean]("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = :name) AS it", + Parameter(":name", ParameterType.STRING, name) :: Nil, Results.toExists) + diff --git a/src/scala/src/test/scala/integration/SQLiteDefinitionIT.scala b/src/scala/src/test/scala/integration/SQLiteDefinitionIT.scala new file mode 100644 index 0000000..051d820 --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteDefinitionIT.scala @@ -0,0 +1,30 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} +import org.junit.jupiter.api.Assertions.assertThrows +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +/** + * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("Scala | SQLite: Definition") +class SQLiteDefinitionIT: + + @Test + @DisplayName("ensureTable creates table and index") + def ensureTable(): Unit = + Using(SQLiteDB()) { db => DefinitionFunctions.ensureTable(db) } + + @Test + @DisplayName("ensureFieldIndex creates an index") + def ensureFieldIndex(): Unit = + Using(SQLiteDB()) { db => DefinitionFunctions.ensureFieldIndex(db) } + + @Test + @DisplayName("ensureDocumentIndex creates a full index") + def ensureDocumentIndexFull(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => DefinitionFunctions.ensureDocumentIndexFull(db)) + } diff --git a/src/scala/src/test/scala/integration/SQLiteDeleteIT.scala b/src/scala/src/test/scala/integration/SQLiteDeleteIT.scala new file mode 100644 index 0000000..35c9c91 --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteDeleteIT.scala @@ -0,0 +1,43 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} +import org.junit.jupiter.api.Assertions.assertThrows +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +/** + * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ +@DisplayName("Scala | SQLite: Delete") +class SQLiteDeleteIT: + + @Test + @DisplayName("byId deletes a matching ID") + def byIdMatch(): Unit = + Using(SQLiteDB()) { db => DeleteFunctions.byIdMatch(db) } + + @Test + @DisplayName("byId succeeds when no ID matches") + def byIdNoMatch(): Unit = + Using(SQLiteDB()) { db => DeleteFunctions.byIdNoMatch(db) } + + @Test + @DisplayName("byFields deletes matching documents") + def byFieldsMatch(): Unit = + Using(SQLiteDB()) { db => DeleteFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields succeeds when no documents match") + def byFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => DeleteFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byContains fails") + def byContainsFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => DeleteFunctions.byContainsMatch(db)) } + + @Test + @DisplayName("byJsonPath fails") + def byJsonPathFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => DeleteFunctions.byJsonPathMatch(db)) } diff --git a/src/scala/src/test/scala/integration/SQLiteDocumentIT.scala b/src/scala/src/test/scala/integration/SQLiteDocumentIT.scala new file mode 100644 index 0000000..9c3040a --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteDocumentIT.scala @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} + +import scala.util.Using + +/** + * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("Scala | SQLite: Document") +class SQLiteDocumentIT: + + @Test + @DisplayName("insert works with default values") + def insertDefault(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.insertDefault(db) } + + @Test + @DisplayName("insert fails with duplicate key") + def insertDupe(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.insertDupe(db) } + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + def insertNumAutoId(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.insertNumAutoId(db) } + + @Test + @DisplayName("insert succeeds with UUID auto ID") + def insertUUIDAutoId(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.insertUUIDAutoId(db) } + + @Test + @DisplayName("insert succeeds with random string auto ID") + def insertStringAutoId(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.insertStringAutoId(db) } + + @Test + @DisplayName("save updates an existing document") + def saveMatch(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.saveMatch(db) } + + @Test + @DisplayName("save inserts a new document") + def saveNoMatch(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.saveNoMatch(db) } + + @Test + @DisplayName("update replaces an existing document") + def updateMatch(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.updateMatch(db) } + + @Test + @DisplayName("update succeeds when no document exists") + def updateNoMatch(): Unit = + Using(SQLiteDB()) { db => DocumentFunctions.updateNoMatch(db) } diff --git a/src/scala/src/test/scala/integration/SQLiteExistsIT.scala b/src/scala/src/test/scala/integration/SQLiteExistsIT.scala new file mode 100644 index 0000000..86636b4 --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteExistsIT.scala @@ -0,0 +1,43 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} +import org.junit.jupiter.api.Assertions.assertThrows +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +/** + * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("Scala | SQLite: Exists") +class SQLiteExistsIT: + + @Test + @DisplayName("byId returns true when a document matches the ID") + def byIdMatch(): Unit = + Using(SQLiteDB()) { db => ExistsFunctions.byIdMatch(db) } + + @Test + @DisplayName("byId returns false when no document matches the ID") + def byIdNoMatch(): Unit = + Using(SQLiteDB()) { db => ExistsFunctions.byIdNoMatch(db) } + + @Test + @DisplayName("byFields returns true when documents match") + def byFieldsMatch(): Unit = + Using(SQLiteDB()) { db => ExistsFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields returns false when no documents match") + def byFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => ExistsFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byContains fails") + def byContainsFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => ExistsFunctions.byContainsMatch(db)) } + + @Test + @DisplayName("byJsonPath fails") + def byJsonPathFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => ExistsFunctions.byJsonPathMatch(db)) } diff --git a/src/scala/src/test/scala/integration/SQLiteFindIT.scala b/src/scala/src/test/scala/integration/SQLiteFindIT.scala new file mode 100644 index 0000000..c99bcdf --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteFindIT.scala @@ -0,0 +1,127 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +/** + * SQLite integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("Scala | SQLite: Find") +class SQLiteFindIT: + + @Test + @DisplayName("all retrieves all documents") + def allDefault(): Unit = + Using(SQLiteDB()) { db => FindFunctions.allDefault(db) } + + @Test + @DisplayName("all sorts data ascending") + def allAscending(): Unit = + Using(SQLiteDB()) { db => FindFunctions.allAscending(db) } + + @Test + @DisplayName("all sorts data descending") + def allDescending(): Unit = + Using(SQLiteDB()) { db => FindFunctions.allDescending(db) } + + @Test + @DisplayName("all sorts data numerically") + def allNumOrder(): Unit = + Using(SQLiteDB()) { db => FindFunctions.allNumOrder(db) } + + @Test + @DisplayName("all succeeds with an empty table") + def allEmpty(): Unit = + Using(SQLiteDB()) { db => FindFunctions.allEmpty(db) } + + @Test + @DisplayName("byId retrieves a document via a string ID") + def byIdString(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byIdString(db) } + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + def byIdNumber(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byIdNumber(db) } + + @Test + @DisplayName("byId returns null when a matching ID is not found") + def byIdNotFound(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byIdNotFound(db) } + + @Test + @DisplayName("byFields retrieves matching documents") + def byFieldsMatch(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields retrieves ordered matching documents") + def byFieldsMatchOrdered(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byFieldsMatchOrdered(db) } + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + def byFieldsMatchNumIn(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byFieldsMatchNumIn(db) } + + @Test + @DisplayName("byFields succeeds when no documents match") + def byFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + def byFieldsMatchInArray(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byFieldsMatchInArray(db) } + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + def byFieldsNoMatchInArray(): Unit = + Using(SQLiteDB()) { db => FindFunctions.byFieldsNoMatchInArray(db) } + + @Test + @DisplayName("byContains fails") + def byContainsFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => FindFunctions.byContainsMatch(db)) } + + @Test + @DisplayName("byJsonPath fails") + def byJsonPathFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => FindFunctions.byJsonPathMatch(db)) } + + @Test + @DisplayName("firstByFields retrieves a matching document") + def firstByFieldsMatchOne(): Unit = + Using(SQLiteDB()) { db => FindFunctions.firstByFieldsMatchOne(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + def firstByFieldsMatchMany(): Unit = + Using(SQLiteDB()) { db => FindFunctions.firstByFieldsMatchMany(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + def firstByFieldsMatchOrdered(): Unit = + Using(SQLiteDB()) { db => FindFunctions.firstByFieldsMatchOrdered(db) } + + @Test + @DisplayName("firstByFields returns null when no document matches") + def firstByFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => FindFunctions.firstByFieldsNoMatch(db) } + + @Test + @DisplayName("firstByContains fails") + def firstByContainsFails(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => FindFunctions.firstByContainsMatchOne(db)) + } + + @Test + @DisplayName("firstByJsonPath fails") + def firstByJsonPathFails(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => FindFunctions.firstByJsonPathMatchOne(db)) + } diff --git a/src/scala/src/test/scala/integration/SQLiteJsonIT.scala b/src/scala/src/test/scala/integration/SQLiteJsonIT.scala new file mode 100644 index 0000000..1de44ef --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteJsonIT.scala @@ -0,0 +1,211 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +/** + * SQLite integration tests for the `Json` object / `json*` connection extension functions + */ +@DisplayName("Scala | SQLite: Json") +class SQLiteJsonIT: + + @Test + @DisplayName("all retrieves all documents") + def allDefault(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.allDefault(db) } + + @Test + @DisplayName("all succeeds with an empty table") + def allEmpty(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.allEmpty(db) } + + @Test + @DisplayName("byId retrieves a document via a string ID") + def byIdString(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byIdString(db) } + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + def byIdNumber(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byIdNumber(db) } + + @Test + @DisplayName("byId returns null when a matching ID is not found") + def byIdNotFound(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byIdNotFound(db) } + + @Test + @DisplayName("byFields retrieves matching documents") + def byFieldsMatch(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields retrieves ordered matching documents") + def byFieldsMatchOrdered(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byFieldsMatchOrdered(db) } + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + def byFieldsMatchNumIn(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byFieldsMatchNumIn(db) } + + @Test + @DisplayName("byFields succeeds when no documents match") + def byFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + def byFieldsMatchInArray(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byFieldsMatchInArray(db) } + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + def byFieldsNoMatchInArray(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.byFieldsNoMatchInArray(db) } + + @Test + @DisplayName("byContains fails") + def byContainsFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => JsonFunctions.byContainsMatch(db)) } + + @Test + @DisplayName("byJsonPath fails") + def byJsonPathFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => JsonFunctions.byJsonPathMatch(db)) } + + @Test + @DisplayName("firstByFields retrieves a matching document") + def firstByFieldsMatchOne(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.firstByFieldsMatchOne(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + def firstByFieldsMatchMany(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.firstByFieldsMatchMany(db) } + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + def firstByFieldsMatchOrdered(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.firstByFieldsMatchOrdered(db) } + + @Test + @DisplayName("firstByFields returns null when no document matches") + def firstByFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.firstByFieldsNoMatch(db) } + + @Test + @DisplayName("firstByContains fails") + def firstByContainsFails(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => JsonFunctions.firstByContainsMatchOne(db)) + } + + @Test + @DisplayName("firstByJsonPath fails") + def firstByJsonPathFails(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => JsonFunctions.firstByJsonPathMatchOne(db)) + } + + @Test + @DisplayName("writeAll retrieves all documents") + def writeAllDefault(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeAllDefault(db) } + + @Test + @DisplayName("writeAll succeeds with an empty table") + def writeAllEmpty(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeAllEmpty(db) } + + @Test + @DisplayName("writeById retrieves a document via a string ID") + def writeByIdString(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByIdString(db) } + + @Test + @DisplayName("writeById retrieves a document via a numeric ID") + def writeByIdNumber(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByIdNumber(db) } + + @Test + @DisplayName("writeById returns null when a matching ID is not found") + def writeByIdNotFound(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByIdNotFound(db) } + + @Test + @DisplayName("writeByFields retrieves matching documents") + def writeByFieldsMatch(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByFieldsMatch(db) } + + @Test + @DisplayName("writeByFields retrieves ordered matching documents") + def writeByFieldsMatchOrdered(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByFieldsMatchOrdered(db) } + + @Test + @DisplayName("writeByFields retrieves matching documents with a numeric IN clause") + def writeByFieldsMatchNumIn(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByFieldsMatchNumIn(db) } + + @Test + @DisplayName("writeByFields succeeds when no documents match") + def writeByFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByFieldsNoMatch(db) } + + @Test + @DisplayName("writeByFields retrieves matching documents with an IN_ARRAY comparison") + def writeByFieldsMatchInArray(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByFieldsMatchInArray(db) } + + @Test + @DisplayName("writeByFields succeeds when no documents match an IN_ARRAY comparison") + def writeByFieldsNoMatchInArray(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeByFieldsNoMatchInArray(db) } + + @Test + @DisplayName("writeByContains fails") + def writeByContainsFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => JsonFunctions.writeByContainsMatch(db)) } + + @Test + @DisplayName("writeByJsonPath fails") + def writeByJsonPathFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => JsonFunctions.writeByJsonPathMatch(db)) } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document") + def writeFirstByFieldsMatchOne(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeFirstByFieldsMatchOne(db) } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many") + def writeFirstByFieldsMatchMany(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeFirstByFieldsMatchMany(db) } + + @Test + @DisplayName("writeFirstByFields retrieves a matching document among many (ordered)") + def writeFirstByFieldsMatchOrdered(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeFirstByFieldsMatchOrdered(db) } + + @Test + @DisplayName("writeFirstByFields returns null when no document matches") + def writeFirstByFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => JsonFunctions.writeFirstByFieldsNoMatch(db) } + + @Test + @DisplayName("writeFirstByContains fails") + def writeFirstByContainsFails(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => JsonFunctions.writeFirstByContainsMatchOne(db)) + } + + @Test + @DisplayName("writeFirstByJsonPath fails") + def writeFirstByJsonPathFails(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => JsonFunctions.writeFirstByJsonPathMatchOne(db)) + } diff --git a/src/scala/src/test/scala/integration/SQLitePatchIT.scala b/src/scala/src/test/scala/integration/SQLitePatchIT.scala new file mode 100644 index 0000000..fb77238 --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLitePatchIT.scala @@ -0,0 +1,44 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} +import org.junit.jupiter.api.Assertions.assertThrows +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +/** + * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("Scala | SQLite: Patch") +class SQLitePatchIT: + + @Test + @DisplayName("byId patches an existing document") + def byIdMatch(): Unit = + Using(SQLiteDB()) { db => PatchFunctions.byIdMatch(db) } + + @Test + @DisplayName("byId succeeds for a non-existent document") + def byIdNoMatch(): Unit = + Using(SQLiteDB()) { db => PatchFunctions.byIdNoMatch(db) } + + @Test + @DisplayName("byFields patches matching document") + def byFieldsMatch(): Unit = + Using(SQLiteDB()) { db => PatchFunctions.byFieldsMatch(db) } + + @Test + @DisplayName("byFields succeeds when no documents match") + def byFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => PatchFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byContains fails") + def byContainsFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => PatchFunctions.byContainsMatch(db)) } + + @Test + @DisplayName("byJsonPath fails") + def byJsonPathFails(): Unit = + Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => PatchFunctions.byJsonPathMatch(db)) } + diff --git a/src/scala/src/test/scala/integration/SQLiteRemoveFieldsIT.scala b/src/scala/src/test/scala/integration/SQLiteRemoveFieldsIT.scala new file mode 100644 index 0000000..0d8a679 --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteRemoveFieldsIT.scala @@ -0,0 +1,57 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.{DisplayName, Test} +import org.junit.jupiter.api.Assertions.assertThrows +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +/** + * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("Scala | SQLite: RemoveFields") +class SQLiteRemoveFieldsIT: + + @Test + @DisplayName("byId removes fields from an existing document") + def byIdMatchFields(): Unit = + Using(SQLiteDB()) { db => RemoveFieldsFunctions.byIdMatchFields(db) } + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + def byIdMatchNoFields(): Unit = + Using(SQLiteDB()) { db => RemoveFieldsFunctions.byIdMatchNoFields(db) } + + @Test + @DisplayName("byId succeeds when no document exists") + def byIdNoMatch(): Unit = + Using(SQLiteDB()) { db => RemoveFieldsFunctions.byIdNoMatch(db) } + + @Test + @DisplayName("byFields removes fields from matching documents") + def byFieldsMatchFields(): Unit = + Using(SQLiteDB()) { db => RemoveFieldsFunctions.byFieldsMatchFields(db) } + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + def byFieldsMatchNoFields(): Unit = + Using(SQLiteDB()) { db => RemoveFieldsFunctions.byFieldsMatchNoFields(db) } + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + def byFieldsNoMatch(): Unit = + Using(SQLiteDB()) { db => RemoveFieldsFunctions.byFieldsNoMatch(db) } + + @Test + @DisplayName("byContains fails") + def byContainsFails(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => RemoveFieldsFunctions.byContainsMatchFields(db)) + } + + @Test + @DisplayName("byJsonPath fails") + def byJsonPathFails(): Unit = + Using(SQLiteDB()) { db => + assertThrows(classOf[DocumentException], () => RemoveFieldsFunctions.byJsonPathMatchFields(db)) + } diff --git a/src/scala/src/test/scala/integration/SubDocument.scala b/src/scala/src/test/scala/integration/SubDocument.scala new file mode 100644 index 0000000..66f65b5 --- /dev/null +++ b/src/scala/src/test/scala/integration/SubDocument.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests.integration + +class SubDocument(val foo: String = "", val bar: String = "") diff --git a/src/scala/src/test/scala/integration/ThrowawayDatabase.scala b/src/scala/src/test/scala/integration/ThrowawayDatabase.scala new file mode 100644 index 0000000..4d12442 --- /dev/null +++ b/src/scala/src/test/scala/integration/ThrowawayDatabase.scala @@ -0,0 +1,28 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.java.DocumentConfig + +import java.sql.Connection + +/** + * Common trait for PostgreSQL and SQLite throwaway databases + */ +trait ThrowawayDatabase extends AutoCloseable: + + /** The database connection for the throwaway database */ + def conn: Connection + + /** + * Determine if a database object exists + * + * @param name The name of the object whose existence should be checked + * @return True if the object exists, false if not + */ + def dbObjectExists(name: String): Boolean + + /** The name for the throwaway database */ + val dbName = s"throwaway_${AutoId.generateRandomString(8)}" + + // Use a Jackson-based document serializer for testing + DocumentConfig.setSerializer(JacksonDocumentSerializer()) diff --git a/src/scala/src/test/scala/package.scala b/src/scala/src/test/scala/package.scala new file mode 100644 index 0000000..dc80f93 --- /dev/null +++ b/src/scala/src/test/scala/package.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +def TEST_TABLE = "test_table"