From 93c1b2740d7abf0e13da37d6336e7fa66e2551b4 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 13 Feb 2025 12:16:40 -0500 Subject: [PATCH 01/88] Migrate common lib (auto ID pending) --- .gitignore | 3 + solutions.bitbadger.documents.iml | 8 + src/common/pom.xml | 88 ++++++++++ src/common/src/main/kotlin/AutoId.kt | 39 +++++ src/common/src/main/kotlin/Comparison.kt | 9 ++ src/common/src/main/kotlin/Configuration.kt | 15 ++ src/common/src/main/kotlin/Dialect.kt | 11 ++ src/common/src/main/kotlin/Field.kt | 132 +++++++++++++++ src/common/src/main/kotlin/FieldFormat.kt | 11 ++ src/common/src/main/kotlin/FieldMatch.kt | 11 ++ src/common/src/main/kotlin/Main.kt | 14 ++ src/common/src/main/kotlin/Op.kt | 29 ++++ src/common/src/main/kotlin/ParameterName.kt | 18 +++ src/common/src/main/kotlin/Query.kt | 170 ++++++++++++++++++++ 14 files changed, 558 insertions(+) create mode 100644 solutions.bitbadger.documents.iml create mode 100644 src/common/pom.xml create mode 100644 src/common/src/main/kotlin/AutoId.kt create mode 100644 src/common/src/main/kotlin/Comparison.kt create mode 100644 src/common/src/main/kotlin/Configuration.kt create mode 100644 src/common/src/main/kotlin/Dialect.kt create mode 100644 src/common/src/main/kotlin/Field.kt create mode 100644 src/common/src/main/kotlin/FieldFormat.kt create mode 100644 src/common/src/main/kotlin/FieldMatch.kt create mode 100644 src/common/src/main/kotlin/Main.kt create mode 100644 src/common/src/main/kotlin/Op.kt create mode 100644 src/common/src/main/kotlin/ParameterName.kt create mode 100644 src/common/src/main/kotlin/Query.kt diff --git a/.gitignore b/.gitignore index 0296a22..e36da4c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ 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 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/common/pom.xml b/src/common/pom.xml new file mode 100644 index 0000000..2bc4cdc --- /dev/null +++ b/src/common/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + solutions.bitbadger.documents + common + 4.0-ALPHA + + + UTF-8 + official + 1.8 + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + 2.1.0 + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + 2.1.0 + test + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.jetbrains.kotlin + kotlin-stdlib + 2.1.0 + + + + \ No newline at end of file diff --git a/src/common/src/main/kotlin/AutoId.kt b/src/common/src/main/kotlin/AutoId.kt new file mode 100644 index 0000000..f42e5a0 --- /dev/null +++ b/src/common/src/main/kotlin/AutoId.kt @@ -0,0 +1,39 @@ +package solutions.bitbadger.documents.common + +/** + * 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 + */ + fun generateUUID(): String = + java.util.UUID.randomUUID().toString().replace("-", "") + + /** + * Generate a string of random hex characters + * + * @param length The length of the string + * @return A string of random hex characters of the requested length + */ + fun generateRandomString(length: Int): String = + kotlin.random.Random.nextBytes((length + 2) / 2) + .joinToString("") { String.format("%02x", it) } + .substring(0, length - 1) + + // TODO: fun needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean + } +} diff --git a/src/common/src/main/kotlin/Comparison.kt b/src/common/src/main/kotlin/Comparison.kt new file mode 100644 index 0000000..d6f7c24 --- /dev/null +++ b/src/common/src/main/kotlin/Comparison.kt @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.common + +/** + * A comparison against a field in a JSON document + * + * @property op The operation for the field comparison + * @property value The value against which the comparison will be made + */ +class Comparison(val op: Op, val value: T) diff --git a/src/common/src/main/kotlin/Configuration.kt b/src/common/src/main/kotlin/Configuration.kt new file mode 100644 index 0000000..1d035f0 --- /dev/null +++ b/src/common/src/main/kotlin/Configuration.kt @@ -0,0 +1,15 @@ +package solutions.bitbadger.documents.common + +object Configuration { + + // TODO: var jsonOpts = Json { some cool options } + + /** The field in which a document's ID is stored */ + var idField = "Id" + + /** The automatic ID strategy to use */ + var autoIdStrategy = AutoId.DISABLED + + /** The length of automatic random hex character string */ + var idStringLength = 16 +} diff --git a/src/common/src/main/kotlin/Dialect.kt b/src/common/src/main/kotlin/Dialect.kt new file mode 100644 index 0000000..bdf686c --- /dev/null +++ b/src/common/src/main/kotlin/Dialect.kt @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.common + +/** + * The SQL dialect to use when building queries + */ +enum class Dialect { + /** PostgreSQL */ + POSTGRESQL, + /** SQLite */ + SQLITE +} diff --git a/src/common/src/main/kotlin/Field.kt b/src/common/src/main/kotlin/Field.kt new file mode 100644 index 0000000..65db8ea --- /dev/null +++ b/src/common/src/main/kotlin/Field.kt @@ -0,0 +1,132 @@ +package solutions.bitbadger.documents.common + +/** + * 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( + val name: String, + val comparison: Comparison, + val parameterName: String? = null, + val qualifier: String? = null) { + + /** + * 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 + */ + fun path(dialect: Dialect, format: FieldFormat = FieldFormat.SQL): String = + (if (qualifier == null) "" else "${qualifier}.") + nameToPath(name, dialect, format) + + companion object { + /** + * Create a field equality comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @return A `Field` with the given comparison + */ + fun Equal(name: String, value: T): Field = + Field(name, Comparison(Op.EQUAL, value)) + + /** + * Create a field greater-than comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @return A `Field` with the given comparison + */ + fun Greater(name: String, value: T): Field = + Field(name, Comparison(Op.GREATER, value)) + + /** + * 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 + * @return A `Field` with the given comparison + */ + fun GreaterOrEqual(name: String, value: T): Field = + Field(name, Comparison(Op.GREATER_OR_EQUAL, value)) + + /** + * Create a field less-than comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @return A `Field` with the given comparison + */ + fun Less(name: String, value: T): Field = + Field(name, Comparison(Op.LESS, value)) + + /** + * 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 + * @return A `Field` with the given comparison + */ + fun LessOrEqual(name: String, value: T): Field = + Field(name, Comparison(Op.LESS_OR_EQUAL, value)) + + /** + * Create a field inequality comparison + * + * @param name The name of the field to be compared + * @param value The value for the comparison + * @return A `Field` with the given comparison + */ + fun NotEqual(name: String, value: T): Field = + Field(name, Comparison(Op.NOT_EQUAL, value)) + + /** + * 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 + * @return A `Field` with the given comparison + */ + fun Between(name: String, minValue: T, maxValue: T): Field> = + Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue))) + + fun In(name: String, values: List): Field> = + Field(name, Comparison(Op.IN, values)) + + fun InArray(name: String, tableName: String, values: List): Field>> = + Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values))) + + fun Exists(name: String): Field = + Field(name, Comparison(Op.EXISTS, "")) + + fun NotExists(name: String): Field = + Field(name, Comparison(Op.NOT_EXISTS, "")) + + fun Named(name: String): Field = + Field(name, Comparison(Op.EQUAL, "")) + + 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 = mutableListOf(name.split('.')) + val last = names.removeLast() + names.forEach { path.append("->'", it, "'") } + path.append("->", extra, "'", last, "'") + } + } else { + path.append("->", extra, "'", name, "'") + } + return path.toString() + } + } +} \ No newline at end of file diff --git a/src/common/src/main/kotlin/FieldFormat.kt b/src/common/src/main/kotlin/FieldFormat.kt new file mode 100644 index 0000000..02d4c20 --- /dev/null +++ b/src/common/src/main/kotlin/FieldFormat.kt @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.common + +/** + * 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/common/src/main/kotlin/FieldMatch.kt b/src/common/src/main/kotlin/FieldMatch.kt new file mode 100644 index 0000000..13a7610 --- /dev/null +++ b/src/common/src/main/kotlin/FieldMatch.kt @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.common + +/** + * How fields should be matched in by-field queries + */ +enum class FieldMatch(sql: String) { + /** Match any of the field criteria (`OR`) */ + ANY("OR"), + /** Match all the field criteria (`AND`) */ + ALL("AND"), +} diff --git a/src/common/src/main/kotlin/Main.kt b/src/common/src/main/kotlin/Main.kt new file mode 100644 index 0000000..7265465 --- /dev/null +++ b/src/common/src/main/kotlin/Main.kt @@ -0,0 +1,14 @@ +//TIP To Run code, press or +// click the icon in the gutter. +fun main() { + val name = "Kotlin" + //TIP Press with your caret at the highlighted text + // to see how IntelliJ IDEA suggests fixing it. + println("Hello, " + name + "!") + + for (i in 1..5) { + //TIP Press to start debugging your code. We have set one breakpoint + // for you, but you can always add more by pressing . + println("i = $i") + } +} \ No newline at end of file diff --git a/src/common/src/main/kotlin/Op.kt b/src/common/src/main/kotlin/Op.kt new file mode 100644 index 0000000..0fbf185 --- /dev/null +++ b/src/common/src/main/kotlin/Op.kt @@ -0,0 +1,29 @@ +package solutions.bitbadger.documents.common + +/** + * A comparison operator used for fields + */ +enum class Op(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/common/src/main/kotlin/ParameterName.kt b/src/common/src/main/kotlin/ParameterName.kt new file mode 100644 index 0000000..566dca0 --- /dev/null +++ b/src/common/src/main/kotlin/ParameterName.kt @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.common + +/** + * 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/common/src/main/kotlin/Query.kt b/src/common/src/main/kotlin/Query.kt new file mode 100644 index 0000000..4eae107 --- /dev/null +++ b/src/common/src/main/kotlin/Query.kt @@ -0,0 +1,170 @@ +package solutions.bitbadger.documents.common + +object Query { + + /** + * 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): String = + "$statement WHERE $where" + + object Definition { + + /** + * 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 + */ + fun ensureTableFor(tableName: String, dataType: String): String = + "CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)" + + /** + * 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): Pair { + val parts = tableName.split('.') + return if (parts.size == 1) Pair("", tableName) else Pair(parts[0], parts[1]) + } + + /** + * SQL statement to create an index on one or more fields in a JSON document + * + * @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 + * @return A query to create the field index + */ + fun ensureIndexOn(tableName: String, indexName: String, fields: List, dialect: Dialect): String { + val (_, tbl) = splitSchemaAndTable(tableName) + val jsonFields = fields.joinToString(", ") { + val parts = it.split(' ') + val direction = if (parts.size > 1) " ${parts[1]}" else "" + "(" + Field.nameToPath(parts[0], dialect, 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 + * @return A query to create the key index + */ + fun ensureKey(tableName: String, dialect: Dialect): String = + ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX") + } + + /** + * Query to insert a document + * + * @param tableName The table into which to insert (may include schema) + * @return A query to insert a document + */ + fun insert(tableName: String): String = + "INSERT INTO $tableName VALUES (:data)" + + /** + * 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 + */ + fun save(tableName: String): String = + String.format("INSERT INTO %s VALUES (:data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data", + tableName, Configuration.idField) + + /** + * Query to count documents in a table (this query has no `WHERE` clause) + * + * @param tableName The table in which to count documents (may include schema) + * @return A query to count documents + */ + fun count(tableName: String): String = + "SELECT COUNT(*) AS it FROM $tableName" + + /** + * 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 + */ + fun exists(tableName: String, where: String): String = + "SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it" + + /** + * Query to select documents from a table (this query has no `WHERE` clause) + * + * @param tableName The table from which documents should be found (may include schema) + * @return A query to retrieve documents + */ + fun find(tableName: String): String = + "SELECT data FROM $tableName" + + /** + * 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 + */ + fun update(tableName: String): String = + "UPDATE $tableName SET data = :data" + + /** + * Query to delete documents from a table (this query has no `WHERE` clause) + * + * @param tableName The table in which documents should be deleted (may include schema) + * @return A query to delete documents + */ + fun delete(tableName: String): String = + "DELETE FROM $tableName" + + /** + * 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 + */ + fun orderBy(fields: List>, dialect: Dialect): String { + 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 = + if (field.name.startsWith("n:")) { + val fld = Field.Named(field.name.substring(2)) + when (dialect) { + Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric" + Dialect.SQLITE -> fld.path(dialect) + } + } else if (field.name.startsWith("i:")) { + val p = Field.Named(field.name.substring(2)).path(dialect) + when (dialect) { + Dialect.POSTGRESQL -> "LOWER($p)" + Dialect.SQLITE -> "$p COLLATE NOCASE" + } + } else { + field.path(dialect) + } + "$path${direction ?: ""}" + } + return "ORDER BY $orderFields" + } +} -- 2.47.2 From 359e09bd5299c8bf0537b31bf2c0d4e6c95c6772 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 13 Feb 2025 17:56:59 -0500 Subject: [PATCH 02/88] WIP on tests for common module --- src/common/pom.xml | 6 ++ src/common/src/main/kotlin/Op.kt | 2 +- src/common/src/test/kotlin/AutoIdTest.kt | 17 ++++++ src/common/src/test/kotlin/FieldTest.kt | 20 +++++++ src/common/src/test/kotlin/OpTest.kt | 74 ++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/common/src/test/kotlin/AutoIdTest.kt create mode 100644 src/common/src/test/kotlin/FieldTest.kt create mode 100644 src/common/src/test/kotlin/OpTest.kt diff --git a/src/common/pom.xml b/src/common/pom.xml index 2bc4cdc..110ad24 100644 --- a/src/common/pom.xml +++ b/src/common/pom.xml @@ -66,6 +66,12 @@ + + org.junit.jupiter + junit-jupiter + 5.11.1 + test + org.jetbrains.kotlin kotlin-test-junit5 diff --git a/src/common/src/main/kotlin/Op.kt b/src/common/src/main/kotlin/Op.kt index 0fbf185..89f6139 100644 --- a/src/common/src/main/kotlin/Op.kt +++ b/src/common/src/main/kotlin/Op.kt @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.common /** * A comparison operator used for fields */ -enum class Op(sql: String) { +enum class Op(val sql: String) { /** Compare using equality */ EQUAL("="), /** Compare using greater-than */ diff --git a/src/common/src/test/kotlin/AutoIdTest.kt b/src/common/src/test/kotlin/AutoIdTest.kt new file mode 100644 index 0000000..9552a81 --- /dev/null +++ b/src/common/src/test/kotlin/AutoIdTest.kt @@ -0,0 +1,17 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class AutoIdTest { + + @Test + @DisplayName("Generates a UUID string") + fun testGenerateUUID() { + val generated = AutoId.generateUUID() + assertNotNull(generated, "The UUID string should not have been null") + assertEquals(32, generated.length, "The UUID should have been a 32-character string") + } +} \ No newline at end of file diff --git a/src/common/src/test/kotlin/FieldTest.kt b/src/common/src/test/kotlin/FieldTest.kt new file mode 100644 index 0000000..9a74bae --- /dev/null +++ b/src/common/src/test/kotlin/FieldTest.kt @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class FieldTest { + + @Test + @DisplayName("Equal constructs a field") + 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") + } +} diff --git a/src/common/src/test/kotlin/OpTest.kt b/src/common/src/test/kotlin/OpTest.kt new file mode 100644 index 0000000..6656928 --- /dev/null +++ b/src/common/src/test/kotlin/OpTest.kt @@ -0,0 +1,74 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +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") + } +} -- 2.47.2 From a576a876e0b818a9d2bd033c235de6e5c48a1098 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 13 Feb 2025 22:48:50 -0500 Subject: [PATCH 03/88] WIP on common tests --- src/common/src/main/kotlin/Field.kt | 59 +++- src/common/src/main/kotlin/FieldMatch.kt | 2 +- src/common/src/main/kotlin/Query.kt | 6 +- src/common/src/test/kotlin/FieldMatchTest.kt | 20 ++ src/common/src/test/kotlin/FieldTest.kt | 261 +++++++++++++++++- .../src/test/kotlin/ParameterNameTest.kt | 26 ++ 6 files changed, 355 insertions(+), 19 deletions(-) create mode 100644 src/common/src/test/kotlin/FieldMatchTest.kt create mode 100644 src/common/src/test/kotlin/ParameterNameTest.kt diff --git a/src/common/src/main/kotlin/Field.kt b/src/common/src/main/kotlin/Field.kt index 65db8ea..214b2e3 100644 --- a/src/common/src/main/kotlin/Field.kt +++ b/src/common/src/main/kotlin/Field.kt @@ -14,6 +14,24 @@ class Field( val parameterName: String? = null, val qualifier: String? = null) { + /** + * 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 = + 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 = + Field(name, comparison, parameterName, alias) + /** * Get the path for this field * @@ -32,7 +50,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun Equal(name: String, value: T): Field = + fun equal(name: String, value: T): Field = Field(name, Comparison(Op.EQUAL, value)) /** @@ -42,7 +60,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun Greater(name: String, value: T): Field = + fun greater(name: String, value: T): Field = Field(name, Comparison(Op.GREATER, value)) /** @@ -52,7 +70,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun GreaterOrEqual(name: String, value: T): Field = + fun greaterOrEqual(name: String, value: T): Field = Field(name, Comparison(Op.GREATER_OR_EQUAL, value)) /** @@ -62,7 +80,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun Less(name: String, value: T): Field = + fun less(name: String, value: T): Field = Field(name, Comparison(Op.LESS, value)) /** @@ -72,7 +90,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun LessOrEqual(name: String, value: T): Field = + fun lessOrEqual(name: String, value: T): Field = Field(name, Comparison(Op.LESS_OR_EQUAL, value)) /** @@ -82,7 +100,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun NotEqual(name: String, value: T): Field = + fun notEqual(name: String, value: T): Field = Field(name, Comparison(Op.NOT_EQUAL, value)) /** @@ -93,22 +111,37 @@ class Field( * @param maxValue The upper value for the comparison * @return A `Field` with the given comparison */ - fun Between(name: String, minValue: T, maxValue: T): Field> = + fun between(name: String, minValue: T, maxValue: T): Field> = Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue))) - fun In(name: String, values: List): Field> = + /** + * 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 + * @return A `Field` with the given comparison + */ + fun any(name: String, values: List): Field> = Field(name, Comparison(Op.IN, values)) - fun InArray(name: String, tableName: String, values: List): Field>> = + /** + * 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 + * @return A `Field` with the given comparison + */ + fun inArray(name: String, tableName: String, values: List): Field>> = Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values))) - fun Exists(name: String): Field = + fun exists(name: String): Field = Field(name, Comparison(Op.EXISTS, "")) - fun NotExists(name: String): Field = + fun notExists(name: String): Field = Field(name, Comparison(Op.NOT_EXISTS, "")) - fun Named(name: String): Field = + fun named(name: String): Field = Field(name, Comparison(Op.EQUAL, "")) fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String { @@ -118,7 +151,7 @@ class Field( if (dialect == Dialect.POSTGRESQL) { path.append("#>", extra, "'{", name.replace('.', ','), "}'") } else { - val names = mutableListOf(name.split('.')) + val names = name.split('.').toMutableList() val last = names.removeLast() names.forEach { path.append("->'", it, "'") } path.append("->", extra, "'", last, "'") diff --git a/src/common/src/main/kotlin/FieldMatch.kt b/src/common/src/main/kotlin/FieldMatch.kt index 13a7610..e621dba 100644 --- a/src/common/src/main/kotlin/FieldMatch.kt +++ b/src/common/src/main/kotlin/FieldMatch.kt @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.common /** * How fields should be matched in by-field queries */ -enum class FieldMatch(sql: String) { +enum class FieldMatch(val sql: String) { /** Match any of the field criteria (`OR`) */ ANY("OR"), /** Match all the field criteria (`AND`) */ diff --git a/src/common/src/main/kotlin/Query.kt b/src/common/src/main/kotlin/Query.kt index 4eae107..28e553d 100644 --- a/src/common/src/main/kotlin/Query.kt +++ b/src/common/src/main/kotlin/Query.kt @@ -143,19 +143,19 @@ object Query { val (field, direction) = if (it.name.indexOf(' ') > -1) { val parts = it.name.split(' ') - Pair(Field.Named(parts[0]), " " + parts.drop(1).joinToString(" ")) + Pair(Field.named(parts[0]), " " + parts.drop(1).joinToString(" ")) } else { Pair, String?>(it, null) } val path = if (field.name.startsWith("n:")) { - val fld = Field.Named(field.name.substring(2)) + val fld = Field.named(field.name.substring(2)) when (dialect) { Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric" Dialect.SQLITE -> fld.path(dialect) } } else if (field.name.startsWith("i:")) { - val p = Field.Named(field.name.substring(2)).path(dialect) + val p = Field.named(field.name.substring(2)).path(dialect) when (dialect) { Dialect.POSTGRESQL -> "LOWER($p)" Dialect.SQLITE -> "$p COLLATE NOCASE" diff --git a/src/common/src/test/kotlin/FieldMatchTest.kt b/src/common/src/test/kotlin/FieldMatchTest.kt new file mode 100644 index 0000000..f73e99f --- /dev/null +++ b/src/common/src/test/kotlin/FieldMatchTest.kt @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +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/common/src/test/kotlin/FieldTest.kt b/src/common/src/test/kotlin/FieldTest.kt index 9a74bae..7c32a0d 100644 --- a/src/common/src/test/kotlin/FieldTest.kt +++ b/src/common/src/test/kotlin/FieldTest.kt @@ -8,13 +8,270 @@ import kotlin.test.assertNull class FieldTest { @Test - @DisplayName("Equal constructs a field") + @DisplayName("equal constructs a field") fun equalCtor() { - val field = Field.Equal("Test", 14) + 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("greater constructs a field") + 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("greaterOrEqual constructs a field") + 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("less constructs a field") + 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("lessOrEqual constructs a field") + 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("notEqual constructs a field") + 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("between constructs a field") + 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("any constructs a field") + fun inCtor() { + 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("inArray constructs a field") + 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("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("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") + } + + @Test + @DisplayName("withParameterName adjusts the parameter name") + fun withParameterName() { + assertEquals(":name", Field.equal("Bob", "Tom").withParameterName(":name").parameterName, + "Parameter name not filled correctly") + } + + @Test + @DisplayName("withQualifier adjust the table qualifier") + fun withQualifier() { + assertEquals("joe", Field.equal("Bill", "Matt").withQualifier("joe").qualifier, + "Qualifier not filled 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") + } } diff --git a/src/common/src/test/kotlin/ParameterNameTest.kt b/src/common/src/test/kotlin/ParameterNameTest.kt new file mode 100644 index 0000000..4c435aa --- /dev/null +++ b/src/common/src/test/kotlin/ParameterNameTest.kt @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +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 -- 2.47.2 From de4df4109c80c78a6ce79ba5c5dfe9bad91a43f4 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 14 Feb 2025 19:57:41 -0500 Subject: [PATCH 04/88] Implement/test Auto ID need --- src/common/pom.xml | 11 +- src/common/src/main/kotlin/AutoId.kt | 44 ++++- src/common/src/main/kotlin/Configuration.kt | 2 +- src/common/src/main/kotlin/Query.kt | 11 +- src/common/src/test/kotlin/AutoIdTest.kt | 155 ++++++++++++++- .../src/test/kotlin/ConfigurationTest.kt | 26 +++ src/common/src/test/kotlin/QueryTest.kt | 181 ++++++++++++++++++ 7 files changed, 409 insertions(+), 21 deletions(-) create mode 100644 src/common/src/test/kotlin/ConfigurationTest.kt create mode 100644 src/common/src/test/kotlin/QueryTest.kt diff --git a/src/common/pom.xml b/src/common/pom.xml index 110ad24..8c65730 100644 --- a/src/common/pom.xml +++ b/src/common/pom.xml @@ -78,17 +78,16 @@ 2.1.0 test - - org.junit.jupiter - junit-jupiter - 5.10.0 - test - org.jetbrains.kotlin kotlin-stdlib 2.1.0 + + org.jetbrains.kotlin + kotlin-reflect + 2.0.20 + \ No newline at end of file diff --git a/src/common/src/main/kotlin/AutoId.kt b/src/common/src/main/kotlin/AutoId.kt index f42e5a0..cf34802 100644 --- a/src/common/src/main/kotlin/AutoId.kt +++ b/src/common/src/main/kotlin/AutoId.kt @@ -1,5 +1,7 @@ package solutions.bitbadger.documents.common +import kotlin.reflect.full.* + /** * Strategies for automatic document IDs */ @@ -32,8 +34,46 @@ enum class AutoId { fun generateRandomString(length: Int): String = kotlin.random.Random.nextBytes((length + 2) / 2) .joinToString("") { String.format("%02x", it) } - .substring(0, length - 1) + .substring(0, length) - // TODO: fun needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean + /** + * 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 IllegalArgumentException If bad input prevents the determination + */ + fun needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean { + if (document == null) throw IllegalArgumentException("document cannot be null") + + if (strategy == DISABLED) return false; + + val id = document!!::class.memberProperties.find { it.name == idProp } + if (id == null) throw IllegalArgumentException("$idProp not found in document") + + if (strategy == NUMBER) { + if (id.returnType == Byte::class.createType()) { + return id.call(document) == 0.toByte() + } + if (id.returnType == Short::class.createType()) { + return id.call(document) == 0.toShort() + } + if (id.returnType == Int::class.createType()) { + return id.call(document) == 0 + } + if (id.returnType == Long::class.createType()) { + return id.call(document) == 0.toLong() + } + throw IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID") + } + + if (id.returnType == String::class.createType()) { + return id.call(document) == "" + } + + throw IllegalArgumentException("$idProp was not a string; cannot auto-generate UUID or random string") + } } } diff --git a/src/common/src/main/kotlin/Configuration.kt b/src/common/src/main/kotlin/Configuration.kt index 1d035f0..ca82639 100644 --- a/src/common/src/main/kotlin/Configuration.kt +++ b/src/common/src/main/kotlin/Configuration.kt @@ -5,7 +5,7 @@ object Configuration { // TODO: var jsonOpts = Json { some cool options } /** The field in which a document's ID is stored */ - var idField = "Id" + var idField = "id" /** The automatic ID strategy to use */ var autoIdStrategy = AutoId.DISABLED diff --git a/src/common/src/main/kotlin/Query.kt b/src/common/src/main/kotlin/Query.kt index 28e553d..31e6a67 100644 --- a/src/common/src/main/kotlin/Query.kt +++ b/src/common/src/main/kotlin/Query.kt @@ -44,12 +44,12 @@ object Query { * @param dialect The SQL dialect to use when creating this index * @return A query to create the field index */ - fun ensureIndexOn(tableName: String, indexName: String, fields: List, dialect: Dialect): String { + fun ensureIndexOn(tableName: String, indexName: String, fields: Collection, dialect: Dialect): String { val (_, tbl) = splitSchemaAndTable(tableName) val jsonFields = fields.joinToString(", ") { val parts = it.split(' ') val direction = if (parts.size > 1) " ${parts[1]}" else "" - "(" + Field.nameToPath(parts[0], dialect, FieldFormat.SQL) + ") $direction" + "(" + Field.nameToPath(parts[0], dialect, FieldFormat.SQL) + ")$direction" } return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)" } @@ -81,8 +81,7 @@ object Query { * @return A query to save a document */ fun save(tableName: String): String = - String.format("INSERT INTO %s VALUES (:data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data", - tableName, Configuration.idField) + "${insert(tableName)} ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data" /** * Query to count documents in a table (this query has no `WHERE` clause) @@ -137,7 +136,7 @@ object Query { * @param dialect The SQL dialect for the generated clause * @return An `ORDER BY` clause for the given fields */ - fun orderBy(fields: List>, dialect: Dialect): String { + fun orderBy(fields: Collection>, dialect: Dialect): String { if (fields.isEmpty()) return "" val orderFields = fields.joinToString(", ") { val (field, direction) = @@ -165,6 +164,6 @@ object Query { } "$path${direction ?: ""}" } - return "ORDER BY $orderFields" + return " ORDER BY $orderFields" } } diff --git a/src/common/src/test/kotlin/AutoIdTest.kt b/src/common/src/test/kotlin/AutoIdTest.kt index 9552a81..038d322 100644 --- a/src/common/src/test/kotlin/AutoIdTest.kt +++ b/src/common/src/test/kotlin/AutoIdTest.kt @@ -2,16 +2,159 @@ package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue class AutoIdTest { @Test @DisplayName("Generates a UUID string") - fun testGenerateUUID() { - val generated = AutoId.generateUUID() - assertNotNull(generated, "The UUID string should not have been null") - assertEquals(32, generated.length, "The UUID should have been a 32-character string") + fun generateUUID() { + assertEquals(32, AutoId.generateUUID().length, "The UUID should have been a 32-character string") } -} \ No newline at end of file + + @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, IntIdClass(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(var id: Byte) +data class ShortIdClass(var id: Short) +data class IntIdClass(var id: Int) +data class LongIdClass(var id: Long) +data class StringIdClass(var id: String) diff --git a/src/common/src/test/kotlin/ConfigurationTest.kt b/src/common/src/test/kotlin/ConfigurationTest.kt new file mode 100644 index 0000000..29a2a61 --- /dev/null +++ b/src/common/src/test/kotlin/ConfigurationTest.kt @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +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") + } +} diff --git a/src/common/src/test/kotlin/QueryTest.kt b/src/common/src/test/kotlin/QueryTest.kt new file mode 100644 index 0000000..565243d --- /dev/null +++ b/src/common/src/test/kotlin/QueryTest.kt @@ -0,0 +1,181 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class QueryTest { + + /** Test table name */ + private val tbl = "test_table" + + @Test + @DisplayName("statementWhere generates correctly") + fun statementWhere() { + assertEquals("x WHERE y", Query.statementWhere("x", "y"), "Statements not combined correctly") + } + + @Test + @DisplayName("Definition.ensureTableFor generates correctly") + fun ensureTableFor() { + assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", + Query.Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly") + } + + @Test + @DisplayName("Definition.ensureKey generates correctly with schema") + fun ensureKeyWithSchema() { + assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", + Query.Definition.ensureKey("test.table", Dialect.POSTGRESQL), + "CREATE INDEX for key statement with schema not constructed correctly") + } + + @Test + @DisplayName("Definition.ensureKey generates correctly without schema") + fun ensureKeyWithoutSchema() { + assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))", + Query.Definition.ensureKey(tbl, Dialect.SQLITE), + "CREATE INDEX for key statement without schema not constructed correctly") + } + + @Test + @DisplayName("Definition.ensureIndexOn generates multiple fields and directions") + fun ensureIndexOnMultipleFields() { + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", + Query.Definition.ensureIndexOn("test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"), + Dialect.POSTGRESQL), + "CREATE INDEX for multiple field statement not constructed correctly") + } + + @Test + @DisplayName("Definition.ensureIndexOn generates nested PostgreSQL field") + fun ensureIndexOnNestedPostgres() { + assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))", + Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL), + "CREATE INDEX for nested PostgreSQL field incorrect") + } + + @Test + @DisplayName("Definition.ensureIndexOn generates nested SQLite field") + fun ensureIndexOnNestedSQLite() { + assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))", + Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE), + "CREATE INDEX for nested SQLite field incorrect") + } + + @Test + @DisplayName("insert generates correctly") + fun insert() { + assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl), "INSERT statement not constructed correctly") + } + + @Test + @DisplayName("save generates correctly") + fun save() { + assertEquals("INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", + Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly") + } + + @Test + @DisplayName("count generates correctly") + fun count() { + assertEquals("SELECT COUNT(*) AS it FROM $tbl", Query.count(tbl), "Count query not constructed correctly") + } + + @Test + @DisplayName("exists generates correctly") + fun exists() { + assertEquals("SELECT EXISTS (SELECT 1 FROM $tbl WHERE turkey) AS it", Query.exists(tbl, "turkey"), + "Exists query not constructed correctly") + } + + @Test + @DisplayName("find generates correctly") + fun find() { + assertEquals("SELECT data FROM $tbl", Query.find(tbl), "Find query not constructed correctly") + } + + @Test + @DisplayName("update generates successfully") + fun update() { + assertEquals("UPDATE $tbl SET data = :data", Query.update(tbl), "Update query not constructed correctly") + } + + @Test + @DisplayName("delete generates successfully") + fun delete() { + assertEquals("DELETE FROM $tbl", Query.delete(tbl), "Delete query not constructed correctly") + } + + @Test + @DisplayName("orderBy generates for no fields") + fun orderByNone() { + assertEquals("", Query.orderBy(listOf(), Dialect.POSTGRESQL), "ORDER BY should have been blank (PostgreSQL)") + assertEquals("", Query.orderBy(listOf(), Dialect.SQLITE), "ORDER BY should have been blank (SQLite)") + } + + @Test + @DisplayName("orderBy generates single, no direction for PostgreSQL") + fun orderBySinglePostgres() { + assertEquals(" ORDER BY data->>'TestField'", + Query.orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly") + } + + @Test + @DisplayName("orderBy generates single, no direction for SQLite") + fun orderBySingleSQLite() { + assertEquals(" ORDER BY data->>'TestField'", Query.orderBy(listOf(Field.named("TestField")), Dialect.SQLITE), + "ORDER BY not constructed correctly") + } + + @Test + @DisplayName("orderBy generates multiple with direction for PostgreSQL") + fun orderByMultiplePostgres() { + assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + Query.orderBy( + listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")), + Dialect.POSTGRESQL), + "ORDER BY not constructed correctly") + } + + @Test + @DisplayName("orderBy generates multiple with direction for SQLite") + fun orderByMultipleSQLite() { + assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + Query.orderBy( + listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")), + Dialect.SQLITE), + "ORDER BY not constructed correctly") + } + + @Test + @DisplayName("orderBy generates numeric ordering PostgreSQL") + fun orderByNumericPostgres() { + assertEquals(" ORDER BY (data->>'Test')::numeric", + Query.orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly") + } + + @Test + @DisplayName("orderBy generates numeric ordering for SQLite") + fun orderByNumericSQLite() { + assertEquals(" ORDER BY data->>'Test'", Query.orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE), + "ORDER BY not constructed correctly") + } + + @Test + @DisplayName("orderBy generates case-insensitive ordering for PostgreSQL") + fun orderByCIPostgres() { + assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", + Query.orderBy(listOf(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL), + "ORDER BY not constructed correctly") + } + + @Test + @DisplayName("orderBy generates case-insensitive ordering for SQLite") + fun orderByCISQLite() { + assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", + Query.orderBy(listOf(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE), + "ORDER BY not constructed correctly") + } +} -- 2.47.2 From c35c41bf3cb3d56e999dfbe69398c55df2c2456d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 14 Feb 2025 23:42:24 -0500 Subject: [PATCH 05/88] WIP on SQLite implementation --- .idea/.gitignore | 8 ++ .idea/compiler.xml | 14 +++ .idea/encodings.xml | 9 ++ .idea/jarRepositories.xml | 25 +++++ .idea/kotlinc.xml | 6 ++ .idea/libraries/KotlinJavaRuntime.xml | 23 +++++ .idea/misc.xml | 15 +++ .idea/modules.xml | 8 ++ .idea/solutions.bitbadger.documents.iml | 10 ++ .idea/vcs.xml | 6 ++ src/sqlite/pom.xml | 99 +++++++++++++++++++ src/sqlite/src/main/kotlin/Configuration.kt | 26 +++++ src/sqlite/src/main/kotlin/Query.kt | 102 ++++++++++++++++++++ 13 files changed, 351 insertions(+) create mode 100644 .idea/.gitignore 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/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/solutions.bitbadger.documents.iml create mode 100644 .idea/vcs.xml create mode 100644 src/sqlite/pom.xml create mode 100644 src/sqlite/src/main/kotlin/Configuration.kt create mode 100644 src/sqlite/src/main/kotlin/Query.kt 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/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..0cf8482 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e98b3a0 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..4d27ef0 --- /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..c22b6fa --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ 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/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..161ad45 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..cf07591 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/solutions.bitbadger.documents.iml b/.idea/solutions.bitbadger.documents.iml new file mode 100644 index 0000000..6ec89fa --- /dev/null +++ b/.idea/solutions.bitbadger.documents.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ 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/src/sqlite/pom.xml b/src/sqlite/pom.xml new file mode 100644 index 0000000..4b69624 --- /dev/null +++ b/src/sqlite/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + solutions.bitbadger.documents + sqlite + 4.0-ALPHA + + + UTF-8 + official + 1.8 + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + 2.1.10 + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + 2.1.10 + test + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.jetbrains.kotlin + kotlin-stdlib + 2.1.10 + + + org.xerial + sqlite-jdbc + 3.46.1.2 + + + solutions.bitbadger.documents + common + 4.0-ALPHA + compile + + + + \ No newline at end of file diff --git a/src/sqlite/src/main/kotlin/Configuration.kt b/src/sqlite/src/main/kotlin/Configuration.kt new file mode 100644 index 0000000..b944d0d --- /dev/null +++ b/src/sqlite/src/main/kotlin/Configuration.kt @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.sqlite + +import java.sql.Connection +import java.sql.DriverManager + +/** + * Configuration for SQLite + */ +object Configuration { + + /** The connection string for the SQLite database */ + var connectionString: String? = null + + /** + * Retrieve a new connection to the SQLite database + * + * @return A new connection to the SQLite database + * @throws IllegalArgumentException If the connection string is not set before calling this + */ + fun dbConn(): Connection { + if (connectionString == null) { + throw IllegalArgumentException("Please provide a connection string before attempting data access") + } + return DriverManager.getConnection(connectionString) + } +} diff --git a/src/sqlite/src/main/kotlin/Query.kt b/src/sqlite/src/main/kotlin/Query.kt new file mode 100644 index 0000000..18863df --- /dev/null +++ b/src/sqlite/src/main/kotlin/Query.kt @@ -0,0 +1,102 @@ +package solutions.bitbadger.documents.sqlite + +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Query + +/** + * Queries with specific syntax in SQLite + */ +object Query { + + /** + * Create a `WHERE` clause fragment to implement a comparison on fields in a JSON document + * + * @param howMatched How the fields should be matched + * @param fields The fields for the comparisons + * @return A `WHERE` clause implementing the comparisons for the given fields + */ + fun whereByFields(howMatched: FieldMatch, fields: Collection>): String { + val name = ParameterName() + return fields.joinToString(" ${howMatched.sql} ") { + val comp = it.comparison + when (comp.op) { + Op.EXISTS, Op.NOT_EXISTS -> { + "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${it.comparison.op.sql}" + } + Op.BETWEEN -> { + val p = name.derive(it.parameterName) + "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${comp.op.sql} ${p}min AND ${p}max" + } + Op.IN -> { + val p = name.derive(it.parameterName) + val values = comp.value as List<*> + val paramNames = values.indices.joinToString(", ") { idx -> "${p}_$idx" } + "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${comp.op.sql} ($paramNames)" + } + Op.IN_ARRAY -> { + val p = name.derive(it.parameterName) + val (table, values) = comp.value as Pair> + val paramNames = values.indices.joinToString(", ") { idx -> "${p}_$idx" } + "EXISTS (SELECT 1 FROM json_each($table.data, '$.${it.name}') WHERE value IN ($paramNames)" + } + else -> { + "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${comp.op.sql} ${name.derive(it.parameterName)}" + } + } + } + } + + /** + * Create a `WHERE` clause fragment to implement an ID-based query + * + * @param docId The ID of the document + * @return A `WHERE` clause fragment identifying a document by its ID + */ + fun whereById(docId: TKey): String = + whereByFields(FieldMatch.ANY, listOf(Field.equal(Configuration.idField, docId).withParameterName(":id"))) + + /** + * Create an `UPDATE` statement to patch documents + * + * @param tableName The table to be updated + * @return A query to patch documents + */ + fun patch(tableName: String): String = + "UPDATE $tableName SET data = json_patch(data, json(:data))" + + // TODO: fun removeFields(tableName: String, fields: Collection): String + + /** + * 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 + */ + fun byId(statement: String, docId: TKey): String = + Query.statementWhere(statement, whereById(docId)) + + /** + * Create a query on JSON fields + * + * @param statement The SQL statement to be run against matching fields + * @param howMatched Whether to match any or all of the field conditions + * @param fields The field conditions to be matched + * @return A query addressing documents by field matching conditions + */ + fun byFields(statement: String, howMatched: FieldMatch, fields: Collection>): String = + Query.statementWhere(statement, whereByFields(howMatched, fields)) + + object Definition { + + /** + * SQL statement to create a document table + * + * @param tableName The name of the table (may include schema) + * @return A query to create the table if it does not exist + */ + fun ensureTable(tableName: String): String = + Query.Definition.ensureTableFor(tableName, "TEXT") + } +} -- 2.47.2 From 3826009dfa6c3acdc358fbdbf8534534e1689c8c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 16 Feb 2025 22:59:32 -0500 Subject: [PATCH 06/88] WIP on named-param queries --- src/common/pom.xml | 27 ++++- src/common/src/main/kotlin/AutoId.kt | 18 ++-- src/common/src/main/kotlin/Configuration.kt | 51 +++++++++- .../src/main/kotlin/ConnectionExtensions.kt | 40 ++++++++ .../src/main/kotlin/DocumentException.kt | 9 ++ src/common/src/main/kotlin/Parameter.kt | 15 +++ src/common/src/main/kotlin/ParameterType.kt | 13 +++ src/common/src/main/kotlin/Parameters.kt | 83 ++++++++++++++++ src/common/src/main/kotlin/Query.kt | 13 ++- src/common/src/main/kotlin/Results.kt | 75 ++++++++++++++ src/common/src/test/kotlin/ParameterTest.kt | 34 +++++++ src/common/src/test/kotlin/ParametersTest.kt | 18 ++++ src/sqlite/src/main/kotlin/Query.kt | 12 ++- src/sqlite/src/test/kotlin/QueryTest.kt | 99 +++++++++++++++++++ 14 files changed, 479 insertions(+), 28 deletions(-) create mode 100644 src/common/src/main/kotlin/ConnectionExtensions.kt create mode 100644 src/common/src/main/kotlin/DocumentException.kt create mode 100644 src/common/src/main/kotlin/Parameter.kt create mode 100644 src/common/src/main/kotlin/ParameterType.kt create mode 100644 src/common/src/main/kotlin/Parameters.kt create mode 100644 src/common/src/main/kotlin/Results.kt create mode 100644 src/common/src/test/kotlin/ParameterTest.kt create mode 100644 src/common/src/test/kotlin/ParametersTest.kt create mode 100644 src/sqlite/src/test/kotlin/QueryTest.kt diff --git a/src/common/pom.xml b/src/common/pom.xml index 8c65730..4b91d63 100644 --- a/src/common/pom.xml +++ b/src/common/pom.xml @@ -12,6 +12,8 @@ UTF-8 official 1.8 + 2.1.0 + 1.8.0 @@ -28,7 +30,7 @@ org.jetbrains.kotlin kotlin-maven-plugin - 2.1.0 + ${kotlin.version} compile @@ -45,6 +47,18 @@ + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + maven-surefire-plugin @@ -75,18 +89,23 @@ org.jetbrains.kotlin kotlin-test-junit5 - 2.1.0 + ${kotlin.version} test org.jetbrains.kotlin kotlin-stdlib - 2.1.0 + ${kotlin.version} org.jetbrains.kotlin kotlin-reflect - 2.0.20 + ${kotlin.version} + + + org.jetbrains.kotlinx + kotlinx-serialization-json + ${serialization.version} diff --git a/src/common/src/main/kotlin/AutoId.kt b/src/common/src/main/kotlin/AutoId.kt index cf34802..f152a8d 100644 --- a/src/common/src/main/kotlin/AutoId.kt +++ b/src/common/src/main/kotlin/AutoId.kt @@ -54,19 +54,13 @@ enum class AutoId { if (id == null) throw IllegalArgumentException("$idProp not found in document") if (strategy == NUMBER) { - if (id.returnType == Byte::class.createType()) { - return id.call(document) == 0.toByte() + 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 IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID") } - if (id.returnType == Short::class.createType()) { - return id.call(document) == 0.toShort() - } - if (id.returnType == Int::class.createType()) { - return id.call(document) == 0 - } - if (id.returnType == Long::class.createType()) { - return id.call(document) == 0.toLong() - } - throw IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID") } if (id.returnType == String::class.createType()) { diff --git a/src/common/src/main/kotlin/Configuration.kt b/src/common/src/main/kotlin/Configuration.kt index ca82639..34c5e93 100644 --- a/src/common/src/main/kotlin/Configuration.kt +++ b/src/common/src/main/kotlin/Configuration.kt @@ -1,8 +1,21 @@ package solutions.bitbadger.documents.common +import kotlinx.serialization.json.Json +import java.sql.Connection +import java.sql.DriverManager + object Configuration { - // TODO: var jsonOpts = Json { some cool options } + /** + * JSON serializer; replace to configure with non-default options + * + * The default sets `encodeDefaults` to `true` and `explicitNulls` to `false`; see + * https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md for all configuration options + */ + var json = Json { + encodeDefaults = true + explicitNulls = false + } /** The field in which a document's ID is stored */ var idField = "id" @@ -12,4 +25,40 @@ object Configuration { /** The length of automatic random hex character string */ var idStringLength = 16 + + /** The connection string for the JDBC connection */ + var connectionString: String? = null + + /** + * Retrieve a new connection to the configured database + * + * @return A new connection to the configured database + * @throws IllegalArgumentException If the connection string is not set before calling this + */ + fun dbConn(): Connection { + if (connectionString == null) { + throw IllegalArgumentException("Please provide a connection string before attempting data access") + } + return DriverManager.getConnection(connectionString) + } + + private var dialectValue: Dialect? = null + + /** The dialect in use */ + val dialect: Dialect + get() { + if (dialectValue == null) { + if (connectionString == null) { + throw IllegalArgumentException("Please provide a connection string before attempting data access") + } + val it = connectionString!! + dialectValue = when { + it.contains("sqlite") -> Dialect.SQLITE + it.contains("postgresql") -> Dialect.POSTGRESQL + else -> throw IllegalArgumentException("Cannot determine dialect from [$it]") + } + } + + return dialectValue!! + } } diff --git a/src/common/src/main/kotlin/ConnectionExtensions.kt b/src/common/src/main/kotlin/ConnectionExtensions.kt new file mode 100644 index 0000000..e18a7f4 --- /dev/null +++ b/src/common/src/main/kotlin/ConnectionExtensions.kt @@ -0,0 +1,40 @@ +package solutions.bitbadger.documents.common + +import java.sql.Connection +import java.sql.ResultSet + +/** + * 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>, + mapFunc: (ResultSet) -> TDoc): List = + Parameters.apply(this, query, parameters).use { stmt -> + Results.toCustomList(stmt, 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>, + mapFunc: (ResultSet) -> TDoc): TDoc? = + this.customList("$query LIMIT 1", parameters, mapFunc).singleOrNull() + +/** + * 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>) { + Parameters.apply(this, query, parameters).use { it.executeUpdate() } +} diff --git a/src/common/src/main/kotlin/DocumentException.kt b/src/common/src/main/kotlin/DocumentException.kt new file mode 100644 index 0000000..6901d2b --- /dev/null +++ b/src/common/src/main/kotlin/DocumentException.kt @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.common + +/** + * 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(message: String, cause: Throwable? = null) : Exception(message, cause) diff --git a/src/common/src/main/kotlin/Parameter.kt b/src/common/src/main/kotlin/Parameter.kt new file mode 100644 index 0000000..eca25b7 --- /dev/null +++ b/src/common/src/main/kotlin/Parameter.kt @@ -0,0 +1,15 @@ +package solutions.bitbadger.documents.common + +/** + * 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)") + } +} diff --git a/src/common/src/main/kotlin/ParameterType.kt b/src/common/src/main/kotlin/ParameterType.kt new file mode 100644 index 0000000..53d6e1b --- /dev/null +++ b/src/common/src/main/kotlin/ParameterType.kt @@ -0,0 +1,13 @@ +package solutions.bitbadger.documents.common + +/** + * 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/common/src/main/kotlin/Parameters.kt b/src/common/src/main/kotlin/Parameters.kt new file mode 100644 index 0000000..4675abc --- /dev/null +++ b/src/common/src/main/kotlin/Parameters.kt @@ -0,0 +1,83 @@ +package solutions.bitbadger.documents.common + +import java.sql.Connection +import java.sql.PreparedStatement +import java.sql.SQLException +import java.sql.Types + +/** + * Functions to assist with the creation and implementation of parameters for SQL queries + * + * @author Daniel J. Summers + */ +object Parameters { + + /** + * 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>) = + 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 + */ + 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) + .let { conn.prepareStatement(it) } + .also { stmt -> + replacements.sortedBy { it.first }.map { it.second }.forEachIndexed { index, param -> + val idx = index + 1 + when (param.type) { + ParameterType.NUMBER -> { + when (param.value) { + null -> stmt.setNull(idx, Types.NULL) + is Byte -> stmt.setByte(idx, param.value) + is Short -> stmt.setShort(idx, param.value) + is Int -> stmt.setInt(idx, param.value) + is Long -> stmt.setLong(idx, param.value) + else -> throw DocumentException( + "Number parameter must be Byte, Short, Int, or Long (${param.value::class.simpleName})") + } + } + ParameterType.STRING -> { + when (param.value) { + null -> stmt.setNull(idx, Types.NULL) + is String -> stmt.setString(idx, param.value) + else -> stmt.setString(idx, param.value.toString()) + } + } + ParameterType.JSON -> stmt.setString(idx, Configuration.json.encodeToString(param.value)) + } + } + } + } catch (ex: SQLException) { + throw DocumentException("Error creating query / binding parameters: ${ex.message}", ex) + } + } +} diff --git a/src/common/src/main/kotlin/Query.kt b/src/common/src/main/kotlin/Query.kt index 31e6a67..4a0dbad 100644 --- a/src/common/src/main/kotlin/Query.kt +++ b/src/common/src/main/kotlin/Query.kt @@ -146,22 +146,21 @@ object Query { } else { Pair, String?>(it, null) } - val path = - if (field.name.startsWith("n:")) { - val fld = Field.named(field.name.substring(2)) + val path = when { + field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld -> when (dialect) { Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric" Dialect.SQLITE -> fld.path(dialect) } - } else if (field.name.startsWith("i:")) { - val p = Field.named(field.name.substring(2)).path(dialect) + } + field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(dialect).let { p -> when (dialect) { Dialect.POSTGRESQL -> "LOWER($p)" Dialect.SQLITE -> "$p COLLATE NOCASE" } - } else { - field.path(dialect) } + else -> field.path(dialect) + } "$path${direction ?: ""}" } return " ORDER BY $orderFields" diff --git a/src/common/src/main/kotlin/Results.kt b/src/common/src/main/kotlin/Results.kt new file mode 100644 index 0000000..777a228 --- /dev/null +++ b/src/common/src/main/kotlin/Results.kt @@ -0,0 +1,75 @@ +package solutions.bitbadger.documents.common + +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 + * @param rs A `ResultSet` set to the row with the document to be constructed + * @return The constructed domain item + */ + inline fun fromDocument(field: String, rs: ResultSet): TDoc = + Configuration.json.decodeFromString(rs.getString(field)) + + /** + * Create a domain item from a document + * + * @param rs A `ResultSet` set to the row with the document to be constructed< + * @return The constructed domain item + */ + inline fun fromData(rs: ResultSet): TDoc = + 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): List = + 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): Long = + 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): Boolean = + when (Configuration.dialect) { + Dialect.POSTGRESQL -> rs.getBoolean("it") + Dialect.SQLITE -> toCount(rs) > 0L + } +} diff --git a/src/common/src/test/kotlin/ParameterTest.kt b/src/common/src/test/kotlin/ParameterTest.kt new file mode 100644 index 0000000..7cc17e3 --- /dev/null +++ b/src/common/src/test/kotlin/ParameterTest.kt @@ -0,0 +1,34 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +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/common/src/test/kotlin/ParametersTest.kt b/src/common/src/test/kotlin/ParametersTest.kt new file mode 100644 index 0000000..0394a12 --- /dev/null +++ b/src/common/src/test/kotlin/ParametersTest.kt @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test +import kotlin.test.assertEquals + +class ParametersTest { + + @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") + } +} diff --git a/src/sqlite/src/main/kotlin/Query.kt b/src/sqlite/src/main/kotlin/Query.kt index 18863df..df75709 100644 --- a/src/sqlite/src/main/kotlin/Query.kt +++ b/src/sqlite/src/main/kotlin/Query.kt @@ -1,7 +1,7 @@ package solutions.bitbadger.documents.sqlite import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Configuration as BaseConfig; import solutions.bitbadger.documents.common.Query /** @@ -30,13 +30,16 @@ object Query { } Op.IN -> { val p = name.derive(it.parameterName) - val values = comp.value as List<*> + val values = comp.value as Collection<*> val paramNames = values.indices.joinToString(", ") { idx -> "${p}_$idx" } "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${comp.op.sql} ($paramNames)" } Op.IN_ARRAY -> { val p = name.derive(it.parameterName) - val (table, values) = comp.value as Pair> + @Suppress("UNCHECKED_CAST") + val tableAndValues = comp.value as? Pair> + ?: throw IllegalArgumentException("InArray field invalid") + val (table, values) = tableAndValues val paramNames = values.indices.joinToString(", ") { idx -> "${p}_$idx" } "EXISTS (SELECT 1 FROM json_each($table.data, '$.${it.name}') WHERE value IN ($paramNames)" } @@ -54,7 +57,8 @@ object Query { * @return A `WHERE` clause fragment identifying a document by its ID */ fun whereById(docId: TKey): String = - whereByFields(FieldMatch.ANY, listOf(Field.equal(Configuration.idField, docId).withParameterName(":id"))) + whereByFields(FieldMatch.ANY, + listOf(Field.equal(BaseConfig.idField, docId).withParameterName(":id"))) /** * Create an `UPDATE` statement to patch documents diff --git a/src/sqlite/src/test/kotlin/QueryTest.kt b/src/sqlite/src/test/kotlin/QueryTest.kt new file mode 100644 index 0000000..627a6f7 --- /dev/null +++ b/src/sqlite/src/test/kotlin/QueryTest.kt @@ -0,0 +1,99 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import kotlin.test.Test +import kotlin.test.assertEquals + +class QueryTest { + + @Test + @DisplayName("whereByFields generates for a single field with logical operator") + fun whereByFieldSingleLogical() { + assertEquals("data->>'theField' > :test", + Query.whereByFields(FieldMatch.ANY, listOf(Field.greater("theField", 0).withParameterName(":test"))), + "WHERE clause not correct") + } + + @Test + @DisplayName("whereByFields generates for a single field with existence operator") + fun whereByFieldSingleExistence() { + assertEquals("data->>'thatField' IS NULL", + Query.whereByFields(FieldMatch.ANY, listOf(Field.notExists("thatField"))), "WHERE clause not correct") + } + + @Test + @DisplayName("whereByFields generates for a single field with between operator") + fun whereByFieldSingleBetween() { + assertEquals("data->>'aField' BETWEEN :rangemin AND :rangemax", + Query.whereByFields(FieldMatch.ALL, listOf(Field.between("aField", 50, 99).withParameterName(":range"))), + "WHERE clause not correct") + } + + @Test + @DisplayName("whereByFields generates for all multiple fields with logical operator") + fun whereByFieldAllMultipleLogical() { + assertEquals("data->>'theFirst' = :field0 AND data->>'numberTwo' = :field1", + Query.whereByFields(FieldMatch.ALL, listOf(Field.equal("theFirst", "1"), Field.equal("numberTwo", "2"))), + "WHERE clause not correct") + } + + @Test + @DisplayName("whereByFields generates for any multiple fields with existence operator") + fun whereByFieldAnyMultipleExistence() { + assertEquals("data->>'thatField' IS NULL OR data->>'thisField' >= :field0", + Query.whereByFields(FieldMatch.ANY, + listOf(Field.notExists("thatField"), Field.greaterOrEqual("thisField", 18))), + "WHERE clause not correct") + } + + @Test + @DisplayName("whereByFields generates for an In comparison") + fun whereByFieldIn() { + assertEquals("data->>'this' IN (:field0_0, :field0_1, :field0_2)", + Query.whereByFields(FieldMatch.ALL, listOf(Field.any("this", listOf("a", "b", "c")))), + "WHERE clause not correct") + } + + @Test + @DisplayName("whereByFields generates for an InArray comparison") + fun whereByFieldInArray() { + assertEquals("EXISTS (SELECT 1 FROM json_each(the_table.data, '$.this') WHERE value IN (:field0_0, :field0_1)", + Query.whereByFields(FieldMatch.ALL, listOf(Field.inArray("this", "the_table", listOf("a", "b")))), + "WHERE clause not correct") + } + + @Test + @DisplayName("whereById generates correctly") + fun whereById() { + assertEquals("data->>'id' = :id", Query.whereById("abc"), "WHERE clause not correct") + } + + @Test + @DisplayName("patch generates the correct query") + fun patch() { + assertEquals("UPDATE my_table SET data = json_patch(data, json(:data))", Query.patch("my_table"), + "Query not correct") + } + + @Test + @DisplayName("byId generates the correct query") + fun byId() { + assertEquals("test WHERE data->>'id' = :id", Query.byId("test", "14"), "By-ID query not correct") + } + + @Test + @DisplayName("byFields generates the correct query") + fun byFields() { + assertEquals("unit WHERE data->>'That' > :field0", + Query.byFields("unit", FieldMatch.ANY, listOf(Field.greater("That", 14))), "By-fields query not correct") + } + + @Test + @DisplayName("Definition.ensureTable generates the correct query") + fun ensureTable() { + assertEquals("CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", Query.Definition.ensureTable("tbl"), + "CREATE TABLE statement not correct") + } +} -- 2.47.2 From 424915925246d04b9c8f047537cb2ac10079ddd2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Feb 2025 12:05:26 -0500 Subject: [PATCH 07/88] Fold work from SQLite into common - Checkpoint before common moved to top level --- .idea/compiler.xml | 4 +- .idea/misc.xml | 5 + src/common/src/main/kotlin/AutoId.kt | 12 +- src/common/src/main/kotlin/Comparison.kt | 18 +- src/common/src/main/kotlin/Configuration.kt | 36 ++-- src/common/src/main/kotlin/Dialect.kt | 19 ++- src/common/src/main/kotlin/Field.kt | 74 +++++++-- src/common/src/main/kotlin/Main.kt | 14 -- src/common/src/main/kotlin/Parameters.kt | 13 ++ src/common/src/main/kotlin/Query.kt | 175 +++++++++++++++++++- src/common/src/main/kotlin/Results.kt | 4 +- src/common/src/test/kotlin/QueryTest.kt | 22 ++- src/sqlite/pom.xml | 99 ----------- src/sqlite/src/main/kotlin/Configuration.kt | 26 --- src/sqlite/src/main/kotlin/Query.kt | 106 ------------ src/sqlite/src/test/kotlin/QueryTest.kt | 99 ----------- 16 files changed, 332 insertions(+), 394 deletions(-) delete mode 100644 src/common/src/main/kotlin/Main.kt delete mode 100644 src/sqlite/pom.xml delete mode 100644 src/sqlite/src/main/kotlin/Configuration.kt delete mode 100644 src/sqlite/src/main/kotlin/Query.kt delete mode 100644 src/sqlite/src/test/kotlin/QueryTest.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 0cf8482..ca3012f 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,8 +7,10 @@ - + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 161ad45..ba84d0a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,6 +8,11 @@ + diff --git a/src/common/src/main/kotlin/AutoId.kt b/src/common/src/main/kotlin/AutoId.kt index f152a8d..e77d78f 100644 --- a/src/common/src/main/kotlin/AutoId.kt +++ b/src/common/src/main/kotlin/AutoId.kt @@ -28,13 +28,15 @@ enum class AutoId { /** * Generate a string of random hex characters * - * @param length The length of the string + * @param length The length of the string (optional; defaults to configured length) * @return A string of random hex characters of the requested length */ - fun generateRandomString(length: Int): String = - kotlin.random.Random.nextBytes((length + 2) / 2) - .joinToString("") { String.format("%02x", it) } - .substring(0, length) + 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 diff --git a/src/common/src/main/kotlin/Comparison.kt b/src/common/src/main/kotlin/Comparison.kt index d6f7c24..01710b5 100644 --- a/src/common/src/main/kotlin/Comparison.kt +++ b/src/common/src/main/kotlin/Comparison.kt @@ -6,4 +6,20 @@ package solutions.bitbadger.documents.common * @property op The operation for the field comparison * @property value The value against which the comparison will be made */ -class Comparison(val op: Op, val value: T) +class Comparison(val op: Op, val value: T) { + + /** Is the value for this comparison a numeric value? */ + val isNumeric: Boolean + get() = + if (op == Op.IN || op == Op.BETWEEN) { + val values = value as? Collection<*> + if (values.isNullOrEmpty()) { + false + } else { + val first = values.elementAt(0) + first is Byte || first is Short || first is Int || first is Long + } + } else { + value is Byte || value is Short || value is Int || value is Long + } +} diff --git a/src/common/src/main/kotlin/Configuration.kt b/src/common/src/main/kotlin/Configuration.kt index 34c5e93..d71d43a 100644 --- a/src/common/src/main/kotlin/Configuration.kt +++ b/src/common/src/main/kotlin/Configuration.kt @@ -26,8 +26,15 @@ object Configuration { /** The length of automatic random hex character string */ var idStringLength = 16 + /** The derived dialect value from the connection string */ + private var dialectValue: Dialect? = null + /** The connection string for the JDBC connection */ var connectionString: String? = null + set(value) { + field = value + dialectValue = if (value.isNullOrBlank()) null else Dialect.deriveFromConnectionString(value) + } /** * Retrieve a new connection to the configured database @@ -42,23 +49,14 @@ object Configuration { return DriverManager.getConnection(connectionString) } - private var dialectValue: Dialect? = null - - /** The dialect in use */ - val dialect: Dialect - get() { - if (dialectValue == null) { - if (connectionString == null) { - throw IllegalArgumentException("Please provide a connection string before attempting data access") - } - val it = connectionString!! - dialectValue = when { - it.contains("sqlite") -> Dialect.SQLITE - it.contains("postgresql") -> Dialect.POSTGRESQL - else -> throw IllegalArgumentException("Cannot determine dialect from [$it]") - } - } - - return dialectValue!! - } + /** + * 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 + */ + fun dialect(process: String? = null): Dialect = + dialectValue ?: throw DocumentException( + "Database mode not set" + if (process == null) "" else "; cannot $process") } diff --git a/src/common/src/main/kotlin/Dialect.kt b/src/common/src/main/kotlin/Dialect.kt index bdf686c..9d4535f 100644 --- a/src/common/src/main/kotlin/Dialect.kt +++ b/src/common/src/main/kotlin/Dialect.kt @@ -7,5 +7,22 @@ enum class Dialect { /** PostgreSQL */ POSTGRESQL, /** SQLite */ - 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 + */ + 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/common/src/main/kotlin/Field.kt b/src/common/src/main/kotlin/Field.kt index 214b2e3..6f45ee3 100644 --- a/src/common/src/main/kotlin/Field.kt +++ b/src/common/src/main/kotlin/Field.kt @@ -8,7 +8,7 @@ package solutions.bitbadger.documents.common * @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( +class Field private constructor( val name: String, val comparison: Comparison, val parameterName: String? = null, @@ -42,7 +42,53 @@ class Field( 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 + */ + 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" + } + } + companion object { + /** * Create a field equality comparison * @@ -50,8 +96,8 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun equal(name: String, value: T): Field = - Field(name, Comparison(Op.EQUAL, value)) + fun equal(name: String, value: T) = + Field(name, Comparison(Op.EQUAL, value)) /** * Create a field greater-than comparison @@ -60,7 +106,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun greater(name: String, value: T): Field = + fun greater(name: String, value: T) = Field(name, Comparison(Op.GREATER, value)) /** @@ -70,7 +116,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun greaterOrEqual(name: String, value: T): Field = + fun greaterOrEqual(name: String, value: T) = Field(name, Comparison(Op.GREATER_OR_EQUAL, value)) /** @@ -80,7 +126,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun less(name: String, value: T): Field = + fun less(name: String, value: T) = Field(name, Comparison(Op.LESS, value)) /** @@ -90,7 +136,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun lessOrEqual(name: String, value: T): Field = + fun lessOrEqual(name: String, value: T) = Field(name, Comparison(Op.LESS_OR_EQUAL, value)) /** @@ -100,7 +146,7 @@ class Field( * @param value The value for the comparison * @return A `Field` with the given comparison */ - fun notEqual(name: String, value: T): Field = + fun notEqual(name: String, value: T) = Field(name, Comparison(Op.NOT_EQUAL, value)) /** @@ -111,7 +157,7 @@ class Field( * @param maxValue The upper value for the comparison * @return A `Field` with the given comparison */ - fun between(name: String, minValue: T, maxValue: T): Field> = + fun between(name: String, minValue: T, maxValue: T) = Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue))) /** @@ -121,7 +167,7 @@ class Field( * @param values The values for the comparison * @return A `Field` with the given comparison */ - fun any(name: String, values: List): Field> = + fun any(name: String, values: List) = Field(name, Comparison(Op.IN, values)) /** @@ -132,16 +178,16 @@ class Field( * @param values The values for the comparison * @return A `Field` with the given comparison */ - fun inArray(name: String, tableName: String, values: List): Field>> = + fun inArray(name: String, tableName: String, values: List) = Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values))) - fun exists(name: String): Field = + fun exists(name: String) = Field(name, Comparison(Op.EXISTS, "")) - fun notExists(name: String): Field = + fun notExists(name: String) = Field(name, Comparison(Op.NOT_EXISTS, "")) - fun named(name: String): Field = + fun named(name: String) = Field(name, Comparison(Op.EQUAL, "")) fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String { diff --git a/src/common/src/main/kotlin/Main.kt b/src/common/src/main/kotlin/Main.kt deleted file mode 100644 index 7265465..0000000 --- a/src/common/src/main/kotlin/Main.kt +++ /dev/null @@ -1,14 +0,0 @@ -//TIP To Run code, press or -// click the icon in the gutter. -fun main() { - val name = "Kotlin" - //TIP Press with your caret at the highlighted text - // to see how IntelliJ IDEA suggests fixing it. - println("Hello, " + name + "!") - - for (i in 1..5) { - //TIP Press to start debugging your code. We have set one breakpoint - // for you, but you can always add more by pressing . - println("i = $i") - } -} \ No newline at end of file diff --git a/src/common/src/main/kotlin/Parameters.kt b/src/common/src/main/kotlin/Parameters.kt index 4675abc..f43a711 100644 --- a/src/common/src/main/kotlin/Parameters.kt +++ b/src/common/src/main/kotlin/Parameters.kt @@ -12,6 +12,19 @@ import java.sql.Types */ 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> { + val name = ParameterName() + return fields.map { + if (it.name.isBlank()) it.withParameterName(name.derive(null)) else it + } + } + /** * Replace the parameter names in the query with question marks * diff --git a/src/common/src/main/kotlin/Query.kt b/src/common/src/main/kotlin/Query.kt index 4a0dbad..b5485fe 100644 --- a/src/common/src/main/kotlin/Query.kt +++ b/src/common/src/main/kotlin/Query.kt @@ -9,9 +9,84 @@ object Query { * @param where The `WHERE` clause for the statement * @return The two parts of the query combined with `WHERE` */ - fun statementWhere(statement: String, where: String): String = + fun statementWhere(statement: String, where: String) = "$statement WHERE $where" + /** + * 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 + */ + 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) + */ + fun byId(parameterName: String = ":id", docId: TKey? = null) = + byFields(listOf(Field.equal(Configuration.idField, docId ?: "").withParameterName(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 + */ + 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 + */ + 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") + } + } + + /** + * 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 + */ + 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 howMatched Whether to match any or all of the field conditions + * @param fields The field conditions to be matched + * @return A query addressing documents by field matching conditions + */ + fun byFields(statement: String, howMatched: FieldMatch, fields: Collection>) = + Query.statementWhere(statement, Where.byFields(fields, howMatched)) + + /** + * Functions to create queries to define tables and indexes + */ object Definition { /** @@ -24,6 +99,18 @@ object Query { fun ensureTableFor(tableName: String, dataType: String): 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) + * @return A query to create a document table + */ + fun ensureTable(tableName: String) = + when (Configuration.dialect("create table creation query")) { + Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB") + Dialect.SQLITE -> ensureTableFor(tableName, "TEXT") + } + /** * Split a schema and table name * @@ -71,8 +158,27 @@ object Query { * @param tableName The table into which to insert (may include schema) * @return A query to insert a document */ - fun insert(tableName: String): String = - "INSERT INTO $tableName VALUES (:data)" + 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") @@ -81,7 +187,8 @@ object Query { * @return A query to save a document */ fun save(tableName: String): String = - "${insert(tableName)} ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data" + insert(tableName, AutoId.DISABLED) + + " ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data" /** * Query to count documents in a table (this query has no `WHERE` clause) @@ -120,6 +227,66 @@ object Query { fun update(tableName: String): String = "UPDATE $tableName SET data = :data" + /** + * Functions to create queries to patch (partially update) JSON documents + */ + object Patch { + + /** + * Create an `UPDATE` statement to patch documents + * + * @param tableName The table to be updated + * @param where The `WHERE` clause for the query + * @return A query to patch documents + */ + private fun patch(tableName: String, where: String): String { + val setValue = when (Configuration.dialect("create patch query")) { + Dialect.POSTGRESQL -> "data || :data" + Dialect.SQLITE -> "json_patch(data, json(:data))" + } + return statementWhere("UPDATE $tableName SET data = $setValue", where) + } + + /** + * 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 + */ + fun byId(tableName: String, docId: TKey? = null) = + patch(tableName, Where.byId(docId = 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 + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + patch(tableName, Where.byFields(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 + */ + fun byContains(tableName: String) = + 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 + */ + fun byJsonPath(tableName: String) = + patch(tableName, Where.jsonPathMatches()) + } + /** * Query to delete documents from a table (this query has no `WHERE` clause) * diff --git a/src/common/src/main/kotlin/Results.kt b/src/common/src/main/kotlin/Results.kt index 777a228..2f67413 100644 --- a/src/common/src/main/kotlin/Results.kt +++ b/src/common/src/main/kotlin/Results.kt @@ -56,7 +56,7 @@ object Results { * @return The count from the row */ fun toCount(rs: ResultSet): Long = - when (Configuration.dialect) { + when (Configuration.dialect()) { Dialect.POSTGRESQL -> rs.getInt("it").toLong() Dialect.SQLITE -> rs.getLong("it") } @@ -68,7 +68,7 @@ object Results { * @return The true/false value from the row */ fun toExists(rs: ResultSet): Boolean = - when (Configuration.dialect) { + when (Configuration.dialect()) { Dialect.POSTGRESQL -> rs.getBoolean("it") Dialect.SQLITE -> toCount(rs) > 0L } diff --git a/src/common/src/test/kotlin/QueryTest.kt b/src/common/src/test/kotlin/QueryTest.kt index 565243d..73248a2 100644 --- a/src/common/src/test/kotlin/QueryTest.kt +++ b/src/common/src/test/kotlin/QueryTest.kt @@ -67,14 +67,30 @@ class QueryTest { @Test @DisplayName("insert generates correctly") fun insert() { - assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl), "INSERT statement not constructed correctly") + try { + Configuration.connectionString = "postgresql" + assertEquals( + "INSERT INTO $tbl VALUES (:data)", + Query.insert(tbl), + "INSERT statement not constructed correctly" + ) + } finally { + Configuration.connectionString = null + } } @Test @DisplayName("save generates correctly") fun save() { - assertEquals("INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", - Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly") + try { + Configuration.connectionString = "postgresql" + assertEquals( + "INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", + Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly" + ) + } finally { + Configuration.connectionString = null + } } @Test diff --git a/src/sqlite/pom.xml b/src/sqlite/pom.xml deleted file mode 100644 index 4b69624..0000000 --- a/src/sqlite/pom.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - 4.0.0 - - solutions.bitbadger.documents - sqlite - 4.0-ALPHA - - - UTF-8 - official - 1.8 - - - - - mavenCentral - https://repo1.maven.org/maven2/ - - - - - src/main/kotlin - src/test/kotlin - - - org.jetbrains.kotlin - kotlin-maven-plugin - 2.1.10 - - - compile - compile - - compile - - - - test-compile - test-compile - - test-compile - - - - - - maven-surefire-plugin - 2.22.2 - - - maven-failsafe-plugin - 2.22.2 - - - org.codehaus.mojo - exec-maven-plugin - 1.6.0 - - MainKt - - - - - - - - org.jetbrains.kotlin - kotlin-test-junit5 - 2.1.10 - test - - - org.junit.jupiter - junit-jupiter - 5.10.0 - test - - - org.jetbrains.kotlin - kotlin-stdlib - 2.1.10 - - - org.xerial - sqlite-jdbc - 3.46.1.2 - - - solutions.bitbadger.documents - common - 4.0-ALPHA - compile - - - - \ No newline at end of file diff --git a/src/sqlite/src/main/kotlin/Configuration.kt b/src/sqlite/src/main/kotlin/Configuration.kt deleted file mode 100644 index b944d0d..0000000 --- a/src/sqlite/src/main/kotlin/Configuration.kt +++ /dev/null @@ -1,26 +0,0 @@ -package solutions.bitbadger.documents.sqlite - -import java.sql.Connection -import java.sql.DriverManager - -/** - * Configuration for SQLite - */ -object Configuration { - - /** The connection string for the SQLite database */ - var connectionString: String? = null - - /** - * Retrieve a new connection to the SQLite database - * - * @return A new connection to the SQLite database - * @throws IllegalArgumentException If the connection string is not set before calling this - */ - fun dbConn(): Connection { - if (connectionString == null) { - throw IllegalArgumentException("Please provide a connection string before attempting data access") - } - return DriverManager.getConnection(connectionString) - } -} diff --git a/src/sqlite/src/main/kotlin/Query.kt b/src/sqlite/src/main/kotlin/Query.kt deleted file mode 100644 index df75709..0000000 --- a/src/sqlite/src/main/kotlin/Query.kt +++ /dev/null @@ -1,106 +0,0 @@ -package solutions.bitbadger.documents.sqlite - -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.Configuration as BaseConfig; -import solutions.bitbadger.documents.common.Query - -/** - * Queries with specific syntax in SQLite - */ -object Query { - - /** - * Create a `WHERE` clause fragment to implement a comparison on fields in a JSON document - * - * @param howMatched How the fields should be matched - * @param fields The fields for the comparisons - * @return A `WHERE` clause implementing the comparisons for the given fields - */ - fun whereByFields(howMatched: FieldMatch, fields: Collection>): String { - val name = ParameterName() - return fields.joinToString(" ${howMatched.sql} ") { - val comp = it.comparison - when (comp.op) { - Op.EXISTS, Op.NOT_EXISTS -> { - "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${it.comparison.op.sql}" - } - Op.BETWEEN -> { - val p = name.derive(it.parameterName) - "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${comp.op.sql} ${p}min AND ${p}max" - } - Op.IN -> { - val p = name.derive(it.parameterName) - val values = comp.value as Collection<*> - val paramNames = values.indices.joinToString(", ") { idx -> "${p}_$idx" } - "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${comp.op.sql} ($paramNames)" - } - Op.IN_ARRAY -> { - val p = name.derive(it.parameterName) - @Suppress("UNCHECKED_CAST") - val tableAndValues = comp.value as? Pair> - ?: throw IllegalArgumentException("InArray field invalid") - val (table, values) = tableAndValues - val paramNames = values.indices.joinToString(", ") { idx -> "${p}_$idx" } - "EXISTS (SELECT 1 FROM json_each($table.data, '$.${it.name}') WHERE value IN ($paramNames)" - } - else -> { - "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${comp.op.sql} ${name.derive(it.parameterName)}" - } - } - } - } - - /** - * Create a `WHERE` clause fragment to implement an ID-based query - * - * @param docId The ID of the document - * @return A `WHERE` clause fragment identifying a document by its ID - */ - fun whereById(docId: TKey): String = - whereByFields(FieldMatch.ANY, - listOf(Field.equal(BaseConfig.idField, docId).withParameterName(":id"))) - - /** - * Create an `UPDATE` statement to patch documents - * - * @param tableName The table to be updated - * @return A query to patch documents - */ - fun patch(tableName: String): String = - "UPDATE $tableName SET data = json_patch(data, json(:data))" - - // TODO: fun removeFields(tableName: String, fields: Collection): String - - /** - * 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 - */ - fun byId(statement: String, docId: TKey): String = - Query.statementWhere(statement, whereById(docId)) - - /** - * Create a query on JSON fields - * - * @param statement The SQL statement to be run against matching fields - * @param howMatched Whether to match any or all of the field conditions - * @param fields The field conditions to be matched - * @return A query addressing documents by field matching conditions - */ - fun byFields(statement: String, howMatched: FieldMatch, fields: Collection>): String = - Query.statementWhere(statement, whereByFields(howMatched, fields)) - - object Definition { - - /** - * SQL statement to create a document table - * - * @param tableName The name of the table (may include schema) - * @return A query to create the table if it does not exist - */ - fun ensureTable(tableName: String): String = - Query.Definition.ensureTableFor(tableName, "TEXT") - } -} diff --git a/src/sqlite/src/test/kotlin/QueryTest.kt b/src/sqlite/src/test/kotlin/QueryTest.kt deleted file mode 100644 index 627a6f7..0000000 --- a/src/sqlite/src/test/kotlin/QueryTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -package solutions.bitbadger.documents.sqlite - -import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import kotlin.test.Test -import kotlin.test.assertEquals - -class QueryTest { - - @Test - @DisplayName("whereByFields generates for a single field with logical operator") - fun whereByFieldSingleLogical() { - assertEquals("data->>'theField' > :test", - Query.whereByFields(FieldMatch.ANY, listOf(Field.greater("theField", 0).withParameterName(":test"))), - "WHERE clause not correct") - } - - @Test - @DisplayName("whereByFields generates for a single field with existence operator") - fun whereByFieldSingleExistence() { - assertEquals("data->>'thatField' IS NULL", - Query.whereByFields(FieldMatch.ANY, listOf(Field.notExists("thatField"))), "WHERE clause not correct") - } - - @Test - @DisplayName("whereByFields generates for a single field with between operator") - fun whereByFieldSingleBetween() { - assertEquals("data->>'aField' BETWEEN :rangemin AND :rangemax", - Query.whereByFields(FieldMatch.ALL, listOf(Field.between("aField", 50, 99).withParameterName(":range"))), - "WHERE clause not correct") - } - - @Test - @DisplayName("whereByFields generates for all multiple fields with logical operator") - fun whereByFieldAllMultipleLogical() { - assertEquals("data->>'theFirst' = :field0 AND data->>'numberTwo' = :field1", - Query.whereByFields(FieldMatch.ALL, listOf(Field.equal("theFirst", "1"), Field.equal("numberTwo", "2"))), - "WHERE clause not correct") - } - - @Test - @DisplayName("whereByFields generates for any multiple fields with existence operator") - fun whereByFieldAnyMultipleExistence() { - assertEquals("data->>'thatField' IS NULL OR data->>'thisField' >= :field0", - Query.whereByFields(FieldMatch.ANY, - listOf(Field.notExists("thatField"), Field.greaterOrEqual("thisField", 18))), - "WHERE clause not correct") - } - - @Test - @DisplayName("whereByFields generates for an In comparison") - fun whereByFieldIn() { - assertEquals("data->>'this' IN (:field0_0, :field0_1, :field0_2)", - Query.whereByFields(FieldMatch.ALL, listOf(Field.any("this", listOf("a", "b", "c")))), - "WHERE clause not correct") - } - - @Test - @DisplayName("whereByFields generates for an InArray comparison") - fun whereByFieldInArray() { - assertEquals("EXISTS (SELECT 1 FROM json_each(the_table.data, '$.this') WHERE value IN (:field0_0, :field0_1)", - Query.whereByFields(FieldMatch.ALL, listOf(Field.inArray("this", "the_table", listOf("a", "b")))), - "WHERE clause not correct") - } - - @Test - @DisplayName("whereById generates correctly") - fun whereById() { - assertEquals("data->>'id' = :id", Query.whereById("abc"), "WHERE clause not correct") - } - - @Test - @DisplayName("patch generates the correct query") - fun patch() { - assertEquals("UPDATE my_table SET data = json_patch(data, json(:data))", Query.patch("my_table"), - "Query not correct") - } - - @Test - @DisplayName("byId generates the correct query") - fun byId() { - assertEquals("test WHERE data->>'id' = :id", Query.byId("test", "14"), "By-ID query not correct") - } - - @Test - @DisplayName("byFields generates the correct query") - fun byFields() { - assertEquals("unit WHERE data->>'That' > :field0", - Query.byFields("unit", FieldMatch.ANY, listOf(Field.greater("That", 14))), "By-fields query not correct") - } - - @Test - @DisplayName("Definition.ensureTable generates the correct query") - fun ensureTable() { - assertEquals("CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", Query.Definition.ensureTable("tbl"), - "CREATE TABLE statement not correct") - } -} -- 2.47.2 From b1a9910d50718dcfe4a8805724f1d884b98ba0b8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Feb 2025 12:15:43 -0500 Subject: [PATCH 08/88] Collapse into top-level module --- .idea/compiler.xml | 3 ++- .idea/encodings.xml | 4 ++-- .idea/kotlinc.xml | 2 +- .idea/misc.xml | 1 + .idea/modules.xml | 8 -------- .idea/solutions.bitbadger.documents.iml | 5 ++++- src/common/pom.xml => pom.xml | 4 ++-- src/{common/src => }/main/kotlin/AutoId.kt | 2 +- src/{common/src => }/main/kotlin/Comparison.kt | 2 +- src/{common/src => }/main/kotlin/Configuration.kt | 2 +- src/{common/src => }/main/kotlin/ConnectionExtensions.kt | 2 +- src/{common/src => }/main/kotlin/Dialect.kt | 2 +- src/{common/src => }/main/kotlin/DocumentException.kt | 2 +- src/{common/src => }/main/kotlin/Field.kt | 2 +- src/{common/src => }/main/kotlin/FieldFormat.kt | 2 +- src/{common/src => }/main/kotlin/FieldMatch.kt | 2 +- src/{common/src => }/main/kotlin/Op.kt | 2 +- src/{common/src => }/main/kotlin/Parameter.kt | 2 +- src/{common/src => }/main/kotlin/ParameterName.kt | 2 +- src/{common/src => }/main/kotlin/ParameterType.kt | 2 +- src/{common/src => }/main/kotlin/Parameters.kt | 2 +- src/{common/src => }/main/kotlin/Query.kt | 2 +- src/{common/src => }/main/kotlin/Results.kt | 2 +- src/{common/src => }/test/kotlin/AutoIdTest.kt | 2 +- src/{common/src => }/test/kotlin/ConfigurationTest.kt | 2 +- src/{common/src => }/test/kotlin/FieldMatchTest.kt | 2 +- src/{common/src => }/test/kotlin/FieldTest.kt | 2 +- src/{common/src => }/test/kotlin/OpTest.kt | 2 +- src/{common/src => }/test/kotlin/ParameterNameTest.kt | 2 +- src/{common/src => }/test/kotlin/ParameterTest.kt | 2 +- src/{common/src => }/test/kotlin/ParametersTest.kt | 2 +- src/{common/src => }/test/kotlin/QueryTest.kt | 2 +- 32 files changed, 37 insertions(+), 40 deletions(-) delete mode 100644 .idea/modules.xml rename src/common/pom.xml => pom.xml (97%) rename src/{common/src => }/main/kotlin/AutoId.kt (98%) rename src/{common/src => }/main/kotlin/Comparison.kt (94%) rename src/{common/src => }/main/kotlin/Configuration.kt (97%) rename src/{common/src => }/main/kotlin/ConnectionExtensions.kt (97%) rename src/{common/src => }/main/kotlin/Dialect.kt (94%) rename src/{common/src => }/main/kotlin/DocumentException.kt (85%) rename src/{common/src => }/main/kotlin/Field.kt (99%) rename src/{common/src => }/main/kotlin/FieldFormat.kt (84%) rename src/{common/src => }/main/kotlin/FieldMatch.kt (83%) rename src/{common/src => }/main/kotlin/Op.kt (94%) rename src/{common/src => }/main/kotlin/Parameter.kt (90%) rename src/{common/src => }/main/kotlin/ParameterName.kt (91%) rename src/{common/src => }/main/kotlin/ParameterType.kt (87%) rename src/{common/src => }/main/kotlin/Parameters.kt (98%) rename src/{common/src => }/main/kotlin/Query.kt (99%) rename src/{common/src => }/main/kotlin/Results.kt (98%) rename src/{common/src => }/test/kotlin/AutoIdTest.kt (99%) rename src/{common/src => }/test/kotlin/ConfigurationTest.kt (94%) rename src/{common/src => }/test/kotlin/FieldMatchTest.kt (90%) rename src/{common/src => }/test/kotlin/FieldTest.kt (99%) rename src/{common/src => }/test/kotlin/OpTest.kt (97%) rename src/{common/src => }/test/kotlin/ParameterNameTest.kt (95%) rename src/{common/src => }/test/kotlin/ParameterTest.kt (96%) rename src/{common/src => }/test/kotlin/ParametersTest.kt (93%) rename src/{common/src => }/test/kotlin/QueryTest.kt (99%) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index ca3012f..1106a32 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -6,10 +6,11 @@ - + + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index e98b3a0..e48c513 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,8 +1,8 @@ - - + + diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index c22b6fa..bb44937 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index ba84d0a..9959fd1 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,6 +10,7 @@ diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index cf07591..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/solutions.bitbadger.documents.iml b/.idea/solutions.bitbadger.documents.iml index 6ec89fa..cd2501b 100644 --- a/.idea/solutions.bitbadger.documents.iml +++ b/.idea/solutions.bitbadger.documents.iml @@ -2,7 +2,10 @@ - + + + + diff --git a/src/common/pom.xml b/pom.xml similarity index 97% rename from src/common/pom.xml rename to pom.xml index 4b91d63..cab9d3e 100644 --- a/src/common/pom.xml +++ b/pom.xml @@ -4,8 +4,8 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - solutions.bitbadger.documents - common + solutions.bitbadger + documents 4.0-ALPHA diff --git a/src/common/src/main/kotlin/AutoId.kt b/src/main/kotlin/AutoId.kt similarity index 98% rename from src/common/src/main/kotlin/AutoId.kt rename to src/main/kotlin/AutoId.kt index e77d78f..1d8da22 100644 --- a/src/common/src/main/kotlin/AutoId.kt +++ b/src/main/kotlin/AutoId.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import kotlin.reflect.full.* diff --git a/src/common/src/main/kotlin/Comparison.kt b/src/main/kotlin/Comparison.kt similarity index 94% rename from src/common/src/main/kotlin/Comparison.kt rename to src/main/kotlin/Comparison.kt index 01710b5..bcb4226 100644 --- a/src/common/src/main/kotlin/Comparison.kt +++ b/src/main/kotlin/Comparison.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * A comparison against a field in a JSON document diff --git a/src/common/src/main/kotlin/Configuration.kt b/src/main/kotlin/Configuration.kt similarity index 97% rename from src/common/src/main/kotlin/Configuration.kt rename to src/main/kotlin/Configuration.kt index d71d43a..069f9a4 100644 --- a/src/common/src/main/kotlin/Configuration.kt +++ b/src/main/kotlin/Configuration.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import kotlinx.serialization.json.Json import java.sql.Connection diff --git a/src/common/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt similarity index 97% rename from src/common/src/main/kotlin/ConnectionExtensions.kt rename to src/main/kotlin/ConnectionExtensions.kt index e18a7f4..51f7f93 100644 --- a/src/common/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import java.sql.Connection import java.sql.ResultSet diff --git a/src/common/src/main/kotlin/Dialect.kt b/src/main/kotlin/Dialect.kt similarity index 94% rename from src/common/src/main/kotlin/Dialect.kt rename to src/main/kotlin/Dialect.kt index 9d4535f..e19807d 100644 --- a/src/common/src/main/kotlin/Dialect.kt +++ b/src/main/kotlin/Dialect.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * The SQL dialect to use when building queries diff --git a/src/common/src/main/kotlin/DocumentException.kt b/src/main/kotlin/DocumentException.kt similarity index 85% rename from src/common/src/main/kotlin/DocumentException.kt rename to src/main/kotlin/DocumentException.kt index 6901d2b..bb0e0ff 100644 --- a/src/common/src/main/kotlin/DocumentException.kt +++ b/src/main/kotlin/DocumentException.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * An exception caused by invalid operations in the document library diff --git a/src/common/src/main/kotlin/Field.kt b/src/main/kotlin/Field.kt similarity index 99% rename from src/common/src/main/kotlin/Field.kt rename to src/main/kotlin/Field.kt index 6f45ee3..1c8102d 100644 --- a/src/common/src/main/kotlin/Field.kt +++ b/src/main/kotlin/Field.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * A field and its comparison diff --git a/src/common/src/main/kotlin/FieldFormat.kt b/src/main/kotlin/FieldFormat.kt similarity index 84% rename from src/common/src/main/kotlin/FieldFormat.kt rename to src/main/kotlin/FieldFormat.kt index 02d4c20..c804a87 100644 --- a/src/common/src/main/kotlin/FieldFormat.kt +++ b/src/main/kotlin/FieldFormat.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * The data format for a document field retrieval diff --git a/src/common/src/main/kotlin/FieldMatch.kt b/src/main/kotlin/FieldMatch.kt similarity index 83% rename from src/common/src/main/kotlin/FieldMatch.kt rename to src/main/kotlin/FieldMatch.kt index e621dba..3b1d3ff 100644 --- a/src/common/src/main/kotlin/FieldMatch.kt +++ b/src/main/kotlin/FieldMatch.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * How fields should be matched in by-field queries diff --git a/src/common/src/main/kotlin/Op.kt b/src/main/kotlin/Op.kt similarity index 94% rename from src/common/src/main/kotlin/Op.kt rename to src/main/kotlin/Op.kt index 89f6139..0566f06 100644 --- a/src/common/src/main/kotlin/Op.kt +++ b/src/main/kotlin/Op.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * A comparison operator used for fields diff --git a/src/common/src/main/kotlin/Parameter.kt b/src/main/kotlin/Parameter.kt similarity index 90% rename from src/common/src/main/kotlin/Parameter.kt rename to src/main/kotlin/Parameter.kt index eca25b7..97fe766 100644 --- a/src/common/src/main/kotlin/Parameter.kt +++ b/src/main/kotlin/Parameter.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * A parameter to use for a query diff --git a/src/common/src/main/kotlin/ParameterName.kt b/src/main/kotlin/ParameterName.kt similarity index 91% rename from src/common/src/main/kotlin/ParameterName.kt rename to src/main/kotlin/ParameterName.kt index 566dca0..a090db0 100644 --- a/src/common/src/main/kotlin/ParameterName.kt +++ b/src/main/kotlin/ParameterName.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * Derive parameter names; each instance wraps a counter to provide names for anonymous fields diff --git a/src/common/src/main/kotlin/ParameterType.kt b/src/main/kotlin/ParameterType.kt similarity index 87% rename from src/common/src/main/kotlin/ParameterType.kt rename to src/main/kotlin/ParameterType.kt index 53d6e1b..77a88da 100644 --- a/src/common/src/main/kotlin/ParameterType.kt +++ b/src/main/kotlin/ParameterType.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * The types of parameters supported by the document library diff --git a/src/common/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt similarity index 98% rename from src/common/src/main/kotlin/Parameters.kt rename to src/main/kotlin/Parameters.kt index f43a711..550ecf0 100644 --- a/src/common/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import java.sql.Connection import java.sql.PreparedStatement diff --git a/src/common/src/main/kotlin/Query.kt b/src/main/kotlin/Query.kt similarity index 99% rename from src/common/src/main/kotlin/Query.kt rename to src/main/kotlin/Query.kt index b5485fe..8348c35 100644 --- a/src/common/src/main/kotlin/Query.kt +++ b/src/main/kotlin/Query.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents object Query { diff --git a/src/common/src/main/kotlin/Results.kt b/src/main/kotlin/Results.kt similarity index 98% rename from src/common/src/main/kotlin/Results.kt rename to src/main/kotlin/Results.kt index 2f67413..f35b483 100644 --- a/src/common/src/main/kotlin/Results.kt +++ b/src/main/kotlin/Results.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import java.sql.PreparedStatement import java.sql.ResultSet diff --git a/src/common/src/test/kotlin/AutoIdTest.kt b/src/test/kotlin/AutoIdTest.kt similarity index 99% rename from src/common/src/test/kotlin/AutoIdTest.kt rename to src/test/kotlin/AutoIdTest.kt index 038d322..1098e1d 100644 --- a/src/common/src/test/kotlin/AutoIdTest.kt +++ b/src/test/kotlin/AutoIdTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/src/common/src/test/kotlin/ConfigurationTest.kt b/src/test/kotlin/ConfigurationTest.kt similarity index 94% rename from src/common/src/test/kotlin/ConfigurationTest.kt rename to src/test/kotlin/ConfigurationTest.kt index 29a2a61..8dc67d1 100644 --- a/src/common/src/test/kotlin/ConfigurationTest.kt +++ b/src/test/kotlin/ConfigurationTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/src/common/src/test/kotlin/FieldMatchTest.kt b/src/test/kotlin/FieldMatchTest.kt similarity index 90% rename from src/common/src/test/kotlin/FieldMatchTest.kt rename to src/test/kotlin/FieldMatchTest.kt index f73e99f..cbb01bb 100644 --- a/src/common/src/test/kotlin/FieldMatchTest.kt +++ b/src/test/kotlin/FieldMatchTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/src/common/src/test/kotlin/FieldTest.kt b/src/test/kotlin/FieldTest.kt similarity index 99% rename from src/common/src/test/kotlin/FieldTest.kt rename to src/test/kotlin/FieldTest.kt index 7c32a0d..1114e0b 100644 --- a/src/common/src/test/kotlin/FieldTest.kt +++ b/src/test/kotlin/FieldTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/src/common/src/test/kotlin/OpTest.kt b/src/test/kotlin/OpTest.kt similarity index 97% rename from src/common/src/test/kotlin/OpTest.kt rename to src/test/kotlin/OpTest.kt index 6656928..b69b386 100644 --- a/src/common/src/test/kotlin/OpTest.kt +++ b/src/test/kotlin/OpTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/src/common/src/test/kotlin/ParameterNameTest.kt b/src/test/kotlin/ParameterNameTest.kt similarity index 95% rename from src/common/src/test/kotlin/ParameterNameTest.kt rename to src/test/kotlin/ParameterNameTest.kt index 4c435aa..e63162e 100644 --- a/src/common/src/test/kotlin/ParameterNameTest.kt +++ b/src/test/kotlin/ParameterNameTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/src/common/src/test/kotlin/ParameterTest.kt b/src/test/kotlin/ParameterTest.kt similarity index 96% rename from src/common/src/test/kotlin/ParameterTest.kt rename to src/test/kotlin/ParameterTest.kt index 7cc17e3..d9d0b2f 100644 --- a/src/common/src/test/kotlin/ParameterTest.kt +++ b/src/test/kotlin/ParameterTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows diff --git a/src/common/src/test/kotlin/ParametersTest.kt b/src/test/kotlin/ParametersTest.kt similarity index 93% rename from src/common/src/test/kotlin/ParametersTest.kt rename to src/test/kotlin/ParametersTest.kt index 0394a12..d5a6592 100644 --- a/src/common/src/test/kotlin/ParametersTest.kt +++ b/src/test/kotlin/ParametersTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import kotlin.test.Test diff --git a/src/common/src/test/kotlin/QueryTest.kt b/src/test/kotlin/QueryTest.kt similarity index 99% rename from src/common/src/test/kotlin/QueryTest.kt rename to src/test/kotlin/QueryTest.kt index 73248a2..3799154 100644 --- a/src/common/src/test/kotlin/QueryTest.kt +++ b/src/test/kotlin/QueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -- 2.47.2 From 623e49243d893339fa10e7d05d7016915be0576b Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Feb 2025 18:46:50 -0500 Subject: [PATCH 09/88] WIP on tests --- src/main/kotlin/AutoId.kt | 10 +- src/main/kotlin/Comparison.kt | 19 +- src/main/kotlin/Dialect.kt | 4 +- src/main/kotlin/Field.kt | 60 ++-- src/main/kotlin/Query.kt | 2 +- src/test/kotlin/AutoIdTest.kt | 62 ++-- src/test/kotlin/ComparisonTest.kt | 91 +++++ src/test/kotlin/ConfigurationTest.kt | 22 ++ src/test/kotlin/DialectTest.kt | 34 ++ src/test/kotlin/FieldTest.kt | 507 +++++++++++++++++++++------ src/test/kotlin/QueryTest.kt | 34 +- 11 files changed, 646 insertions(+), 199 deletions(-) create mode 100644 src/test/kotlin/ComparisonTest.kt create mode 100644 src/test/kotlin/DialectTest.kt diff --git a/src/main/kotlin/AutoId.kt b/src/main/kotlin/AutoId.kt index 1d8da22..a5bfa54 100644 --- a/src/main/kotlin/AutoId.kt +++ b/src/main/kotlin/AutoId.kt @@ -45,15 +45,15 @@ enum class AutoId { * @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 IllegalArgumentException If bad input prevents the determination + * @throws DocumentException If bad input prevents the determination */ fun needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean { - if (document == null) throw IllegalArgumentException("document cannot be null") + if (document == null) throw DocumentException("document cannot be null") if (strategy == DISABLED) return false; val id = document!!::class.memberProperties.find { it.name == idProp } - if (id == null) throw IllegalArgumentException("$idProp not found in document") + if (id == null) throw DocumentException("$idProp not found in document") if (strategy == NUMBER) { return when (id.returnType) { @@ -61,7 +61,7 @@ enum class AutoId { 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 IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID") + else -> throw DocumentException("$idProp was not a number; cannot auto-generate number ID") } } @@ -69,7 +69,7 @@ enum class AutoId { return id.call(document) == "" } - throw IllegalArgumentException("$idProp was not a string; cannot auto-generate UUID or random string") + throw DocumentException("$idProp was not a string; cannot auto-generate UUID or random string") } } } diff --git a/src/main/kotlin/Comparison.kt b/src/main/kotlin/Comparison.kt index bcb4226..987f946 100644 --- a/src/main/kotlin/Comparison.kt +++ b/src/main/kotlin/Comparison.kt @@ -10,16 +10,15 @@ class Comparison(val op: Op, val value: T) { /** Is the value for this comparison a numeric value? */ val isNumeric: Boolean - get() = - if (op == Op.IN || op == Op.BETWEEN) { - val values = value as? Collection<*> - if (values.isNullOrEmpty()) { - false - } else { - val first = values.elementAt(0) - first is Byte || first is Short || first is Int || first is Long + get() { + val toCheck = when (op) { + Op.IN -> { + val values = value as? Collection<*> + if (values.isNullOrEmpty()) "" else values.elementAt(0) } - } else { - value is Byte || value is Short || value is Int || value is Long + Op.BETWEEN -> (value as Pair<*, *>).first + else -> value } + return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long + } } diff --git a/src/main/kotlin/Dialect.kt b/src/main/kotlin/Dialect.kt index e19807d..986dddb 100644 --- a/src/main/kotlin/Dialect.kt +++ b/src/main/kotlin/Dialect.kt @@ -20,8 +20,8 @@ enum class Dialect { */ fun deriveFromConnectionString(connectionString: String): Dialect = when { - connectionString.contains("sqlite") -> SQLITE - connectionString.contains("postgresql") -> POSTGRESQL + connectionString.contains(":sqlite:") -> SQLITE + connectionString.contains(":postgresql:") -> POSTGRESQL else -> throw DocumentException("Cannot determine dialect from [$connectionString]") } } diff --git a/src/main/kotlin/Field.kt b/src/main/kotlin/Field.kt index 1c8102d..11fb00d 100644 --- a/src/main/kotlin/Field.kt +++ b/src/main/kotlin/Field.kt @@ -20,8 +20,11 @@ class Field private constructor( * @param paramName The parameter name to use for this field * @return A new `Field` with the parameter name specified */ - fun withParameterName(paramName: String): Field = - Field(name, comparison, paramName, qualifier) + fun withParameterName(paramName: String): Field { + if (!paramName.startsWith(':') && !paramName.startsWith('@')) + throw DocumentException("Parameter must start with : or @ ($paramName)") + return Field(name, comparison, paramName, qualifier) + } /** * Specify a qualifier (alias) for the document table @@ -29,7 +32,7 @@ class Field private constructor( * @param alias The table alias for this field * @return A new `Field` with the table qualifier specified */ - fun withQualifier(alias: String): Field = + fun withQualifier(alias: String) = Field(name, comparison, parameterName, alias) /** @@ -74,16 +77,16 @@ class Field private constructor( 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 "" + 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)" + "EXISTS (SELECT 1 FROM json_each($table.data, '$.$name') WHERE value IN ($inParameterNames))" } else { - "$fieldPath ${comparison.op.sql} $criteria" + "$fieldPath ${comparison.op.sql}$criteria" } } @@ -94,60 +97,66 @@ class Field private constructor( * * @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 */ - fun equal(name: String, value: T) = - Field(name, Comparison(Op.EQUAL, value)) + fun equal(name: String, value: T, paramName: String? = null) = + Field(name, Comparison(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 */ - fun greater(name: String, value: T) = - Field(name, Comparison(Op.GREATER, value)) + fun greater(name: String, value: T, paramName: String? = null) = + Field(name, Comparison(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 */ - fun greaterOrEqual(name: String, value: T) = - Field(name, Comparison(Op.GREATER_OR_EQUAL, value)) + fun greaterOrEqual(name: String, value: T, paramName: String? = null) = + Field(name, Comparison(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 */ - fun less(name: String, value: T) = - Field(name, Comparison(Op.LESS, value)) + fun less(name: String, value: T, paramName: String? = null) = + Field(name, Comparison(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 */ - fun lessOrEqual(name: String, value: T) = - Field(name, Comparison(Op.LESS_OR_EQUAL, value)) + fun lessOrEqual(name: String, value: T, paramName: String? = null) = + Field(name, Comparison(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 */ - fun notEqual(name: String, value: T) = - Field(name, Comparison(Op.NOT_EQUAL, value)) + fun notEqual(name: String, value: T, paramName: String? = null) = + Field(name, Comparison(Op.NOT_EQUAL, value), paramName) /** * Create a field range comparison @@ -155,20 +164,22 @@ class Field private constructor( * @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 */ - fun between(name: String, minValue: T, maxValue: T) = - Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue))) + fun between(name: String, minValue: T, maxValue: T, paramName: String? = null) = + Field(name, Comparison(Op.BETWEEN, 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 */ - fun any(name: String, values: List) = - Field(name, Comparison(Op.IN, values)) + fun any(name: String, values: Collection, paramName: String? = null) = + Field(name, Comparison(Op.IN, values), paramName) /** * Create a field where values should exist in a document's array @@ -176,10 +187,11 @@ class Field private constructor( * @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 */ - fun inArray(name: String, tableName: String, values: List) = - Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values))) + fun inArray(name: String, tableName: String, values: Collection, paramName: String? = null) = + Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values)), paramName) fun exists(name: String) = Field(name, Comparison(Op.EXISTS, "")) diff --git a/src/main/kotlin/Query.kt b/src/main/kotlin/Query.kt index 8348c35..6852f91 100644 --- a/src/main/kotlin/Query.kt +++ b/src/main/kotlin/Query.kt @@ -34,7 +34,7 @@ object Query { * @param docId The ID value (optional; used for type determinations, string assumed if not provided) */ fun byId(parameterName: String = ":id", docId: TKey? = null) = - byFields(listOf(Field.equal(Configuration.idField, docId ?: "").withParameterName(parameterName))) + byFields(listOf(Field.equal(Configuration.idField, docId ?: "", parameterName))) /** * Create a `WHERE` clause fragment to implement a JSON containment query (PostgreSQL only) diff --git a/src/test/kotlin/AutoIdTest.kt b/src/test/kotlin/AutoIdTest.kt index 1098e1d..f99c0e0 100644 --- a/src/test/kotlin/AutoIdTest.kt +++ b/src/test/kotlin/AutoIdTest.kt @@ -12,9 +12,8 @@ class AutoIdTest { @Test @DisplayName("Generates a UUID string") - fun generateUUID() { + 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") @@ -41,120 +40,107 @@ class AutoIdTest { @Test @DisplayName("needsAutoId fails for null document") fun needsAutoIdFailsForNullDocument() { - assertThrows { AutoId.needsAutoId(AutoId.DISABLED, null, "id") } + 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") } + assertThrows { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } } @Test @DisplayName("needsAutoId returns false if disabled") - fun needsAutoIdFalseIfDisabled() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + fun needsAutoIdFalseForLongWithNonZero() = assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(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") } + assertThrows { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } } @Test @DisplayName("needsAutoId returns true for UUID strategy and blank ID") - fun needsAutoIdTrueForUUIDWithBlank() { + 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() { + 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") } + assertThrows { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } } @Test @DisplayName("needsAutoId returns true for Random String strategy and blank ID") - fun needsAutoIdTrueForRandomWithBlank() { + 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() { + 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") } + assertThrows { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } } } -data class ByteIdClass(var id: Byte) -data class ShortIdClass(var id: Short) -data class IntIdClass(var id: Int) -data class LongIdClass(var id: Long) -data class StringIdClass(var id: String) +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/test/kotlin/ComparisonTest.kt b/src/test/kotlin/ComparisonTest.kt new file mode 100644 index 0000000..7c015e0 --- /dev/null +++ b/src/test/kotlin/ComparisonTest.kt @@ -0,0 +1,91 @@ +package solutions.bitbadger.documents + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ComparisonTest { + + @Test + @DisplayName("isNumeric is false for empty list of values") + fun isNumericFalseForEmptyList() = + assertFalse(Comparison(Op.IN, listOf()).isNumeric, "An IN with empty list should not be numeric") + + @Test + @DisplayName("isNumeric is false for IN with strings") + fun isNumericFalseForStringsAndIn() = + assertFalse(Comparison(Op.IN, listOf("a", "b", "c")).isNumeric, "An IN with strings should not be numeric") + + @Test + @DisplayName("isNumeric is true for IN with bytes") + fun isNumericTrueForByteAndIn() = + assertTrue(Comparison(Op.IN, listOf(4, 8)).isNumeric, "An IN with bytes should be numeric") + + @Test + @DisplayName("isNumeric is true for IN with shorts") + fun isNumericTrueForShortAndIn() = + assertTrue(Comparison(Op.IN, listOf(18, 22)).isNumeric, "An IN with shorts should be numeric") + + @Test + @DisplayName("isNumeric is true for IN with ints") + fun isNumericTrueForIntAndIn() = + assertTrue(Comparison(Op.IN, listOf(7, 8, 9)).isNumeric, "An IN with ints should be numeric") + + @Test + @DisplayName("isNumeric is true for IN with longs") + fun isNumericTrueForLongAndIn() = + assertTrue(Comparison(Op.IN, listOf(3L)).isNumeric, "An IN with longs should be numeric") + + @Test + @DisplayName("isNumeric is false for BETWEEN with strings") + fun isNumericFalseForStringsAndBetween() = + assertFalse(Comparison(Op.BETWEEN, Pair("eh", "zed")).isNumeric, + "A BETWEEN with strings should not be numeric") + + @Test + @DisplayName("isNumeric is true for BETWEEN with bytes") + fun isNumericTrueForByteAndBetween() = + assertTrue(Comparison(Op.BETWEEN, Pair(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric") + + @Test + @DisplayName("isNumeric is true for BETWEEN with shorts") + fun isNumericTrueForShortAndBetween() = + assertTrue(Comparison(Op.BETWEEN, Pair(0, 9)).isNumeric, + "A BETWEEN with shorts should be numeric") + + @Test + @DisplayName("isNumeric is true for BETWEEN with ints") + fun isNumericTrueForIntAndBetween() = + assertTrue(Comparison(Op.BETWEEN, Pair(15, 44)).isNumeric, "A BETWEEN with ints should be numeric") + + @Test + @DisplayName("isNumeric is true for BETWEEN with longs") + fun isNumericTrueForLongAndBetween() = + assertTrue(Comparison(Op.BETWEEN, Pair(9L, 12L)).isNumeric, "A BETWEEN with longs should be numeric") + + @Test + @DisplayName("isNumeric is false for string value") + fun isNumericFalseForString() = + assertFalse(Comparison(Op.EQUAL, "80").isNumeric, "A string should not be numeric") + + @Test + @DisplayName("isNumeric is true for byte value") + fun isNumericTrueForByte() = + assertTrue(Comparison(Op.EQUAL, 47.toByte()).isNumeric, "A byte should be numeric") + + @Test + @DisplayName("isNumeric is true for short value") + fun isNumericTrueForShort() = + assertTrue(Comparison(Op.EQUAL, 2.toShort()).isNumeric, "A short should be numeric") + + @Test + @DisplayName("isNumeric is true for int value") + fun isNumericTrueForInt() = + assertTrue(Comparison(Op.EQUAL, 555).isNumeric, "An int should be numeric") + + @Test + @DisplayName("isNumeric is true for long value") + fun isNumericTrueForLong() = + assertTrue(Comparison(Op.EQUAL, 82L).isNumeric, "A long should be numeric") +} diff --git a/src/test/kotlin/ConfigurationTest.kt b/src/test/kotlin/ConfigurationTest.kt index 8dc67d1..cc8f06d 100644 --- a/src/test/kotlin/ConfigurationTest.kt +++ b/src/test/kotlin/ConfigurationTest.kt @@ -2,10 +2,20 @@ package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class ConfigurationTest { + @Test + @DisplayName("Default JSON options are as expected") + fun defaultJsonOptions() { + assertTrue(Configuration.json.configuration.encodeDefaults, "Encode Defaults should have been set") + assertFalse(Configuration.json.configuration.explicitNulls, "Explicit Nulls should not have been set") + } + @Test @DisplayName("Default ID field is `id`") fun defaultIdField() { @@ -23,4 +33,16 @@ class ConfigurationTest { 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/test/kotlin/DialectTest.kt b/src/test/kotlin/DialectTest.kt new file mode 100644 index 0000000..a7e4015 --- /dev/null +++ b/src/test/kotlin/DialectTest.kt @@ -0,0 +1,34 @@ +package solutions.bitbadger.documents + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +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 PostgreSQL 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") + } 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/test/kotlin/FieldTest.kt b/src/test/kotlin/FieldTest.kt index 1114e0b..9782532 100644 --- a/src/test/kotlin/FieldTest.kt +++ b/src/test/kotlin/FieldTest.kt @@ -1,14 +1,302 @@ package solutions.bitbadger.documents +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 kotlin.test.assertEquals +import kotlin.test.assertNotSame import kotlin.test.assertNull class FieldTest { + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + Configuration.connectionString = null + } + + // ~~~ INSTANCE METHODS ~~~ + @Test - @DisplayName("equal constructs a field") + @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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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() { + Configuration.connectionString = ":postgresql:" + 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() { + Configuration.connectionString = ":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") @@ -19,7 +307,18 @@ class FieldTest { } @Test - @DisplayName("greater constructs a field") + @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") @@ -30,7 +329,18 @@ class FieldTest { } @Test - @DisplayName("greaterOrEqual constructs a field") + @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") @@ -41,7 +351,18 @@ class FieldTest { } @Test - @DisplayName("less constructs a field") + @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") @@ -52,7 +373,18 @@ class FieldTest { } @Test - @DisplayName("lessOrEqual constructs a field") + @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") @@ -63,7 +395,18 @@ class FieldTest { } @Test - @DisplayName("notEqual constructs a field") + @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") @@ -74,7 +417,18 @@ class FieldTest { } @Test - @DisplayName("between constructs a field") + @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") @@ -86,8 +440,20 @@ class FieldTest { } @Test - @DisplayName("any constructs a field") - fun inCtor() { + @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, "The parameter name should have been null") + 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") @@ -97,7 +463,18 @@ class FieldTest { } @Test - @DisplayName("inArray constructs a field") + @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") @@ -108,6 +485,18 @@ class FieldTest { 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() { @@ -143,135 +532,53 @@ class FieldTest { @Test @DisplayName("nameToPath creates a simple PostgreSQL SQL name") - fun nameToPathPostgresSimpleSQL() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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") - } - - @Test - @DisplayName("withParameterName adjusts the parameter name") - fun withParameterName() { - assertEquals(":name", Field.equal("Bob", "Tom").withParameterName(":name").parameterName, - "Parameter name not filled correctly") - } - - @Test - @DisplayName("withQualifier adjust the table qualifier") - fun withQualifier() { - assertEquals("joe", Field.equal("Bill", "Matt").withQualifier("joe").qualifier, - "Qualifier not filled 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") - } } diff --git a/src/test/kotlin/QueryTest.kt b/src/test/kotlin/QueryTest.kt index 3799154..16de55a 100644 --- a/src/test/kotlin/QueryTest.kt +++ b/src/test/kotlin/QueryTest.kt @@ -1,5 +1,6 @@ package solutions.bitbadger.documents +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import kotlin.test.assertEquals @@ -9,6 +10,14 @@ class QueryTest { /** Test table name */ private val tbl = "test_table" + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + fun cleanUp() { + Configuration.connectionString = null + } + @Test @DisplayName("statementWhere generates correctly") fun statementWhere() { @@ -67,30 +76,17 @@ class QueryTest { @Test @DisplayName("insert generates correctly") fun insert() { - try { - Configuration.connectionString = "postgresql" - assertEquals( - "INSERT INTO $tbl VALUES (:data)", - Query.insert(tbl), - "INSERT statement not constructed correctly" - ) - } finally { - Configuration.connectionString = null - } + Configuration.connectionString = ":postgresql:" + assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl), "INSERT statement not constructed correctly") } @Test @DisplayName("save generates correctly") fun save() { - try { - Configuration.connectionString = "postgresql" - assertEquals( - "INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", - Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly" - ) - } finally { - Configuration.connectionString = null - } + Configuration.connectionString = ":postgresql:" + assertEquals( + "INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", + Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly") } @Test -- 2.47.2 From 0b11b803ccd48d9040e78d8d215fc91203355a36 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Feb 2025 18:53:33 -0500 Subject: [PATCH 10/88] Add validation to field param name --- src/main/kotlin/Field.kt | 12 +++++++----- src/test/kotlin/FieldTest.kt | 6 ++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/Field.kt b/src/main/kotlin/Field.kt index 11fb00d..924944d 100644 --- a/src/main/kotlin/Field.kt +++ b/src/main/kotlin/Field.kt @@ -14,17 +14,19 @@ class Field private constructor( 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 { - if (!paramName.startsWith(':') && !paramName.startsWith('@')) - throw DocumentException("Parameter must start with : or @ ($paramName)") - return Field(name, comparison, paramName, qualifier) - } + fun withParameterName(paramName: String) = + Field(name, comparison, paramName, qualifier) /** * Specify a qualifier (alias) for the document table diff --git a/src/test/kotlin/FieldTest.kt b/src/test/kotlin/FieldTest.kt index 9782532..5aeff2a 100644 --- a/src/test/kotlin/FieldTest.kt +++ b/src/test/kotlin/FieldTest.kt @@ -530,6 +530,12 @@ class FieldTest { 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() = -- 2.47.2 From 2b6dff9fd3d6b8143a8499f1058368a383c8c367 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 18 Feb 2025 17:54:18 -0500 Subject: [PATCH 11/88] WIP on query tests --- .idea/misc.xml | 1 + src/main/kotlin/Comparison.kt | 2 +- src/main/kotlin/Parameters.kt | 9 +- src/main/kotlin/Results.kt | 12 +-- src/test/kotlin/ParametersTest.kt | 28 +++++ src/test/kotlin/QueryTest.kt | 171 +++++++++++++++++++++++------- 6 files changed, 173 insertions(+), 50 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 9959fd1..e56fe0c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,6 +6,7 @@ \ No newline at end of file diff --git a/src/integration-test/kotlin/CustomSQLiteIT.kt b/src/integration-test/kotlin/CustomSQLiteIT.kt index c15ff76..1b97baf 100644 --- a/src/integration-test/kotlin/CustomSQLiteIT.kt +++ b/src/integration-test/kotlin/CustomSQLiteIT.kt @@ -1,43 +1,103 @@ package solutions.bitbadger.documents -import kotlinx.serialization.Serializable -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.query.Definition +import solutions.bitbadger.documents.query.Count import solutions.bitbadger.documents.query.Find +import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull - +/** + * SQLite integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("SQLite - Custom") class CustomSQLiteIT { - private val tbl = "test_table"; - - @BeforeEach - fun setUp() { - Configuration.connectionString = "jdbc:sqlite:memory" - } - - /** - * Reset the dialect - */ - @AfterEach - fun cleanUp() { - Configuration.dialectValue = null - } - @Test @DisplayName("list succeeds with empty list") - fun listEmpty() { - Configuration.dbConn().use { conn -> - conn.customNonQuery(Definition.ensureTable(tbl), listOf()) - conn.customNonQuery(Definition.ensureKey(tbl, Dialect.SQLITE), listOf()) - val result = conn.customList(Find.all(tbl), listOf(), Results::fromData) + fun listEmpty() = + SQLiteDB().use { db -> + JsonDocument.load(db.conn, SQLiteDB.tableName) + db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}") + val result = db.conn.customList(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData) assertEquals(0, result.size, "There should have been no results") } - } -} -@Serializable -data class TestDocument(val id: String) + @Test + @DisplayName("list succeeds with a non-empty list") + fun listAll() = + SQLiteDB().use { db -> + JsonDocument.load(db.conn, SQLiteDB.tableName) + val result = db.conn.customList(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData) + assertEquals(5, result.size, "There should have been 5 results") + } + + @Test + @DisplayName("single succeeds when document not found") + fun singleNone() = + SQLiteDB().use { db -> + assertNull( + db.conn.customSingle(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData), + "There should not have been a document returned" + ) + } + + @Test + @DisplayName("single succeeds when a document is found") + fun singleOne() { + SQLiteDB().use { db -> + JsonDocument.load(db.conn, SQLiteDB.tableName) + assertNotNull( + db.conn.customSingle(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData), + "There should not have been a document returned" + ) + } + } + + @Test + @DisplayName("nonQuery makes changes") + fun nonQueryChanges() = + SQLiteDB().use { db -> + JsonDocument.load(db.conn, SQLiteDB.tableName) + assertEquals( + 5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount), + "There should have been 5 documents in the table" + ) + db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}") + assertEquals( + 0L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount), + "There should have been no documents in the table" + ) + } + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + fun nonQueryNoChanges() = + SQLiteDB().use { db -> + JsonDocument.load(db.conn, SQLiteDB.tableName) + assertEquals( + 5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount), + "There should have been 5 documents in the table" + ) + db.conn.customNonQuery( + "DELETE FROM ${SQLiteDB.tableName} WHERE data->>'id' = :id", + listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) + ) + assertEquals( + 5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount), + "There should still have been 5 documents in the table" + ) + } + + @Test + @DisplayName("scalar succeeds") + fun scalar() = + SQLiteDB().use { db -> + assertEquals( + 3L, + db.conn.customScalar("SELECT 3 AS it FROM ${SQLiteDB.catalog} LIMIT 1", mapFunc = Results::toCount), + "The number 3 should have been returned" + ) + } +} diff --git a/src/integration-test/kotlin/DefinitionSQLiteIT.kt b/src/integration-test/kotlin/DefinitionSQLiteIT.kt new file mode 100644 index 0000000..4e27ffd --- /dev/null +++ b/src/integration-test/kotlin/DefinitionSQLiteIT.kt @@ -0,0 +1,45 @@ +package solutions.bitbadger.documents + +import org.junit.jupiter.api.DisplayName +import java.sql.Connection +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("SQLite - Definition") +class DefinitionSQLiteIT { + + /** + * Determine if a database item exists + * + * @param item The items whose existence should be checked + * @param conn The current database connection + * @return True if the item exists in the given database, false if not + */ + private fun itExists(item: String, conn: Connection) = + conn.customScalar("SELECT EXISTS (SELECT 1 FROM ${SQLiteDB.catalog} WHERE name = :name) AS it", + listOf(Parameter(":name", ParameterType.STRING, item)), Results::toExists) + + @Test + @DisplayName("ensureTable creates table and index") + fun ensureTable() = + SQLiteDB().use { db -> + assertFalse(itExists("ensured", db.conn), "The 'ensured' table should not exist") + assertFalse(itExists("idx_ensured_key", db.conn), "The PK index for the 'ensured' table should not exist") + db.conn.ensureTable("ensured") + assertTrue(itExists("ensured", db.conn), "The 'ensured' table should exist") + assertTrue(itExists("idx_ensured_key", db.conn), "The PK index for the 'ensured' table should now exist") + } + + @Test + @DisplayName("ensureFieldIndex creates an index") + fun ensureFieldIndex() = + SQLiteDB().use { db -> + assertFalse(itExists("idx_${SQLiteDB.tableName}_test", db.conn), "The test index should not exist") + db.conn.ensureFieldIndex(SQLiteDB.tableName, "test", listOf("id", "category")) + assertTrue(itExists("idx_${SQLiteDB.tableName}_test", db.conn), "The test index should now exist") + } +} diff --git a/src/integration-test/kotlin/DocumentSQLiteIT.kt b/src/integration-test/kotlin/DocumentSQLiteIT.kt new file mode 100644 index 0000000..e4dd09a --- /dev/null +++ b/src/integration-test/kotlin/DocumentSQLiteIT.kt @@ -0,0 +1,66 @@ +package solutions.bitbadger.documents + +import org.junit.jupiter.api.DisplayName +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +/** + * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("SQLite - Document") +class DocumentSQLiteIT { + + @Test + @DisplayName("insert works with default values") + fun insertDefault() = + SQLiteDB().use { db -> + assertEquals(0L, db.conn.countAll(SQLiteDB.tableName), "There should be no documents in the table") + val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) + db.conn.insert(SQLiteDB.tableName, doc) + val after = db.conn.findAll(SQLiteDB.tableName) + assertEquals(1, after.size, "There should be one document in the table") + assertEquals(doc, after[0], "The document should be what was inserted") + } + + @Test + @DisplayName("insert fails with duplicate key") + fun insertDupe() = + SQLiteDB().use { db -> + db.conn.insert(SQLiteDB.tableName, JsonDocument("a", "", 0, null)) + try { + db.conn.insert(SQLiteDB.tableName, JsonDocument("a", "b", 22, null)) + fail("Inserting a document with a duplicate key should have thrown an exception") + } catch (_: Exception) { + // yay + } + } + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + fun insertNumAutoId() = + SQLiteDB().use { db -> + try { + Configuration.autoIdStrategy = AutoId.NUMBER + Configuration.idField = "key" + assertEquals(0L, db.conn.countAll(SQLiteDB.tableName), "There should be no documents in the table") + + db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "one")) + db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "two")) + db.conn.insert(SQLiteDB.tableName, NumIdDocument(77, "three")) + db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "four")) + + val after = db.conn.findAll(SQLiteDB.tableName, 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" + } + } + + // TODO: UUID, Random String +} diff --git a/src/integration-test/kotlin/SQLiteDB.kt b/src/integration-test/kotlin/SQLiteDB.kt new file mode 100644 index 0000000..2f0118a --- /dev/null +++ b/src/integration-test/kotlin/SQLiteDB.kt @@ -0,0 +1,36 @@ +package solutions.bitbadger.documents + +import java.io.File + +/** + * A wrapper for a throwaway SQLite database + */ +class SQLiteDB : AutoCloseable { + + private var dbName = ""; + + init { + dbName = "test-db-${AutoId.generateRandomString(8)}.db" + Configuration.connectionString = "jdbc:sqlite:$dbName" + } + + val conn = Configuration.dbConn() + + init { + conn.ensureTable(tableName) + } + + override fun close() { + conn.close() + File(dbName).delete() + } + + companion object { + + /** The catalog table for SQLite's schema */ + val catalog = "sqlite_master" + + /** The table used for test documents */ + val tableName = "test_table" + } +} diff --git a/src/integration-test/kotlin/Types.kt b/src/integration-test/kotlin/Types.kt new file mode 100644 index 0000000..a82af38 --- /dev/null +++ b/src/integration-test/kotlin/Types.kt @@ -0,0 +1,40 @@ +package solutions.bitbadger.documents + +import kotlinx.serialization.Serializable +import java.sql.Connection + +@Serializable +data class NumIdDocument(val key: Int, val text: String) + +@Serializable +data class SubDocument(val foo: String, val bar: String) + +@Serializable +data class ArrayDocument(val id: String, val values: List) { + 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, val sub: SubDocument?) { + companion object { + /** An empty JsonDocument */ + val emptyDoc = JsonDocument("", "", 0, null) + + /** Documents to use for testing */ + 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(conn: Connection, tableName: String = "test_table") = + testDocuments.forEach { conn.insert(tableName, it) } + } +} diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 553107b..8c87011 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -3,6 +3,8 @@ package solutions.bitbadger.documents import java.sql.Connection import java.sql.ResultSet +// ~~~ CUSTOM QUERIES ~~~ + /** * Execute a query that returns a list of results * @@ -12,7 +14,7 @@ import java.sql.ResultSet * @return A list of results for the given query */ inline fun Connection.customList( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Custom.list(query, parameters, this, mapFunc) /** @@ -24,7 +26,7 @@ inline fun Connection.customList( * @return The document if one matches the query, `null` otherwise */ inline fun Connection.customSingle( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Custom.single(query, parameters, this, mapFunc) /** @@ -33,7 +35,7 @@ inline fun Connection.customSingle( * @param query The query to retrieve the results * @param parameters Parameters to use for the query */ -fun Connection.customNonQuery(query: String, parameters: Collection>) = +fun Connection.customNonQuery(query: String, parameters: Collection> = listOf()) = Custom.nonQuery(query, parameters, this) /** @@ -46,6 +48,69 @@ fun Connection.customNonQuery(query: String, parameters: Collection */ inline fun Connection.customScalar( query: String, - parameters: Collection>, + parameters: Collection> = listOf(), mapFunc: (ResultSet) -> T & Any ) = 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) + +// ~~~ 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) + +// ~~~ 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) + + +// ~~~ DOCUMENT RETRIEVAL QUERIES ~~~ + +/** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @return A list of documents from the given table + */ +inline fun Connection.findAll(tableName: String) = + Find.all(tableName, this) + +/** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @return A list of documents from the given table + */ +inline fun Connection.findAll(tableName: String, orderBy: Collection>) = + Find.all(tableName, orderBy, this) diff --git a/src/main/kotlin/Count.kt b/src/main/kotlin/Count.kt new file mode 100644 index 0000000..c168a22 --- /dev/null +++ b/src/main/kotlin/Count.kt @@ -0,0 +1,29 @@ +package solutions.bitbadger.documents + +import solutions.bitbadger.documents.query.Count +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(Count.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) } +} diff --git a/src/main/kotlin/Custom.kt b/src/main/kotlin/Custom.kt index 87aa352..1902aec 100644 --- a/src/main/kotlin/Custom.kt +++ b/src/main/kotlin/Custom.kt @@ -18,7 +18,7 @@ object Custom { * @return A list of results for the given query */ inline fun list( - query: String, parameters: Collection>, conn: Connection, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc ) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, mapFunc) } /** @@ -30,7 +30,7 @@ object Custom { * @return A list of results for the given query */ inline fun list( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) } /** @@ -43,7 +43,7 @@ object Custom { * @return The document if one matches the query, `null` otherwise */ inline fun single( - query: String, parameters: Collection>, conn: Connection, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc ) = list("$query LIMIT 1", parameters, conn, mapFunc).singleOrNull() /** @@ -55,7 +55,7 @@ object Custom { * @return The document if one matches the query, `null` otherwise */ inline fun single( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) } /** @@ -65,7 +65,7 @@ object Custom { * @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>, conn: Connection) { + fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) { Parameters.apply(conn, query, parameters).use { it.executeUpdate() } } @@ -75,7 +75,7 @@ object Custom { * @param query The query to retrieve the results * @param parameters Parameters to use for the query */ - fun nonQuery(query: String, parameters: Collection>) = + fun nonQuery(query: String, parameters: Collection> = listOf()) = Configuration.dbConn().use { nonQuery(query, parameters, it) } /** @@ -88,7 +88,7 @@ object Custom { * @return The scalar value from the query */ inline fun scalar( - query: String, parameters: Collection>, conn: Connection, mapFunc: (ResultSet) -> T & Any + query: String, parameters: Collection> = listOf(), conn: Connection, mapFunc: (ResultSet) -> T & Any ) = Parameters.apply(conn, query, parameters).use { stmt -> stmt.executeQuery().use { rs -> rs.next() @@ -105,6 +105,6 @@ object Custom { * @return The scalar value from the query */ inline fun scalar( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> T & Any + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> T & Any ) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) } } diff --git a/src/main/kotlin/Definition.kt b/src/main/kotlin/Definition.kt new file mode 100644 index 0000000..9ad8da0 --- /dev/null +++ b/src/main/kotlin/Definition.kt @@ -0,0 +1,51 @@ +package solutions.bitbadger.documents + +import java.sql.Connection +import solutions.bitbadger.documents.query.Definition + +/** + * 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) = + Configuration.dialect("ensure $tableName exists").let { + conn.customNonQuery(Definition.ensureTable(tableName, it)) + conn.customNonQuery(Definition.ensureKey(tableName, it)) + } + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + */ + 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 + */ + fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection, conn: Connection) = + conn.customNonQuery(Definition.ensureIndexOn(tableName, indexName, fields)) + + /** + * 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) = + Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) } +} diff --git a/src/main/kotlin/Document.kt b/src/main/kotlin/Document.kt new file mode 100644 index 0000000..1b6ab3f --- /dev/null +++ b/src/main/kotlin/Document.kt @@ -0,0 +1,54 @@ +package solutions.bitbadger.documents + +import java.sql.Connection +import solutions.bitbadger.documents.query.Document + +/** + * 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) { + Document.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 (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 { + when (dialect) { + Dialect.POSTGRESQL -> ":data::jsonb || ('{\"$idField\":$it}')::jsonb" + Dialect.SQLITE -> "json_set(:data, '$.$idField', $it)" + } + } + } else { + ":data" + } + + Document.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) } +} diff --git a/src/main/kotlin/Find.kt b/src/main/kotlin/Find.kt new file mode 100644 index 0000000..b9131ea --- /dev/null +++ b/src/main/kotlin/Find.kt @@ -0,0 +1,52 @@ +package solutions.bitbadger.documents + +import java.sql.Connection +import solutions.bitbadger.documents.query.Find +import solutions.bitbadger.documents.query.orderBy + +/** + * Functions to find and retrieve documents + */ +object Find { + + /** + * 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) = + conn.customList(Find.all(tableName), mapFunc = Results::fromData) + + /** + * Retrieve all documents in the given table + * + * @param tableName The table from which documents should be retrieved + * @return A list of documents from the given table + */ + inline fun all(tableName: String) = + Configuration.dbConn().use { all(tableName, it) } + + /** + * 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 + * @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>, conn: Connection) = + conn.customList(Find.all(tableName) + orderBy(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 + * @return A list of documents from the given table + */ + inline fun all(tableName: String, orderBy: Collection>) = + Configuration.dbConn().use { all(tableName, orderBy, it) } + +} diff --git a/src/main/kotlin/Parameter.kt b/src/main/kotlin/Parameter.kt index 97fe766..0503812 100644 --- a/src/main/kotlin/Parameter.kt +++ b/src/main/kotlin/Parameter.kt @@ -12,4 +12,7 @@ class Parameter(val name: String, val type: ParameterType, val value: T) { if (!name.startsWith(':') && !name.startsWith('@')) throw DocumentException("Name must start with : or @ ($name)") } + + override fun toString() = + "$type[$name] = $value" } diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index c44cd34..912e9f8 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -29,6 +29,16 @@ object Parameters { } } + /** + * 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, Configuration.json.encodeToString(value)) + /** * Replace the parameter names in the query with question marks * @@ -93,7 +103,7 @@ object Parameters { } } - ParameterType.JSON -> stmt.setString(idx, Configuration.json.encodeToString(param.value)) + ParameterType.JSON -> stmt.setString(idx, param.value as String) } } } diff --git a/src/main/kotlin/query/Definition.kt b/src/main/kotlin/query/Definition.kt index b2425e4..8180383 100644 --- a/src/main/kotlin/query/Definition.kt +++ b/src/main/kotlin/query/Definition.kt @@ -24,10 +24,11 @@ object Definition { * 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 */ - fun ensureTable(tableName: String) = - when (Configuration.dialect("create table creation query")) { + 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") } @@ -47,15 +48,21 @@ object Definition { * @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 + * @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 */ - fun ensureIndexOn(tableName: String, indexName: String, fields: Collection, dialect: Dialect): String { + 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], dialect, FieldFormat.SQL) + ")$direction" + "(" + Field.nameToPath(parts[0], mode, FieldFormat.SQL) + ")$direction" } return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)" } @@ -64,9 +71,9 @@ object Definition { * 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 + * @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 */ - fun ensureKey(tableName: String, dialect: Dialect) = + fun ensureKey(tableName: String, dialect: Dialect? = null) = ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX") } diff --git a/src/main/kotlin/query/Query.kt b/src/main/kotlin/query/Query.kt index 20e6096..39784bc 100644 --- a/src/main/kotlin/query/Query.kt +++ b/src/main/kotlin/query/Query.kt @@ -1,5 +1,6 @@ package solutions.bitbadger.documents.query +import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch @@ -44,7 +45,8 @@ fun byFields(statement: String, fields: Collection>, howMatched: FieldM * @param dialect The SQL dialect for the generated clause * @return An `ORDER BY` clause for the given fields */ -fun orderBy(fields: Collection>, dialect: Dialect): String { +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) = @@ -56,18 +58,18 @@ fun orderBy(fields: Collection>, dialect: Dialect): String { } val path = when { field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld -> - when (dialect) { - Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric" - Dialect.SQLITE -> fld.path(dialect) + when (mode) { + Dialect.POSTGRESQL -> "(${fld.path(mode)})::numeric" + Dialect.SQLITE -> fld.path(mode) } } - field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(dialect).let { p -> - when (dialect) { + 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(dialect) + else -> field.path(mode) } "$path${direction ?: ""}" } -- 2.47.2 From b2d700e6586490250370e570fc013104bfe4d909 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 26 Feb 2025 23:24:02 -0500 Subject: [PATCH 20/88] Move ITs to common files, call from both DBs - Fix PostgreSQL auto-number syntax --- pom.xml | 6 + src/integration-test/kotlin/CustomSQLiteIT.kt | 103 ------------------ .../kotlin/DocumentSQLiteIT.kt | 66 ----------- src/integration-test/kotlin/Types.kt | 3 + src/integration-test/kotlin/common/Custom.kt | 80 ++++++++++++++ .../kotlin/common/Document.kt | 59 ++++++++++ .../kotlin/postgresql/CustomIT.kt | 48 ++++++++ .../kotlin/postgresql/DocumentIT.kt | 27 +++++ .../kotlin/postgresql/PgDB.kt | 53 +++++++++ .../kotlin/sqlite/CustomIT.kt | 47 ++++++++ .../DefinitionIT.kt} | 5 +- .../kotlin/sqlite/DocumentIT.kt | 28 +++++ .../kotlin/{ => sqlite}/SQLiteDB.kt | 5 +- src/main/kotlin/Document.kt | 30 +++-- src/main/kotlin/Parameters.kt | 4 +- 15 files changed, 378 insertions(+), 186 deletions(-) delete mode 100644 src/integration-test/kotlin/CustomSQLiteIT.kt delete mode 100644 src/integration-test/kotlin/DocumentSQLiteIT.kt create mode 100644 src/integration-test/kotlin/common/Custom.kt create mode 100644 src/integration-test/kotlin/common/Document.kt create mode 100644 src/integration-test/kotlin/postgresql/CustomIT.kt create mode 100644 src/integration-test/kotlin/postgresql/DocumentIT.kt create mode 100644 src/integration-test/kotlin/postgresql/PgDB.kt create mode 100644 src/integration-test/kotlin/sqlite/CustomIT.kt rename src/integration-test/kotlin/{DefinitionSQLiteIT.kt => sqlite/DefinitionIT.kt} (94%) create mode 100644 src/integration-test/kotlin/sqlite/DocumentIT.kt rename src/integration-test/kotlin/{ => sqlite}/SQLiteDB.kt (78%) diff --git a/pom.xml b/pom.xml index 3e2ef74..4c36a0f 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,12 @@ 3.46.1.2 integration-test + + org.postgresql + postgresql + 42.7.5 + integration-test + \ No newline at end of file diff --git a/src/integration-test/kotlin/CustomSQLiteIT.kt b/src/integration-test/kotlin/CustomSQLiteIT.kt deleted file mode 100644 index 1b97baf..0000000 --- a/src/integration-test/kotlin/CustomSQLiteIT.kt +++ /dev/null @@ -1,103 +0,0 @@ -package solutions.bitbadger.documents - -import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.query.Count -import solutions.bitbadger.documents.query.Find -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull - -/** - * SQLite integration tests for the `Custom` object / `custom*` connection extension functions - */ -@DisplayName("SQLite - Custom") -class CustomSQLiteIT { - - @Test - @DisplayName("list succeeds with empty list") - fun listEmpty() = - SQLiteDB().use { db -> - JsonDocument.load(db.conn, SQLiteDB.tableName) - db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}") - val result = db.conn.customList(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData) - assertEquals(0, result.size, "There should have been no results") - } - - @Test - @DisplayName("list succeeds with a non-empty list") - fun listAll() = - SQLiteDB().use { db -> - JsonDocument.load(db.conn, SQLiteDB.tableName) - val result = db.conn.customList(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData) - assertEquals(5, result.size, "There should have been 5 results") - } - - @Test - @DisplayName("single succeeds when document not found") - fun singleNone() = - SQLiteDB().use { db -> - assertNull( - db.conn.customSingle(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData), - "There should not have been a document returned" - ) - } - - @Test - @DisplayName("single succeeds when a document is found") - fun singleOne() { - SQLiteDB().use { db -> - JsonDocument.load(db.conn, SQLiteDB.tableName) - assertNotNull( - db.conn.customSingle(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData), - "There should not have been a document returned" - ) - } - } - - @Test - @DisplayName("nonQuery makes changes") - fun nonQueryChanges() = - SQLiteDB().use { db -> - JsonDocument.load(db.conn, SQLiteDB.tableName) - assertEquals( - 5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount), - "There should have been 5 documents in the table" - ) - db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}") - assertEquals( - 0L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount), - "There should have been no documents in the table" - ) - } - - @Test - @DisplayName("nonQuery makes no changes when where clause matches nothing") - fun nonQueryNoChanges() = - SQLiteDB().use { db -> - JsonDocument.load(db.conn, SQLiteDB.tableName) - assertEquals( - 5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount), - "There should have been 5 documents in the table" - ) - db.conn.customNonQuery( - "DELETE FROM ${SQLiteDB.tableName} WHERE data->>'id' = :id", - listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) - ) - assertEquals( - 5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount), - "There should still have been 5 documents in the table" - ) - } - - @Test - @DisplayName("scalar succeeds") - fun scalar() = - SQLiteDB().use { db -> - assertEquals( - 3L, - db.conn.customScalar("SELECT 3 AS it FROM ${SQLiteDB.catalog} LIMIT 1", mapFunc = Results::toCount), - "The number 3 should have been returned" - ) - } -} diff --git a/src/integration-test/kotlin/DocumentSQLiteIT.kt b/src/integration-test/kotlin/DocumentSQLiteIT.kt deleted file mode 100644 index e4dd09a..0000000 --- a/src/integration-test/kotlin/DocumentSQLiteIT.kt +++ /dev/null @@ -1,66 +0,0 @@ -package solutions.bitbadger.documents - -import org.junit.jupiter.api.DisplayName -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.fail - -/** - * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions - */ -@DisplayName("SQLite - Document") -class DocumentSQLiteIT { - - @Test - @DisplayName("insert works with default values") - fun insertDefault() = - SQLiteDB().use { db -> - assertEquals(0L, db.conn.countAll(SQLiteDB.tableName), "There should be no documents in the table") - val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) - db.conn.insert(SQLiteDB.tableName, doc) - val after = db.conn.findAll(SQLiteDB.tableName) - assertEquals(1, after.size, "There should be one document in the table") - assertEquals(doc, after[0], "The document should be what was inserted") - } - - @Test - @DisplayName("insert fails with duplicate key") - fun insertDupe() = - SQLiteDB().use { db -> - db.conn.insert(SQLiteDB.tableName, JsonDocument("a", "", 0, null)) - try { - db.conn.insert(SQLiteDB.tableName, JsonDocument("a", "b", 22, null)) - fail("Inserting a document with a duplicate key should have thrown an exception") - } catch (_: Exception) { - // yay - } - } - - @Test - @DisplayName("insert succeeds with numeric auto IDs") - fun insertNumAutoId() = - SQLiteDB().use { db -> - try { - Configuration.autoIdStrategy = AutoId.NUMBER - Configuration.idField = "key" - assertEquals(0L, db.conn.countAll(SQLiteDB.tableName), "There should be no documents in the table") - - db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "one")) - db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "two")) - db.conn.insert(SQLiteDB.tableName, NumIdDocument(77, "three")) - db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "four")) - - val after = db.conn.findAll(SQLiteDB.tableName, 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" - } - } - - // TODO: UUID, Random String -} diff --git a/src/integration-test/kotlin/Types.kt b/src/integration-test/kotlin/Types.kt index a82af38..6b6c595 100644 --- a/src/integration-test/kotlin/Types.kt +++ b/src/integration-test/kotlin/Types.kt @@ -38,3 +38,6 @@ data class JsonDocument(val id: String, val value: String, val numValue: Int, va testDocuments.forEach { conn.insert(tableName, it) } } } + +/** The test table name to use for integration tests */ +val testTableName = "test_table" diff --git a/src/integration-test/kotlin/common/Custom.kt b/src/integration-test/kotlin/common/Custom.kt new file mode 100644 index 0000000..54b7a93 --- /dev/null +++ b/src/integration-test/kotlin/common/Custom.kt @@ -0,0 +1,80 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.Count +import solutions.bitbadger.documents.query.Find +import java.sql.Connection +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +/** + * Integration tests for the `Custom` object + */ +object Custom { + + fun listEmpty(conn: Connection) { + JsonDocument.load(conn, testTableName) + conn.customNonQuery("DELETE FROM $testTableName") + val result = conn.customList(Find.all(testTableName), mapFunc = Results::fromData) + assertEquals(0, result.size, "There should have been no results") + } + + fun listAll(conn: Connection) { + JsonDocument.load(conn, testTableName) + val result = conn.customList(Find.all(testTableName), mapFunc = Results::fromData) + assertEquals(5, result.size, "There should have been 5 results") + } + + fun singleNone(conn: Connection) = + assertNull( + conn.customSingle(Find.all(testTableName), mapFunc = Results::fromData), + "There should not have been a document returned" + ) + + fun singleOne(conn: Connection) { + JsonDocument.load(conn, testTableName) + assertNotNull( + conn.customSingle(Find.all(testTableName), mapFunc = Results::fromData), + "There should not have been a document returned" + ) + } + + fun nonQueryChanges(conn: Connection) { + JsonDocument.load(conn, testTableName) + assertEquals( + 5L, conn.customScalar(Count.all(testTableName), mapFunc = Results::toCount), + "There should have been 5 documents in the table" + ) + conn.customNonQuery("DELETE FROM $testTableName") + assertEquals( + 0L, conn.customScalar(Count.all(testTableName), mapFunc = Results::toCount), + "There should have been no documents in the table" + ) + } + + fun nonQueryNoChanges(conn: Connection) { + JsonDocument.load(conn, testTableName) + assertEquals( + 5L, conn.customScalar(Count.all(testTableName), mapFunc = Results::toCount), + "There should have been 5 documents in the table" + ) + conn.customNonQuery( + "DELETE FROM $testTableName WHERE data->>'id' = :id", + listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) + ) + assertEquals( + 5L, conn.customScalar(Count.all(testTableName), mapFunc = Results::toCount), + "There should still have been 5 documents in the table" + ) + } + + fun scalar(conn: Connection) { + JsonDocument.load(conn, testTableName) + assertEquals( + 3L, + conn.customScalar("SELECT 3 AS it FROM $testTableName LIMIT 1", mapFunc = Results::toCount), + "The number 3 should have been returned" + ) + } +} diff --git a/src/integration-test/kotlin/common/Document.kt b/src/integration-test/kotlin/common/Document.kt new file mode 100644 index 0000000..613f831 --- /dev/null +++ b/src/integration-test/kotlin/common/Document.kt @@ -0,0 +1,59 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.sqlite.SQLiteDB +import java.sql.Connection +import kotlin.test.assertEquals +import kotlin.test.fail + +/** + * Integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +object Document { + + fun insertDefault(conn: Connection) { + assertEquals(0L, conn.countAll(testTableName), "There should be no documents in the table") + val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) + conn.insert(testTableName, doc) + val after = conn.findAll(testTableName) + 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(conn: Connection) { + conn.insert(testTableName, JsonDocument("a", "", 0, null)) + try { + conn.insert(testTableName, JsonDocument("a", "b", 22, null)) + fail("Inserting a document with a duplicate key should have thrown an exception") + } catch (_: Exception) { + // yay + } + } + + fun insertNumAutoId(conn: Connection) { + try { + Configuration.autoIdStrategy = AutoId.NUMBER + Configuration.idField = "key" + assertEquals(0L, conn.countAll(SQLiteDB.tableName), "There should be no documents in the table") + + conn.insert(SQLiteDB.tableName, NumIdDocument(0, "one")) + conn.insert(SQLiteDB.tableName, NumIdDocument(0, "two")) + conn.insert(SQLiteDB.tableName, NumIdDocument(77, "three")) + conn.insert(SQLiteDB.tableName, NumIdDocument(0, "four")) + + val after = conn.findAll(SQLiteDB.tableName, 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" + } + } + + // TODO: UUID, Random String + +} \ No newline at end of file diff --git a/src/integration-test/kotlin/postgresql/CustomIT.kt b/src/integration-test/kotlin/postgresql/CustomIT.kt new file mode 100644 index 0000000..d356e57 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/CustomIT.kt @@ -0,0 +1,48 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Custom + +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("PostgreSQL - Custom") +class CustomIT { + + @Test + @DisplayName("list succeeds with empty list") + fun listEmpty() = + PgDB().use { Custom.listEmpty(it.conn) } + + @Test + @DisplayName("list succeeds with a non-empty list") + fun listAll() = + PgDB().use { Custom.listAll(it.conn) } + + @Test + @DisplayName("single succeeds when document not found") + fun singleNone() = + PgDB().use { Custom.singleNone(it.conn) } + + @Test + @DisplayName("single succeeds when a document is found") + fun singleOne() = + PgDB().use { Custom.singleOne(it.conn) } + + @Test + @DisplayName("nonQuery makes changes") + fun nonQueryChanges() = + PgDB().use { Custom.nonQueryChanges(it.conn) } + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + fun nonQueryNoChanges() = + PgDB().use { Custom.nonQueryNoChanges(it.conn) } + + @Test + @DisplayName("scalar succeeds") + fun scalar() = + PgDB().use { Custom.scalar(it.conn) } +} diff --git a/src/integration-test/kotlin/postgresql/DocumentIT.kt b/src/integration-test/kotlin/postgresql/DocumentIT.kt new file mode 100644 index 0000000..003b23d --- /dev/null +++ b/src/integration-test/kotlin/postgresql/DocumentIT.kt @@ -0,0 +1,27 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Document +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("PostgreSQL - Document") +class DocumentIT { + + @Test + @DisplayName("insert works with default values") + fun insertDefault() = + PgDB().use { Document.insertDefault(it.conn) } + + @Test + @DisplayName("insert fails with duplicate key") + fun insertDupe() = + PgDB().use { Document.insertDupe(it.conn) } + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + fun insertNumAutoId() = + PgDB().use { Document.insertNumAutoId(it.conn) } +} diff --git a/src/integration-test/kotlin/postgresql/PgDB.kt b/src/integration-test/kotlin/postgresql/PgDB.kt new file mode 100644 index 0000000..cfc4da3 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/PgDB.kt @@ -0,0 +1,53 @@ +package solutions.bitbadger.documents.postgresql + +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.customNonQuery +import solutions.bitbadger.documents.ensureTable + +/** + * A wrapper for a throwaway PostgreSQL database + */ +class PgDB : AutoCloseable { + + private var dbName = "" + + init { + dbName = "throwaway_${AutoId.generateRandomString(8)}" + Configuration.connectionString = connString("postgres") + Configuration.dbConn().use { + it.customNonQuery("CREATE DATABASE $dbName") + } + Configuration.connectionString = connString(dbName) + } + + val conn = Configuration.dbConn() + + init { + conn.ensureTable(tableName) + } + + override fun close() { + conn.close() + Configuration.connectionString = connString("postgres") + Configuration.dbConn().use { + it.customNonQuery("DROP DATABASE $dbName") + } + Configuration.connectionString = null + } + + companion object { + + /** The table used for test documents */ + val tableName = "test_table" + + /** + * 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/integration-test/kotlin/sqlite/CustomIT.kt b/src/integration-test/kotlin/sqlite/CustomIT.kt new file mode 100644 index 0000000..723376b --- /dev/null +++ b/src/integration-test/kotlin/sqlite/CustomIT.kt @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Custom +import kotlin.test.Test + +/** + * SQLite integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("SQLite - Custom") +class CustomIT { + + @Test + @DisplayName("list succeeds with empty list") + fun listEmpty() = + SQLiteDB().use { Custom.listEmpty(it.conn) } + + @Test + @DisplayName("list succeeds with a non-empty list") + fun listAll() = + SQLiteDB().use { Custom.listAll(it.conn) } + + @Test + @DisplayName("single succeeds when document not found") + fun singleNone() = + SQLiteDB().use { Custom.singleNone(it.conn) } + + @Test + @DisplayName("single succeeds when a document is found") + fun singleOne() = + SQLiteDB().use { Custom.singleOne(it.conn) } + + @Test + @DisplayName("nonQuery makes changes") + fun nonQueryChanges() = + SQLiteDB().use { Custom.nonQueryChanges(it.conn) } + + @Test + @DisplayName("nonQuery makes no changes when where clause matches nothing") + fun nonQueryNoChanges() = + SQLiteDB().use { Custom.nonQueryNoChanges(it.conn) } + + @Test + @DisplayName("scalar succeeds") + fun scalar() = + SQLiteDB().use { Custom.scalar(it.conn) } +} diff --git a/src/integration-test/kotlin/DefinitionSQLiteIT.kt b/src/integration-test/kotlin/sqlite/DefinitionIT.kt similarity index 94% rename from src/integration-test/kotlin/DefinitionSQLiteIT.kt rename to src/integration-test/kotlin/sqlite/DefinitionIT.kt index 4e27ffd..4e85357 100644 --- a/src/integration-test/kotlin/DefinitionSQLiteIT.kt +++ b/src/integration-test/kotlin/sqlite/DefinitionIT.kt @@ -1,6 +1,7 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.sqlite import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.* import java.sql.Connection import kotlin.test.Test import kotlin.test.assertFalse @@ -10,7 +11,7 @@ import kotlin.test.assertTrue * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions */ @DisplayName("SQLite - Definition") -class DefinitionSQLiteIT { +class DefinitionIT { /** * Determine if a database item exists diff --git a/src/integration-test/kotlin/sqlite/DocumentIT.kt b/src/integration-test/kotlin/sqlite/DocumentIT.kt new file mode 100644 index 0000000..e2a7d98 --- /dev/null +++ b/src/integration-test/kotlin/sqlite/DocumentIT.kt @@ -0,0 +1,28 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Document +import kotlin.test.Test + +/** + * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions + */ +@DisplayName("SQLite - Document") +class DocumentIT { + + @Test + @DisplayName("insert works with default values") + fun insertDefault() = + SQLiteDB().use { Document.insertDefault(it.conn) } + + @Test + @DisplayName("insert fails with duplicate key") + fun insertDupe() = + SQLiteDB().use { Document.insertDupe(it.conn) } + + @Test + @DisplayName("insert succeeds with numeric auto IDs") + fun insertNumAutoId() = + SQLiteDB().use { Document.insertNumAutoId(it.conn) } + +} diff --git a/src/integration-test/kotlin/SQLiteDB.kt b/src/integration-test/kotlin/sqlite/SQLiteDB.kt similarity index 78% rename from src/integration-test/kotlin/SQLiteDB.kt rename to src/integration-test/kotlin/sqlite/SQLiteDB.kt index 2f0118a..db500a9 100644 --- a/src/integration-test/kotlin/SQLiteDB.kt +++ b/src/integration-test/kotlin/sqlite/SQLiteDB.kt @@ -1,5 +1,8 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.sqlite +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.ensureTable import java.io.File /** diff --git a/src/main/kotlin/Document.kt b/src/main/kotlin/Document.kt index 1b6ab3f..0f21a75 100644 --- a/src/main/kotlin/Document.kt +++ b/src/main/kotlin/Document.kt @@ -20,19 +20,25 @@ object Document { val query = if (strategy == AutoId.DISABLED) { Document.insert(tableName) } else { - val idField = Configuration.idField - val dialect = Configuration.dialect("Create auto-ID insert query") + val idField = Configuration.idField + val dialect = Configuration.dialect("Create auto-ID insert query") val dataParam = if (AutoId.needsAutoId(strategy, document, idField)) { - 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 { - when (dialect) { - Dialect.POSTGRESQL -> ":data::jsonb || ('{\"$idField\":$it}')::jsonb" - Dialect.SQLITE -> "json_set(:data, '$.$idField', $it)" - } + 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" diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 912e9f8..2f8466c 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -47,7 +47,7 @@ object Parameters { * @return The query, with name parameters changed to `?`s */ fun replaceNamesInQuery(query: String, parameters: Collection>) = - parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") } + parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") }.also(::println) /** * Apply the given parameters to the given query, returning a prepared statement @@ -103,7 +103,7 @@ object Parameters { } } - ParameterType.JSON -> stmt.setString(idx, param.value as String) + ParameterType.JSON -> stmt.setObject(idx, param.value as String, Types.OTHER) } } } -- 2.47.2 From ad3b7d231641f43fb526c4b72c33289ee92e69c0 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 26 Feb 2025 23:30:54 -0500 Subject: [PATCH 21/88] Tweaks to moved tests --- src/integration-test/kotlin/Types.kt | 2 +- src/integration-test/kotlin/common/Custom.kt | 36 +++++++++---------- .../kotlin/common/Document.kt | 26 +++++++------- .../kotlin/sqlite/DefinitionIT.kt | 8 ++--- .../kotlin/sqlite/SQLiteDB.kt | 10 +++--- 5 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/integration-test/kotlin/Types.kt b/src/integration-test/kotlin/Types.kt index 6b6c595..4421a20 100644 --- a/src/integration-test/kotlin/Types.kt +++ b/src/integration-test/kotlin/Types.kt @@ -40,4 +40,4 @@ data class JsonDocument(val id: String, val value: String, val numValue: Int, va } /** The test table name to use for integration tests */ -val testTableName = "test_table" +const val TEST_TABLE = "test_table" diff --git a/src/integration-test/kotlin/common/Custom.kt b/src/integration-test/kotlin/common/Custom.kt index 54b7a93..faaed25 100644 --- a/src/integration-test/kotlin/common/Custom.kt +++ b/src/integration-test/kotlin/common/Custom.kt @@ -14,66 +14,66 @@ import kotlin.test.assertNull object Custom { fun listEmpty(conn: Connection) { - JsonDocument.load(conn, testTableName) - conn.customNonQuery("DELETE FROM $testTableName") - val result = conn.customList(Find.all(testTableName), mapFunc = Results::fromData) + JsonDocument.load(conn, TEST_TABLE) + conn.customNonQuery("DELETE FROM $TEST_TABLE") + val result = conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) assertEquals(0, result.size, "There should have been no results") } fun listAll(conn: Connection) { - JsonDocument.load(conn, testTableName) - val result = conn.customList(Find.all(testTableName), mapFunc = Results::fromData) + JsonDocument.load(conn, TEST_TABLE) + val result = conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) assertEquals(5, result.size, "There should have been 5 results") } fun singleNone(conn: Connection) = assertNull( - conn.customSingle(Find.all(testTableName), mapFunc = Results::fromData), + conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), "There should not have been a document returned" ) fun singleOne(conn: Connection) { - JsonDocument.load(conn, testTableName) + JsonDocument.load(conn, TEST_TABLE) assertNotNull( - conn.customSingle(Find.all(testTableName), mapFunc = Results::fromData), + conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), "There should not have been a document returned" ) } fun nonQueryChanges(conn: Connection) { - JsonDocument.load(conn, testTableName) + JsonDocument.load(conn, TEST_TABLE) assertEquals( - 5L, conn.customScalar(Count.all(testTableName), mapFunc = Results::toCount), + 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been 5 documents in the table" ) - conn.customNonQuery("DELETE FROM $testTableName") + conn.customNonQuery("DELETE FROM $TEST_TABLE") assertEquals( - 0L, conn.customScalar(Count.all(testTableName), mapFunc = Results::toCount), + 0L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been no documents in the table" ) } fun nonQueryNoChanges(conn: Connection) { - JsonDocument.load(conn, testTableName) + JsonDocument.load(conn, TEST_TABLE) assertEquals( - 5L, conn.customScalar(Count.all(testTableName), mapFunc = Results::toCount), + 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been 5 documents in the table" ) conn.customNonQuery( - "DELETE FROM $testTableName WHERE data->>'id' = :id", + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) ) assertEquals( - 5L, conn.customScalar(Count.all(testTableName), mapFunc = Results::toCount), + 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should still have been 5 documents in the table" ) } fun scalar(conn: Connection) { - JsonDocument.load(conn, testTableName) + JsonDocument.load(conn, TEST_TABLE) assertEquals( 3L, - conn.customScalar("SELECT 3 AS it FROM $testTableName LIMIT 1", mapFunc = Results::toCount), + conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount), "The number 3 should have been returned" ) } diff --git a/src/integration-test/kotlin/common/Document.kt b/src/integration-test/kotlin/common/Document.kt index 613f831..63a4ed6 100644 --- a/src/integration-test/kotlin/common/Document.kt +++ b/src/integration-test/kotlin/common/Document.kt @@ -1,8 +1,6 @@ package solutions.bitbadger.documents.common -import org.junit.jupiter.api.DisplayName import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.sqlite.SQLiteDB import java.sql.Connection import kotlin.test.assertEquals import kotlin.test.fail @@ -13,18 +11,18 @@ import kotlin.test.fail object Document { fun insertDefault(conn: Connection) { - assertEquals(0L, conn.countAll(testTableName), "There should be no documents in the table") + assertEquals(0L, conn.countAll(TEST_TABLE), "There should be no documents in the table") val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) - conn.insert(testTableName, doc) - val after = conn.findAll(testTableName) + conn.insert(TEST_TABLE, doc) + val after = 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(conn: Connection) { - conn.insert(testTableName, JsonDocument("a", "", 0, null)) + conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) try { - conn.insert(testTableName, JsonDocument("a", "b", 22, null)) + conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) fail("Inserting a document with a duplicate key should have thrown an exception") } catch (_: Exception) { // yay @@ -35,14 +33,14 @@ object Document { try { Configuration.autoIdStrategy = AutoId.NUMBER Configuration.idField = "key" - assertEquals(0L, conn.countAll(SQLiteDB.tableName), "There should be no documents in the table") + assertEquals(0L, conn.countAll(TEST_TABLE), "There should be no documents in the table") - conn.insert(SQLiteDB.tableName, NumIdDocument(0, "one")) - conn.insert(SQLiteDB.tableName, NumIdDocument(0, "two")) - conn.insert(SQLiteDB.tableName, NumIdDocument(77, "three")) - conn.insert(SQLiteDB.tableName, NumIdDocument(0, "four")) + conn.insert(TEST_TABLE, NumIdDocument(0, "one")) + conn.insert(TEST_TABLE, NumIdDocument(0, "two")) + conn.insert(TEST_TABLE, NumIdDocument(77, "three")) + conn.insert(TEST_TABLE, NumIdDocument(0, "four")) - val after = conn.findAll(SQLiteDB.tableName, listOf(Field.named("key"))) + val after = 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() }, @@ -56,4 +54,4 @@ object Document { // TODO: UUID, Random String -} \ No newline at end of file +} diff --git a/src/integration-test/kotlin/sqlite/DefinitionIT.kt b/src/integration-test/kotlin/sqlite/DefinitionIT.kt index 4e85357..a952d5c 100644 --- a/src/integration-test/kotlin/sqlite/DefinitionIT.kt +++ b/src/integration-test/kotlin/sqlite/DefinitionIT.kt @@ -21,7 +21,7 @@ class DefinitionIT { * @return True if the item exists in the given database, false if not */ private fun itExists(item: String, conn: Connection) = - conn.customScalar("SELECT EXISTS (SELECT 1 FROM ${SQLiteDB.catalog} WHERE name = :name) AS it", + conn.customScalar("SELECT EXISTS (SELECT 1 FROM ${SQLiteDB.CATALOG} WHERE name = :name) AS it", listOf(Parameter(":name", ParameterType.STRING, item)), Results::toExists) @Test @@ -39,8 +39,8 @@ class DefinitionIT { @DisplayName("ensureFieldIndex creates an index") fun ensureFieldIndex() = SQLiteDB().use { db -> - assertFalse(itExists("idx_${SQLiteDB.tableName}_test", db.conn), "The test index should not exist") - db.conn.ensureFieldIndex(SQLiteDB.tableName, "test", listOf("id", "category")) - assertTrue(itExists("idx_${SQLiteDB.tableName}_test", db.conn), "The test index should now exist") + assertFalse(itExists("idx_${TEST_TABLE}_test", db.conn), "The test index should not exist") + db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category")) + assertTrue(itExists("idx_${TEST_TABLE}_test", db.conn), "The test index should now exist") } } diff --git a/src/integration-test/kotlin/sqlite/SQLiteDB.kt b/src/integration-test/kotlin/sqlite/SQLiteDB.kt index db500a9..0351911 100644 --- a/src/integration-test/kotlin/sqlite/SQLiteDB.kt +++ b/src/integration-test/kotlin/sqlite/SQLiteDB.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.sqlite import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.TEST_TABLE import solutions.bitbadger.documents.ensureTable import java.io.File @@ -10,7 +11,7 @@ import java.io.File */ class SQLiteDB : AutoCloseable { - private var dbName = ""; + private var dbName = "" init { dbName = "test-db-${AutoId.generateRandomString(8)}.db" @@ -20,7 +21,7 @@ class SQLiteDB : AutoCloseable { val conn = Configuration.dbConn() init { - conn.ensureTable(tableName) + conn.ensureTable(TEST_TABLE) } override fun close() { @@ -31,9 +32,6 @@ class SQLiteDB : AutoCloseable { companion object { /** The catalog table for SQLite's schema */ - val catalog = "sqlite_master" - - /** The table used for test documents */ - val tableName = "test_table" + const val CATALOG = "sqlite_master" } } -- 2.47.2 From 250e216ae8b07494d90dd79b63eacd63568eaebb Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 27 Feb 2025 17:32:00 -0500 Subject: [PATCH 22/88] Add common throwaway db; all ITs now work in both --- .../kotlin/ThrowawayDatabase.kt | 20 ++++++ src/integration-test/kotlin/Types.kt | 14 ++--- src/integration-test/kotlin/common/Custom.kt | 54 ++++++++-------- .../kotlin/common/Definition.kt | 28 +++++++++ .../kotlin/common/Document.kt | 63 ++++++++++++++----- .../kotlin/postgresql/CustomIT.kt | 14 ++--- .../kotlin/postgresql/DefinitionIT.kt | 22 +++++++ .../kotlin/postgresql/DocumentIT.kt | 16 ++++- .../kotlin/postgresql/PgDB.kt | 18 +++--- .../kotlin/sqlite/CustomIT.kt | 14 ++--- .../kotlin/sqlite/DefinitionIT.kt | 26 +------- .../kotlin/sqlite/DocumentIT.kt | 15 ++++- .../kotlin/sqlite/SQLiteDB.kt | 18 +++--- src/main/kotlin/Comparison.kt | 3 + src/main/kotlin/ConnectionExtensions.kt | 21 +++++++ src/main/kotlin/Delete.kt | 55 ++++++++++++++++ src/main/kotlin/Field.kt | 3 + src/main/kotlin/Parameters.kt | 26 +++++++- 18 files changed, 315 insertions(+), 115 deletions(-) create mode 100644 src/integration-test/kotlin/ThrowawayDatabase.kt create mode 100644 src/integration-test/kotlin/common/Definition.kt create mode 100644 src/integration-test/kotlin/postgresql/DefinitionIT.kt create mode 100644 src/main/kotlin/Delete.kt diff --git a/src/integration-test/kotlin/ThrowawayDatabase.kt b/src/integration-test/kotlin/ThrowawayDatabase.kt new file mode 100644 index 0000000..901f50b --- /dev/null +++ b/src/integration-test/kotlin/ThrowawayDatabase.kt @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents + +import java.sql.Connection + +/** + * Common interface for PostgreSQL and SQLite throwaway databases + */ +interface ThrowawayDatabase : AutoCloseable { + + /** The database connection for the throwaway database */ + 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 + */ + fun dbObjectExists(name: String): Boolean +} diff --git a/src/integration-test/kotlin/Types.kt b/src/integration-test/kotlin/Types.kt index 4421a20..c191561 100644 --- a/src/integration-test/kotlin/Types.kt +++ b/src/integration-test/kotlin/Types.kt @@ -3,6 +3,9 @@ package solutions.bitbadger.documents import kotlinx.serialization.Serializable import java.sql.Connection +/** 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) @@ -14,9 +17,9 @@ data class ArrayDocument(val id: String, val values: List) { companion object { /** A set of documents used for integration tests */ val testDocuments = listOf( - ArrayDocument("first", listOf("a", "b", "c" )), + ArrayDocument("first", listOf("a", "b", "c")), ArrayDocument("second", listOf("c", "d", "e")), - ArrayDocument("third", listOf("x", "y", "z"))) + ArrayDocument("third", listOf("x", "y", "z"))) } } @@ -34,10 +37,7 @@ data class JsonDocument(val id: String, val value: String, val numValue: Int, va JsonDocument("four", "purple", 17, SubDocument("green", "red")), JsonDocument("five", "purple", 18, null)) - fun load(conn: Connection, tableName: String = "test_table") = - testDocuments.forEach { conn.insert(tableName, it) } + fun load(db: ThrowawayDatabase, tableName: String = TEST_TABLE) = + testDocuments.forEach { db.conn.insert(tableName, it) } } } - -/** The test table name to use for integration tests */ -const val TEST_TABLE = "test_table" diff --git a/src/integration-test/kotlin/common/Custom.kt b/src/integration-test/kotlin/common/Custom.kt index faaed25..be28838 100644 --- a/src/integration-test/kotlin/common/Custom.kt +++ b/src/integration-test/kotlin/common/Custom.kt @@ -2,8 +2,8 @@ package solutions.bitbadger.documents.common import solutions.bitbadger.documents.* import solutions.bitbadger.documents.query.Count +import solutions.bitbadger.documents.query.Delete import solutions.bitbadger.documents.query.Find -import java.sql.Connection import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -13,67 +13,67 @@ import kotlin.test.assertNull */ object Custom { - fun listEmpty(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) - conn.customNonQuery("DELETE FROM $TEST_TABLE") - val result = conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) + fun listEmpty(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.deleteByFields(TEST_TABLE, listOf(Field.exists(Configuration.idField))) + val result = db.conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) assertEquals(0, result.size, "There should have been no results") } - fun listAll(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) - val result = conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) + fun listAll(db: ThrowawayDatabase) { + JsonDocument.load(db) + val result = db.conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) assertEquals(5, result.size, "There should have been 5 results") } - fun singleNone(conn: Connection) = + fun singleNone(db: ThrowawayDatabase) = assertNull( - conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), + db.conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), "There should not have been a document returned" ) - fun singleOne(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) + fun singleOne(db: ThrowawayDatabase) { + JsonDocument.load(db) assertNotNull( - conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), + db.conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), "There should not have been a document returned" ) } - fun nonQueryChanges(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) + fun nonQueryChanges(db: ThrowawayDatabase) { + JsonDocument.load(db) assertEquals( - 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been 5 documents in the table" ) - conn.customNonQuery("DELETE FROM $TEST_TABLE") + db.conn.customNonQuery("DELETE FROM $TEST_TABLE") assertEquals( - 0L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 0L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been no documents in the table" ) } - fun nonQueryNoChanges(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) + fun nonQueryNoChanges(db: ThrowawayDatabase) { + JsonDocument.load(db) assertEquals( - 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been 5 documents in the table" ) - conn.customNonQuery( - "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", + db.conn.customNonQuery( + Delete.byId(TEST_TABLE, "eighty-two"), listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) ) assertEquals( - 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should still have been 5 documents in the table" ) } - fun scalar(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) + fun scalar(db: ThrowawayDatabase) { + JsonDocument.load(db) assertEquals( 3L, - conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount), + db.conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount), "The number 3 should have been returned" ) } diff --git a/src/integration-test/kotlin/common/Definition.kt b/src/integration-test/kotlin/common/Definition.kt new file mode 100644 index 0000000..c8f7715 --- /dev/null +++ b/src/integration-test/kotlin/common/Definition.kt @@ -0,0 +1,28 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.TEST_TABLE +import solutions.bitbadger.documents.ThrowawayDatabase +import solutions.bitbadger.documents.ensureFieldIndex +import solutions.bitbadger.documents.ensureTable +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Definition` object / `ensure*` connection extension functions + */ +object Definition { + + 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") + } +} diff --git a/src/integration-test/kotlin/common/Document.kt b/src/integration-test/kotlin/common/Document.kt index 63a4ed6..88a0f50 100644 --- a/src/integration-test/kotlin/common/Document.kt +++ b/src/integration-test/kotlin/common/Document.kt @@ -1,7 +1,6 @@ package solutions.bitbadger.documents.common import solutions.bitbadger.documents.* -import java.sql.Connection import kotlin.test.assertEquals import kotlin.test.fail @@ -10,37 +9,37 @@ import kotlin.test.fail */ object Document { - fun insertDefault(conn: Connection) { - assertEquals(0L, conn.countAll(TEST_TABLE), "There should be no documents in the table") + 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")) - conn.insert(TEST_TABLE, doc) - val after = conn.findAll(TEST_TABLE) + 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(conn: Connection) { - conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) + fun insertDupe(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) try { - conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) + db.conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) fail("Inserting a document with a duplicate key should have thrown an exception") } catch (_: Exception) { // yay } } - fun insertNumAutoId(conn: Connection) { + fun insertNumAutoId(db: ThrowawayDatabase) { try { Configuration.autoIdStrategy = AutoId.NUMBER Configuration.idField = "key" - assertEquals(0L, conn.countAll(TEST_TABLE), "There should be no documents in the table") + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") - conn.insert(TEST_TABLE, NumIdDocument(0, "one")) - conn.insert(TEST_TABLE, NumIdDocument(0, "two")) - conn.insert(TEST_TABLE, NumIdDocument(77, "three")) - conn.insert(TEST_TABLE, NumIdDocument(0, "four")) + 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 = conn.findAll(TEST_TABLE, listOf(Field.named("key"))) + 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() }, @@ -52,6 +51,38 @@ object Document { } } - // TODO: UUID, Random String + 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.emptyDoc) + + 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.emptyDoc) + + Configuration.idStringLength = 21 + db.conn.insert(TEST_TABLE, JsonDocument.emptyDoc) + + 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 + } + } } diff --git a/src/integration-test/kotlin/postgresql/CustomIT.kt b/src/integration-test/kotlin/postgresql/CustomIT.kt index d356e57..8538446 100644 --- a/src/integration-test/kotlin/postgresql/CustomIT.kt +++ b/src/integration-test/kotlin/postgresql/CustomIT.kt @@ -14,35 +14,35 @@ class CustomIT { @Test @DisplayName("list succeeds with empty list") fun listEmpty() = - PgDB().use { Custom.listEmpty(it.conn) } + PgDB().use(Custom::listEmpty) @Test @DisplayName("list succeeds with a non-empty list") fun listAll() = - PgDB().use { Custom.listAll(it.conn) } + PgDB().use(Custom::listAll) @Test @DisplayName("single succeeds when document not found") fun singleNone() = - PgDB().use { Custom.singleNone(it.conn) } + PgDB().use(Custom::singleNone) @Test @DisplayName("single succeeds when a document is found") fun singleOne() = - PgDB().use { Custom.singleOne(it.conn) } + PgDB().use(Custom::singleOne) @Test @DisplayName("nonQuery makes changes") fun nonQueryChanges() = - PgDB().use { Custom.nonQueryChanges(it.conn) } + PgDB().use(Custom::nonQueryChanges) @Test @DisplayName("nonQuery makes no changes when where clause matches nothing") fun nonQueryNoChanges() = - PgDB().use { Custom.nonQueryNoChanges(it.conn) } + PgDB().use(Custom::nonQueryNoChanges) @Test @DisplayName("scalar succeeds") fun scalar() = - PgDB().use { Custom.scalar(it.conn) } + PgDB().use(Custom::scalar) } diff --git a/src/integration-test/kotlin/postgresql/DefinitionIT.kt b/src/integration-test/kotlin/postgresql/DefinitionIT.kt new file mode 100644 index 0000000..826fd28 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/DefinitionIT.kt @@ -0,0 +1,22 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Definition +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("PostgreSQL - Definition") +class DefinitionIT { + + @Test + @DisplayName("ensureTable creates table and index") + fun ensureTable() = + PgDB().use(Definition::ensureTable) + + @Test + @DisplayName("ensureFieldIndex creates an index") + fun ensureFieldIndex() = + PgDB().use(Definition::ensureFieldIndex) +} diff --git a/src/integration-test/kotlin/postgresql/DocumentIT.kt b/src/integration-test/kotlin/postgresql/DocumentIT.kt index 003b23d..ee318a7 100644 --- a/src/integration-test/kotlin/postgresql/DocumentIT.kt +++ b/src/integration-test/kotlin/postgresql/DocumentIT.kt @@ -13,15 +13,25 @@ class DocumentIT { @Test @DisplayName("insert works with default values") fun insertDefault() = - PgDB().use { Document.insertDefault(it.conn) } + PgDB().use(Document::insertDefault) @Test @DisplayName("insert fails with duplicate key") fun insertDupe() = - PgDB().use { Document.insertDupe(it.conn) } + PgDB().use(Document::insertDupe) @Test @DisplayName("insert succeeds with numeric auto IDs") fun insertNumAutoId() = - PgDB().use { Document.insertNumAutoId(it.conn) } + PgDB().use(Document::insertNumAutoId) + + @Test + @DisplayName("insert succeeds with UUID auto ID") + fun insertUUIDAutoId() = + PgDB().use(Document::insertUUIDAutoId) + + @Test + @DisplayName("insert succeeds with random string auto ID") + fun insertStringAutoId() = + PgDB().use(Document::insertStringAutoId) } diff --git a/src/integration-test/kotlin/postgresql/PgDB.kt b/src/integration-test/kotlin/postgresql/PgDB.kt index cfc4da3..d4cd76e 100644 --- a/src/integration-test/kotlin/postgresql/PgDB.kt +++ b/src/integration-test/kotlin/postgresql/PgDB.kt @@ -1,14 +1,11 @@ package solutions.bitbadger.documents.postgresql -import solutions.bitbadger.documents.AutoId -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.customNonQuery -import solutions.bitbadger.documents.ensureTable +import solutions.bitbadger.documents.* /** * A wrapper for a throwaway PostgreSQL database */ -class PgDB : AutoCloseable { +class PgDB : ThrowawayDatabase { private var dbName = "" @@ -21,10 +18,10 @@ class PgDB : AutoCloseable { Configuration.connectionString = connString(dbName) } - val conn = Configuration.dbConn() + override val conn = Configuration.dbConn() init { - conn.ensureTable(tableName) + conn.ensureTable(TEST_TABLE) } override fun close() { @@ -36,10 +33,11 @@ class PgDB : AutoCloseable { Configuration.connectionString = null } - companion object { + 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) - /** The table used for test documents */ - val tableName = "test_table" + companion object { /** * Create a connection string for the given database diff --git a/src/integration-test/kotlin/sqlite/CustomIT.kt b/src/integration-test/kotlin/sqlite/CustomIT.kt index 723376b..7b8dbd5 100644 --- a/src/integration-test/kotlin/sqlite/CustomIT.kt +++ b/src/integration-test/kotlin/sqlite/CustomIT.kt @@ -13,35 +13,35 @@ class CustomIT { @Test @DisplayName("list succeeds with empty list") fun listEmpty() = - SQLiteDB().use { Custom.listEmpty(it.conn) } + SQLiteDB().use(Custom::listEmpty) @Test @DisplayName("list succeeds with a non-empty list") fun listAll() = - SQLiteDB().use { Custom.listAll(it.conn) } + SQLiteDB().use(Custom::listAll) @Test @DisplayName("single succeeds when document not found") fun singleNone() = - SQLiteDB().use { Custom.singleNone(it.conn) } + SQLiteDB().use(Custom::singleNone) @Test @DisplayName("single succeeds when a document is found") fun singleOne() = - SQLiteDB().use { Custom.singleOne(it.conn) } + SQLiteDB().use(Custom::singleOne) @Test @DisplayName("nonQuery makes changes") fun nonQueryChanges() = - SQLiteDB().use { Custom.nonQueryChanges(it.conn) } + SQLiteDB().use(Custom::nonQueryChanges) @Test @DisplayName("nonQuery makes no changes when where clause matches nothing") fun nonQueryNoChanges() = - SQLiteDB().use { Custom.nonQueryNoChanges(it.conn) } + SQLiteDB().use(Custom::nonQueryNoChanges) @Test @DisplayName("scalar succeeds") fun scalar() = - SQLiteDB().use { Custom.scalar(it.conn) } + SQLiteDB().use(Custom::scalar) } diff --git a/src/integration-test/kotlin/sqlite/DefinitionIT.kt b/src/integration-test/kotlin/sqlite/DefinitionIT.kt index a952d5c..4bb14f7 100644 --- a/src/integration-test/kotlin/sqlite/DefinitionIT.kt +++ b/src/integration-test/kotlin/sqlite/DefinitionIT.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.sqlite import org.junit.jupiter.api.DisplayName import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Definition import java.sql.Connection import kotlin.test.Test import kotlin.test.assertFalse @@ -13,34 +14,13 @@ import kotlin.test.assertTrue @DisplayName("SQLite - Definition") class DefinitionIT { - /** - * Determine if a database item exists - * - * @param item The items whose existence should be checked - * @param conn The current database connection - * @return True if the item exists in the given database, false if not - */ - private fun itExists(item: String, conn: Connection) = - conn.customScalar("SELECT EXISTS (SELECT 1 FROM ${SQLiteDB.CATALOG} WHERE name = :name) AS it", - listOf(Parameter(":name", ParameterType.STRING, item)), Results::toExists) - @Test @DisplayName("ensureTable creates table and index") fun ensureTable() = - SQLiteDB().use { db -> - assertFalse(itExists("ensured", db.conn), "The 'ensured' table should not exist") - assertFalse(itExists("idx_ensured_key", db.conn), "The PK index for the 'ensured' table should not exist") - db.conn.ensureTable("ensured") - assertTrue(itExists("ensured", db.conn), "The 'ensured' table should exist") - assertTrue(itExists("idx_ensured_key", db.conn), "The PK index for the 'ensured' table should now exist") - } + SQLiteDB().use(Definition::ensureTable) @Test @DisplayName("ensureFieldIndex creates an index") fun ensureFieldIndex() = - SQLiteDB().use { db -> - assertFalse(itExists("idx_${TEST_TABLE}_test", db.conn), "The test index should not exist") - db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category")) - assertTrue(itExists("idx_${TEST_TABLE}_test", db.conn), "The test index should now exist") - } + SQLiteDB().use(Definition::ensureFieldIndex) } diff --git a/src/integration-test/kotlin/sqlite/DocumentIT.kt b/src/integration-test/kotlin/sqlite/DocumentIT.kt index e2a7d98..f8c00d2 100644 --- a/src/integration-test/kotlin/sqlite/DocumentIT.kt +++ b/src/integration-test/kotlin/sqlite/DocumentIT.kt @@ -13,16 +13,25 @@ class DocumentIT { @Test @DisplayName("insert works with default values") fun insertDefault() = - SQLiteDB().use { Document.insertDefault(it.conn) } + SQLiteDB().use(Document::insertDefault) @Test @DisplayName("insert fails with duplicate key") fun insertDupe() = - SQLiteDB().use { Document.insertDupe(it.conn) } + SQLiteDB().use(Document::insertDupe) @Test @DisplayName("insert succeeds with numeric auto IDs") fun insertNumAutoId() = - SQLiteDB().use { Document.insertNumAutoId(it.conn) } + SQLiteDB().use(Document::insertNumAutoId) + @Test + @DisplayName("insert succeeds with UUID auto ID") + fun insertUUIDAutoId() = + SQLiteDB().use(Document::insertUUIDAutoId) + + @Test + @DisplayName("insert succeeds with random string auto ID") + fun insertStringAutoId() = + SQLiteDB().use(Document::insertStringAutoId) } diff --git a/src/integration-test/kotlin/sqlite/SQLiteDB.kt b/src/integration-test/kotlin/sqlite/SQLiteDB.kt index 0351911..5528e45 100644 --- a/src/integration-test/kotlin/sqlite/SQLiteDB.kt +++ b/src/integration-test/kotlin/sqlite/SQLiteDB.kt @@ -1,15 +1,13 @@ package solutions.bitbadger.documents.sqlite -import solutions.bitbadger.documents.AutoId -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.TEST_TABLE -import solutions.bitbadger.documents.ensureTable +import solutions.bitbadger.documents.* import java.io.File +import java.sql.Connection /** * A wrapper for a throwaway SQLite database */ -class SQLiteDB : AutoCloseable { +class SQLiteDB : ThrowawayDatabase { private var dbName = "" @@ -18,7 +16,7 @@ class SQLiteDB : AutoCloseable { Configuration.connectionString = "jdbc:sqlite:$dbName" } - val conn = Configuration.dbConn() + override val conn = Configuration.dbConn() init { conn.ensureTable(TEST_TABLE) @@ -29,9 +27,7 @@ class SQLiteDB : AutoCloseable { File(dbName).delete() } - companion object { - - /** The catalog table for SQLite's schema */ - const val CATALOG = "sqlite_master" - } + 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/main/kotlin/Comparison.kt b/src/main/kotlin/Comparison.kt index c4bdd05..73ff2fe 100644 --- a/src/main/kotlin/Comparison.kt +++ b/src/main/kotlin/Comparison.kt @@ -21,4 +21,7 @@ class Comparison(val op: Op, val value: T) { } return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long } + + override fun toString() = + "$op $value" } diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 8c87011..733fe49 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -114,3 +114,24 @@ inline fun Connection.findAll(tableName: String) = */ inline fun Connection.findAll(tableName: String, orderBy: Collection>) = Find.all(tableName, orderBy, 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.byId(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.byField(tableName, fields, howMatched, this) diff --git a/src/main/kotlin/Delete.kt b/src/main/kotlin/Delete.kt new file mode 100644 index 0000000..3adfe5e --- /dev/null +++ b/src/main/kotlin/Delete.kt @@ -0,0 +1,55 @@ +package solutions.bitbadger.documents + +import solutions.bitbadger.documents.query.Delete +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) = + conn.customNonQuery( + Delete.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId))) + ) + + /** + * 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) = + 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 + */ + fun byField(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { + val named = Parameters.nameFields(fields) + conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named)) + } + + /** + * 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 byField(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Configuration.dbConn().use { byField(tableName, fields, howMatched, it) } +} diff --git a/src/main/kotlin/Field.kt b/src/main/kotlin/Field.kt index 924944d..e1796aa 100644 --- a/src/main/kotlin/Field.kt +++ b/src/main/kotlin/Field.kt @@ -92,6 +92,9 @@ class Field private constructor( } } + override fun toString() = + "Field ${parameterName ?: ""} $comparison${qualifier?.let { " (qualifier $it)"} ?: ""}" + companion object { /** diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 2f8466c..961cdcb 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -39,6 +39,30 @@ object Parameters { inline fun json(name: String, value: T) = Parameter(name, ParameterType.JSON, Configuration.json.encodeToString(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() + ): MutableCollection> { + existing.addAll( + fields + .filter { it.comparison.op != Op.EXISTS && it.comparison.op != Op.NOT_EXISTS } + .map { + Parameter( + it.parameterName!!, + if (it.comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING, + it.comparison.value + ) + }) + return existing + } + /** * Replace the parameter names in the query with question marks * @@ -47,7 +71,7 @@ object Parameters { * @return The query, with name parameters changed to `?`s */ fun replaceNamesInQuery(query: String, parameters: Collection>) = - parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") }.also(::println) + 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 -- 2.47.2 From a84c8289b186b83d1ba8dcc18c6548760265a57b Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 27 Feb 2025 23:38:38 -0500 Subject: [PATCH 23/88] WIP on count impl; reworking comparisons --- src/integration-test/kotlin/Types.kt | 1 - src/integration-test/kotlin/common/Count.kt | 69 +++++++++++++++ .../kotlin/common/Definition.kt | 23 ++++- .../kotlin/postgresql/CountIT.kt | 47 +++++++++++ .../kotlin/postgresql/DefinitionIT.kt | 10 +++ src/integration-test/kotlin/sqlite/CountIT.kt | 41 +++++++++ .../kotlin/sqlite/DefinitionIT.kt | 16 +++- .../kotlin/sqlite/SQLiteDB.kt | 1 - src/main/kotlin/Comparison.kt | 55 ++++++++---- src/main/kotlin/ConnectionExtensions.kt | 44 +++++++++- src/main/kotlin/Count.kt | 84 +++++++++++++++++++ src/main/kotlin/Definition.kt | 23 ++++- src/main/kotlin/Delete.kt | 6 +- src/main/kotlin/DocumentIndex.kt | 13 +++ src/main/kotlin/Field.kt | 65 +++++++++++--- src/main/kotlin/Parameters.kt | 20 +---- src/main/kotlin/query/Definition.kt | 21 ++++- src/test/kotlin/ComparisonTest.kt | 32 +++---- src/test/kotlin/DocumentIndexTest.kt | 24 ++++++ src/test/kotlin/query/DefinitionTest.kt | 30 +++++++ 20 files changed, 548 insertions(+), 77 deletions(-) create mode 100644 src/integration-test/kotlin/common/Count.kt create mode 100644 src/integration-test/kotlin/postgresql/CountIT.kt create mode 100644 src/integration-test/kotlin/sqlite/CountIT.kt create mode 100644 src/main/kotlin/DocumentIndex.kt create mode 100644 src/test/kotlin/DocumentIndexTest.kt diff --git a/src/integration-test/kotlin/Types.kt b/src/integration-test/kotlin/Types.kt index c191561..c698ad5 100644 --- a/src/integration-test/kotlin/Types.kt +++ b/src/integration-test/kotlin/Types.kt @@ -1,7 +1,6 @@ package solutions.bitbadger.documents import kotlinx.serialization.Serializable -import java.sql.Connection /** The test table name to use for integration tests */ const val TEST_TABLE = "test_table" diff --git a/src/integration-test/kotlin/common/Count.kt b/src/integration-test/kotlin/common/Count.kt new file mode 100644 index 0000000..adffb9f --- /dev/null +++ b/src/integration-test/kotlin/common/Count.kt @@ -0,0 +1,69 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.* +import kotlin.test.assertEquals + +/** + * Integration tests for the `Count` object + */ +object Count { + + 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("num_value", 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, "$.num_value ? (@ < 5)"), + "There should have been 2 matching documents" + ) + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + 0L, + db.conn.countByJsonPath(TEST_TABLE, "$.num_value ? (@ > 100)"), + "There should have been no matching documents" + ) + } +} diff --git a/src/integration-test/kotlin/common/Definition.kt b/src/integration-test/kotlin/common/Definition.kt index c8f7715..70e99b4 100644 --- a/src/integration-test/kotlin/common/Definition.kt +++ b/src/integration-test/kotlin/common/Definition.kt @@ -1,9 +1,6 @@ package solutions.bitbadger.documents.common -import solutions.bitbadger.documents.TEST_TABLE -import solutions.bitbadger.documents.ThrowawayDatabase -import solutions.bitbadger.documents.ensureFieldIndex -import solutions.bitbadger.documents.ensureTable +import solutions.bitbadger.documents.* import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -25,4 +22,22 @@ object Definition { 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/integration-test/kotlin/postgresql/CountIT.kt b/src/integration-test/kotlin/postgresql/CountIT.kt new file mode 100644 index 0000000..cf3c6d1 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/CountIT.kt @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Count +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("PostgreSQL - Count") +class CountIT { + + @Test + @DisplayName("all counts all documents") + fun all() = + PgDB().use(Count::all) + + @Test + @DisplayName("byFields counts documents by a numeric value") + fun byFieldsNumeric() = + PgDB().use(Count::byFieldsNumeric) + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + fun byFieldsAlpha() = + PgDB().use(Count::byFieldsAlpha) + + @Test + @DisplayName("byContains counts documents when matches are found") + fun byContainsMatch() = + PgDB().use(Count::byContainsMatch) + + @Test + @DisplayName("byContains counts documents when no matches are found") + fun byContainsNoMatch() = + PgDB().use(Count::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath counts documents when matches are found") + fun byJsonPathMatch() = + PgDB().use(Count::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath counts documents when no matches are found") + fun byJsonPathNoMatch() = + PgDB().use(Count::byJsonPathNoMatch) +} diff --git a/src/integration-test/kotlin/postgresql/DefinitionIT.kt b/src/integration-test/kotlin/postgresql/DefinitionIT.kt index 826fd28..2533fdc 100644 --- a/src/integration-test/kotlin/postgresql/DefinitionIT.kt +++ b/src/integration-test/kotlin/postgresql/DefinitionIT.kt @@ -19,4 +19,14 @@ class DefinitionIT { @DisplayName("ensureFieldIndex creates an index") fun ensureFieldIndex() = PgDB().use(Definition::ensureFieldIndex) + + @Test + @DisplayName("ensureDocumentIndex creates a full index") + fun ensureDocumentIndexFull() = + PgDB().use(Definition::ensureDocumentIndexFull) + + @Test + @DisplayName("ensureDocumentIndex creates an optimized index") + fun ensureDocumentIndexOptimized() = + PgDB().use(Definition::ensureDocumentIndexOptimized) } diff --git a/src/integration-test/kotlin/sqlite/CountIT.kt b/src/integration-test/kotlin/sqlite/CountIT.kt new file mode 100644 index 0000000..969b534 --- /dev/null +++ b/src/integration-test/kotlin/sqlite/CountIT.kt @@ -0,0 +1,41 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.Count +import kotlin.test.Test + +/** + * SQLite integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("SQLite - Count") +class CountIT { + + @Test + @DisplayName("all counts all documents") + fun all() = + SQLiteDB().use(Count::all) + + @Test + @DisplayName("byFields counts documents by a numeric value") + fun byFieldsNumeric() = + SQLiteDB().use(Count::byFieldsNumeric) + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + fun byFieldsAlpha() = + SQLiteDB().use(Count::byFieldsAlpha) + + @Test + @DisplayName("byContains fails") + fun byContainsMatch() { + assertThrows { SQLiteDB().use(Count::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathMatch() { + assertThrows { SQLiteDB().use(Count::byJsonPathMatch) } + } +} diff --git a/src/integration-test/kotlin/sqlite/DefinitionIT.kt b/src/integration-test/kotlin/sqlite/DefinitionIT.kt index 4bb14f7..8866751 100644 --- a/src/integration-test/kotlin/sqlite/DefinitionIT.kt +++ b/src/integration-test/kotlin/sqlite/DefinitionIT.kt @@ -1,12 +1,10 @@ package solutions.bitbadger.documents.sqlite import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Definition -import java.sql.Connection import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue /** * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions @@ -23,4 +21,16 @@ class DefinitionIT { @DisplayName("ensureFieldIndex creates an index") fun ensureFieldIndex() = SQLiteDB().use(Definition::ensureFieldIndex) + + @Test + @DisplayName("ensureDocumentIndex fails for full index") + fun ensureDocumentIndexFull() { + assertThrows { SQLiteDB().use(Definition::ensureDocumentIndexFull) } + } + + @Test + @DisplayName("ensureDocumentIndex fails for optimized index") + fun ensureDocumentIndexOptimized() { + assertThrows { SQLiteDB().use(Definition::ensureDocumentIndexOptimized) } + } } diff --git a/src/integration-test/kotlin/sqlite/SQLiteDB.kt b/src/integration-test/kotlin/sqlite/SQLiteDB.kt index 5528e45..1724e34 100644 --- a/src/integration-test/kotlin/sqlite/SQLiteDB.kt +++ b/src/integration-test/kotlin/sqlite/SQLiteDB.kt @@ -2,7 +2,6 @@ package solutions.bitbadger.documents.sqlite import solutions.bitbadger.documents.* import java.io.File -import java.sql.Connection /** * A wrapper for a throwaway SQLite database diff --git a/src/main/kotlin/Comparison.kt b/src/main/kotlin/Comparison.kt index 73ff2fe..ac0c0e0 100644 --- a/src/main/kotlin/Comparison.kt +++ b/src/main/kotlin/Comparison.kt @@ -1,27 +1,54 @@ package solutions.bitbadger.documents +interface Comparison { + + val op: Op + + val isNumeric: Boolean + + val value: T +} + /** - * A comparison against a field in a JSON document + * A single-value comparison against a field in a JSON document * * @property op The operation for the field comparison * @property value The value against which the comparison will be made */ -class Comparison(val op: Op, val value: T) { +class SingleComparison(override val op: Op, override val value: T) : Comparison { /** Is the value for this comparison a numeric value? */ - val isNumeric: Boolean - get() { - val toCheck = when (op) { - Op.IN -> { - val values = value as? Collection<*> - if (values.isNullOrEmpty()) "" else values.elementAt(0) - } - Op.BETWEEN -> (value as Pair<*, *>).first - else -> value - } - return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long - } + override val isNumeric: Boolean + get() = value.let { it is Byte || it is Short || it is Int || it is Long } override fun toString() = "$op $value" } + +/** + * A range comparison against a field in a JSON document + */ +class BetweenComparison(override val op: Op = Op.BETWEEN, override val value: Pair) : Comparison> { + + override val isNumeric: Boolean + get() = value.first.let { it is Byte || it is Short || it is Int || it is Long } +} + +/** + * A check within a collection of values + */ +class InComparison(override val op: Op = Op.IN, override val value: Collection) : Comparison> { + + override val isNumeric: Boolean + get() = !value.isEmpty() && value.elementAt(0).let { it is Byte || it is Short || it is Int || it is Long } +} + +/** + * A check within a collection of values + */ +class InArrayComparison(override val op: Op = Op.IN_ARRAY, override val value: Pair>) : Comparison>> { + + override val isNumeric: Boolean + get() = !value.second.isEmpty() && value.second.elementAt(0) + .let { it is Byte || it is Short || it is Int || it is Long } +} diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 733fe49..2d4edf3 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -72,6 +72,16 @@ fun Connection.ensureTable(tableName: String) = 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 ~~~ /** @@ -94,6 +104,38 @@ inline fun Connection.insert(tableName: String, document: TDoc) = 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: T) = + Count.byContains(tableName, criteria, this) + +/** + * Count documents using a JSON containment 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 RETRIEVAL QUERIES ~~~ @@ -134,4 +176,4 @@ fun Connection.byId(tableName: String, docId: TKey) = * @param howMatched How the fields should be matched */ fun Connection.deleteByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = - Delete.byField(tableName, fields, howMatched, this) + Delete.byFields(tableName, fields, howMatched, this) diff --git a/src/main/kotlin/Count.kt b/src/main/kotlin/Count.kt index c168a22..ddf3010 100644 --- a/src/main/kotlin/Count.kt +++ b/src/main/kotlin/Count.kt @@ -26,4 +26,88 @@ object Count { */ 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( + Count.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 deletion 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: T, conn: Connection) = + conn.customScalar(Count.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: T) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Count documents using a JSON containment 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 deletion 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( + Count.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path)), + 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 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/main/kotlin/Definition.kt b/src/main/kotlin/Definition.kt index 9ad8da0..d61712c 100644 --- a/src/main/kotlin/Definition.kt +++ b/src/main/kotlin/Definition.kt @@ -41,11 +41,32 @@ object Definition { /** * 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) = 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 + */ + fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) = + conn.customNonQuery(Definition.ensureDocumentIndexOn(tableName, indexType)) + + /** + * 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) = + Configuration.dbConn().use { ensureDocumentIndex(tableName, indexType, it) } } diff --git a/src/main/kotlin/Delete.kt b/src/main/kotlin/Delete.kt index 3adfe5e..d9002b0 100644 --- a/src/main/kotlin/Delete.kt +++ b/src/main/kotlin/Delete.kt @@ -38,7 +38,7 @@ object Delete { * @param howMatched How the fields should be matched * @param conn The connection on which the deletion should be executed */ - fun byField(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { val named = Parameters.nameFields(fields) conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named)) } @@ -50,6 +50,6 @@ object Delete { * @param fields The fields which should be compared * @param howMatched How the fields should be matched */ - fun byField(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = - Configuration.dbConn().use { byField(tableName, fields, howMatched, it) } + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } } diff --git a/src/main/kotlin/DocumentIndex.kt b/src/main/kotlin/DocumentIndex.kt new file mode 100644 index 0000000..3ce8743 --- /dev/null +++ b/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/main/kotlin/Field.kt b/src/main/kotlin/Field.kt index e1796aa..680609a 100644 --- a/src/main/kotlin/Field.kt +++ b/src/main/kotlin/Field.kt @@ -92,6 +92,47 @@ class Field private constructor( } } + /** + * 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 BetweenComparison<*> -> { + existing.add(Parameter("${parameterName}min", typ, comparison.value.first)) + existing.add(Parameter("${parameterName}max", typ, comparison.value.second)) + } + + is InComparison<*> -> { + comparison.value.forEachIndexed { index, item -> + existing.add(Parameter("${parameterName}_$index", typ, item)) + } + } + + is InArrayComparison<*> -> { + val mkString = Configuration.dialect("append parameters for InArray") == Dialect.POSTGRESQL + // TODO: I think this is actually Pair> + 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)"} ?: ""}" @@ -106,7 +147,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun equal(name: String, value: T, paramName: String? = null) = - Field(name, Comparison(Op.EQUAL, value), paramName) + Field(name, SingleComparison(Op.EQUAL, value), paramName) /** * Create a field greater-than comparison @@ -117,7 +158,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun greater(name: String, value: T, paramName: String? = null) = - Field(name, Comparison(Op.GREATER, value), paramName) + Field(name, SingleComparison(Op.GREATER, value), paramName) /** * Create a field greater-than-or-equal-to comparison @@ -128,7 +169,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun greaterOrEqual(name: String, value: T, paramName: String? = null) = - Field(name, Comparison(Op.GREATER_OR_EQUAL, value), paramName) + Field(name, SingleComparison(Op.GREATER_OR_EQUAL, value), paramName) /** * Create a field less-than comparison @@ -139,7 +180,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun less(name: String, value: T, paramName: String? = null) = - Field(name, Comparison(Op.LESS, value), paramName) + Field(name, SingleComparison(Op.LESS, value), paramName) /** * Create a field less-than-or-equal-to comparison @@ -150,7 +191,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun lessOrEqual(name: String, value: T, paramName: String? = null) = - Field(name, Comparison(Op.LESS_OR_EQUAL, value), paramName) + Field(name, SingleComparison(Op.LESS_OR_EQUAL, value), paramName) /** * Create a field inequality comparison @@ -161,7 +202,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun notEqual(name: String, value: T, paramName: String? = null) = - Field(name, Comparison(Op.NOT_EQUAL, value), paramName) + Field(name, SingleComparison(Op.NOT_EQUAL, value), paramName) /** * Create a field range comparison @@ -173,7 +214,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun between(name: String, minValue: T, maxValue: T, paramName: String? = null) = - Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue)), paramName) + Field(name, BetweenComparison(value = Pair(minValue, maxValue)), paramName) /** * Create a field where any values match (SQL `IN`) @@ -184,7 +225,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun any(name: String, values: Collection, paramName: String? = null) = - Field(name, Comparison(Op.IN, values), paramName) + Field(name, InComparison(value = values), paramName) /** * Create a field where values should exist in a document's array @@ -196,16 +237,16 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun inArray(name: String, tableName: String, values: Collection, paramName: String? = null) = - Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values)), paramName) + Field(name, InArrayComparison(value = Pair(tableName, values)), paramName) fun exists(name: String) = - Field(name, Comparison(Op.EXISTS, "")) + Field(name, SingleComparison(Op.EXISTS, "")) fun notExists(name: String) = - Field(name, Comparison(Op.NOT_EXISTS, "")) + Field(name, SingleComparison(Op.NOT_EXISTS, "")) fun named(name: String) = - Field(name, Comparison(Op.EQUAL, "")) + Field(name, SingleComparison(Op.EQUAL, "")) fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String { val path = StringBuilder("data") diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 961cdcb..c428795 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -46,22 +46,8 @@ object Parameters { * @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() - ): MutableCollection> { - existing.addAll( - fields - .filter { it.comparison.op != Op.EXISTS && it.comparison.op != Op.NOT_EXISTS } - .map { - Parameter( - it.parameterName!!, - if (it.comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING, - it.comparison.value - ) - }) - return existing - } + 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 @@ -72,7 +58,7 @@ object Parameters { */ fun replaceNamesInQuery(query: String, parameters: Collection>) = parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") } - + .also(::println) /** * Apply the given parameters to the given query, returning a prepared statement * diff --git a/src/main/kotlin/query/Definition.kt b/src/main/kotlin/query/Definition.kt index 8180383..e3c2ccd 100644 --- a/src/main/kotlin/query/Definition.kt +++ b/src/main/kotlin/query/Definition.kt @@ -1,9 +1,6 @@ package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldFormat +import solutions.bitbadger.documents.* /** * Functions to create queries to define tables and indexes @@ -76,4 +73,20 @@ object Definition { */ 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 + */ + 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/test/kotlin/ComparisonTest.kt b/src/test/kotlin/ComparisonTest.kt index daa0545..fb558df 100644 --- a/src/test/kotlin/ComparisonTest.kt +++ b/src/test/kotlin/ComparisonTest.kt @@ -14,82 +14,82 @@ class ComparisonTest { @Test @DisplayName("isNumeric is false for empty list of values") fun isNumericFalseForEmptyList() = - assertFalse(Comparison(Op.IN, listOf()).isNumeric, "An IN with empty list should not be numeric") + assertFalse(InComparison(Op.IN, listOf()).isNumeric, "An IN with empty list should not be numeric") @Test @DisplayName("isNumeric is false for IN with strings") fun isNumericFalseForStringsAndIn() = - assertFalse(Comparison(Op.IN, listOf("a", "b", "c")).isNumeric, "An IN with strings should not be numeric") + assertFalse(InComparison(Op.IN, listOf("a", "b", "c")).isNumeric, "An IN with strings should not be numeric") @Test @DisplayName("isNumeric is true for IN with bytes") fun isNumericTrueForByteAndIn() = - assertTrue(Comparison(Op.IN, listOf(4, 8)).isNumeric, "An IN with bytes should be numeric") + assertTrue(InComparison(Op.IN, listOf(4, 8)).isNumeric, "An IN with bytes should be numeric") @Test @DisplayName("isNumeric is true for IN with shorts") fun isNumericTrueForShortAndIn() = - assertTrue(Comparison(Op.IN, listOf(18, 22)).isNumeric, "An IN with shorts should be numeric") + assertTrue(InComparison(Op.IN, listOf(18, 22)).isNumeric, "An IN with shorts should be numeric") @Test @DisplayName("isNumeric is true for IN with ints") fun isNumericTrueForIntAndIn() = - assertTrue(Comparison(Op.IN, listOf(7, 8, 9)).isNumeric, "An IN with ints should be numeric") + assertTrue(InComparison(Op.IN, listOf(7, 8, 9)).isNumeric, "An IN with ints should be numeric") @Test @DisplayName("isNumeric is true for IN with longs") fun isNumericTrueForLongAndIn() = - assertTrue(Comparison(Op.IN, listOf(3L)).isNumeric, "An IN with longs should be numeric") + assertTrue(InComparison(Op.IN, listOf(3L)).isNumeric, "An IN with longs should be numeric") @Test @DisplayName("isNumeric is false for BETWEEN with strings") fun isNumericFalseForStringsAndBetween() = - assertFalse(Comparison(Op.BETWEEN, Pair("eh", "zed")).isNumeric, + assertFalse(BetweenComparison(Op.BETWEEN, Pair("eh", "zed")).isNumeric, "A BETWEEN with strings should not be numeric") @Test @DisplayName("isNumeric is true for BETWEEN with bytes") fun isNumericTrueForByteAndBetween() = - assertTrue(Comparison(Op.BETWEEN, Pair(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric") + assertTrue(BetweenComparison(Op.BETWEEN, Pair(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric") @Test @DisplayName("isNumeric is true for BETWEEN with shorts") fun isNumericTrueForShortAndBetween() = - assertTrue(Comparison(Op.BETWEEN, Pair(0, 9)).isNumeric, + assertTrue(BetweenComparison(Op.BETWEEN, Pair(0, 9)).isNumeric, "A BETWEEN with shorts should be numeric") @Test @DisplayName("isNumeric is true for BETWEEN with ints") fun isNumericTrueForIntAndBetween() = - assertTrue(Comparison(Op.BETWEEN, Pair(15, 44)).isNumeric, "A BETWEEN with ints should be numeric") + assertTrue(BetweenComparison(Op.BETWEEN, Pair(15, 44)).isNumeric, "A BETWEEN with ints should be numeric") @Test @DisplayName("isNumeric is true for BETWEEN with longs") fun isNumericTrueForLongAndBetween() = - assertTrue(Comparison(Op.BETWEEN, Pair(9L, 12L)).isNumeric, "A BETWEEN with longs should be numeric") + assertTrue(BetweenComparison(Op.BETWEEN, Pair(9L, 12L)).isNumeric, "A BETWEEN with longs should be numeric") @Test @DisplayName("isNumeric is false for string value") fun isNumericFalseForString() = - assertFalse(Comparison(Op.EQUAL, "80").isNumeric, "A string should not be numeric") + assertFalse(SingleComparison(Op.EQUAL, "80").isNumeric, "A string should not be numeric") @Test @DisplayName("isNumeric is true for byte value") fun isNumericTrueForByte() = - assertTrue(Comparison(Op.EQUAL, 47.toByte()).isNumeric, "A byte should be numeric") + assertTrue(SingleComparison(Op.EQUAL, 47.toByte()).isNumeric, "A byte should be numeric") @Test @DisplayName("isNumeric is true for short value") fun isNumericTrueForShort() = - assertTrue(Comparison(Op.EQUAL, 2.toShort()).isNumeric, "A short should be numeric") + assertTrue(SingleComparison(Op.EQUAL, 2.toShort()).isNumeric, "A short should be numeric") @Test @DisplayName("isNumeric is true for int value") fun isNumericTrueForInt() = - assertTrue(Comparison(Op.EQUAL, 555).isNumeric, "An int should be numeric") + assertTrue(SingleComparison(Op.EQUAL, 555).isNumeric, "An int should be numeric") @Test @DisplayName("isNumeric is true for long value") fun isNumericTrueForLong() = - assertTrue(Comparison(Op.EQUAL, 82L).isNumeric, "A long should be numeric") + assertTrue(SingleComparison(Op.EQUAL, 82L).isNumeric, "A long should be numeric") } diff --git a/src/test/kotlin/DocumentIndexTest.kt b/src/test/kotlin/DocumentIndexTest.kt new file mode 100644 index 0000000..0947752 --- /dev/null +++ b/src/test/kotlin/DocumentIndexTest.kt @@ -0,0 +1,24 @@ +package solutions.bitbadger.documents + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +/** + * Unit tests for the `DocumentIndex` enum + */ +@DisplayName("Op") +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/test/kotlin/query/DefinitionTest.kt b/src/test/kotlin/query/DefinitionTest.kt index 018fb1d..bcad33a 100644 --- a/src/test/kotlin/query/DefinitionTest.kt +++ b/src/test/kotlin/query/DefinitionTest.kt @@ -7,6 +7,7 @@ import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentIndex import kotlin.test.assertEquals /** @@ -101,4 +102,33 @@ class DefinitionTest { Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE), "CREATE INDEX for nested SQLite field incorrect" ) + + @Test + @DisplayName("ensureDocumentIndexOn generates Full for PostgreSQL") + fun ensureDocumentIndexOnFullPostgres() { + Configuration.dialectValue = Dialect.POSTGRESQL + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tbl USING GIN (data)", + Definition.ensureDocumentIndexOn(tbl, DocumentIndex.FULL), + "CREATE INDEX for full document index incorrect" + ) + } + + @Test + @DisplayName("ensureDocumentIndexOn generates Optimized for PostgreSQL") + fun ensureDocumentIndexOnOptimizedPostgres() { + Configuration.dialectValue = Dialect.POSTGRESQL + assertEquals( + "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tbl USING GIN (data jsonb_path_ops)", + Definition.ensureDocumentIndexOn(tbl, DocumentIndex.OPTIMIZED), + "CREATE INDEX for optimized document index incorrect" + ) + } + + @Test + @DisplayName("ensureDocumentIndexOn fails for SQLite") + fun ensureDocumentIndexOnFailsSQLite() { + Configuration.dialectValue = Dialect.SQLITE + assertThrows { Definition.ensureDocumentIndexOn(tbl, DocumentIndex.FULL) } + } } -- 2.47.2 From e14fd23eadb5765d3694593d9d2e2dff0f67b018 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 28 Feb 2025 18:10:47 -0500 Subject: [PATCH 24/88] Fix count tests; use types for varying comparison values --- src/integration-test/kotlin/common/Count.kt | 6 +- src/main/kotlin/Comparison.kt | 60 ++++---- src/main/kotlin/Field.kt | 30 ++-- src/main/kotlin/Parameter.kt | 37 +++++ src/main/kotlin/Parameters.kt | 35 +---- src/test/kotlin/ComparisonTest.kt | 143 +++++++++++++++----- 6 files changed, 204 insertions(+), 107 deletions(-) diff --git a/src/integration-test/kotlin/common/Count.kt b/src/integration-test/kotlin/common/Count.kt index adffb9f..878d079 100644 --- a/src/integration-test/kotlin/common/Count.kt +++ b/src/integration-test/kotlin/common/Count.kt @@ -17,7 +17,7 @@ object Count { JsonDocument.load(db) assertEquals( 3L, - db.conn.countByFields(TEST_TABLE, listOf(Field.between("num_value", 10, 20))), + db.conn.countByFields(TEST_TABLE, listOf(Field.between("numValue", 10, 20))), "There should have been 3 matching documents" ) } @@ -53,7 +53,7 @@ object Count { JsonDocument.load(db) assertEquals( 2L, - db.conn.countByJsonPath(TEST_TABLE, "$.num_value ? (@ < 5)"), + db.conn.countByJsonPath(TEST_TABLE, "$.numValue ? (@ < 5)"), "There should have been 2 matching documents" ) } @@ -62,7 +62,7 @@ object Count { JsonDocument.load(db) assertEquals( 0L, - db.conn.countByJsonPath(TEST_TABLE, "$.num_value ? (@ > 100)"), + db.conn.countByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), "There should have been no matching documents" ) } diff --git a/src/main/kotlin/Comparison.kt b/src/main/kotlin/Comparison.kt index ac0c0e0..f187882 100644 --- a/src/main/kotlin/Comparison.kt +++ b/src/main/kotlin/Comparison.kt @@ -1,25 +1,43 @@ package solutions.bitbadger.documents +/** + * Information required to generate a JSON field comparison + */ interface Comparison { + /** The operation for the field comparison */ val op: Op - val isNumeric: Boolean - + /** The value against which the comparison will be made */ val value: T + + /** Whether the value should be considered numeric */ + val isNumeric: Boolean } /** - * A single-value comparison against a field in a JSON document + * Function to determine if a value is numeric * - * @property op The operation for the field comparison - * @property value The value against which the comparison will be made + * @param it The value in question + * @return True if it is a numeric type, false if not */ -class SingleComparison(override val op: Op, override val value: T) : Comparison { +private fun isNumeric(it: T) = + it is Byte || it is Short || it is Int || it is Long - /** Is the value for this comparison a numeric value? */ - override val isNumeric: Boolean - get() = value.let { 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" @@ -28,27 +46,23 @@ class SingleComparison(override val op: Op, override val value: T) : Comparis /** * A range comparison against a field in a JSON document */ -class BetweenComparison(override val op: Op = Op.BETWEEN, override val value: Pair) : Comparison> { - - override val isNumeric: Boolean - get() = value.first.let { it is Byte || it is Short || it is Int || it is Long } +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 InComparison(override val op: Op = Op.IN, override val value: Collection) : Comparison> { - - override val isNumeric: Boolean - get() = !value.isEmpty() && value.elementAt(0).let { it is Byte || it is Short || it is Int || it is Long } +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 + * A check within a collection of values against an array in a document */ -class InArrayComparison(override val op: Op = Op.IN_ARRAY, override val value: Pair>) : Comparison>> { - - override val isNumeric: Boolean - get() = !value.second.isEmpty() && value.second.elementAt(0) - .let { it is Byte || it is Short || it is Int || it is Long } +class ComparisonInArray(override val value: Pair>) : Comparison>> { + override val op = Op.IN_ARRAY + override val isNumeric = false } diff --git a/src/main/kotlin/Field.kt b/src/main/kotlin/Field.kt index 680609a..4a3d8e8 100644 --- a/src/main/kotlin/Field.kt +++ b/src/main/kotlin/Field.kt @@ -101,18 +101,18 @@ class Field private constructor( fun appendParameter(existing: MutableCollection>): MutableCollection> { val typ = if (comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING when (comparison) { - is BetweenComparison<*> -> { + is ComparisonBetween<*> -> { existing.add(Parameter("${parameterName}min", typ, comparison.value.first)) existing.add(Parameter("${parameterName}max", typ, comparison.value.second)) } - is InComparison<*> -> { + is ComparisonIn<*> -> { comparison.value.forEachIndexed { index, item -> existing.add(Parameter("${parameterName}_$index", typ, item)) } } - is InArrayComparison<*> -> { + is ComparisonInArray<*> -> { val mkString = Configuration.dialect("append parameters for InArray") == Dialect.POSTGRESQL // TODO: I think this is actually Pair> comparison.value.second.forEachIndexed { index, item -> @@ -147,7 +147,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun equal(name: String, value: T, paramName: String? = null) = - Field(name, SingleComparison(Op.EQUAL, value), paramName) + Field(name, ComparisonSingle(Op.EQUAL, value), paramName) /** * Create a field greater-than comparison @@ -158,7 +158,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun greater(name: String, value: T, paramName: String? = null) = - Field(name, SingleComparison(Op.GREATER, value), paramName) + Field(name, ComparisonSingle(Op.GREATER, value), paramName) /** * Create a field greater-than-or-equal-to comparison @@ -169,7 +169,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun greaterOrEqual(name: String, value: T, paramName: String? = null) = - Field(name, SingleComparison(Op.GREATER_OR_EQUAL, value), paramName) + Field(name, ComparisonSingle(Op.GREATER_OR_EQUAL, value), paramName) /** * Create a field less-than comparison @@ -180,7 +180,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun less(name: String, value: T, paramName: String? = null) = - Field(name, SingleComparison(Op.LESS, value), paramName) + Field(name, ComparisonSingle(Op.LESS, value), paramName) /** * Create a field less-than-or-equal-to comparison @@ -191,7 +191,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun lessOrEqual(name: String, value: T, paramName: String? = null) = - Field(name, SingleComparison(Op.LESS_OR_EQUAL, value), paramName) + Field(name, ComparisonSingle(Op.LESS_OR_EQUAL, value), paramName) /** * Create a field inequality comparison @@ -202,7 +202,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun notEqual(name: String, value: T, paramName: String? = null) = - Field(name, SingleComparison(Op.NOT_EQUAL, value), paramName) + Field(name, ComparisonSingle(Op.NOT_EQUAL, value), paramName) /** * Create a field range comparison @@ -214,7 +214,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun between(name: String, minValue: T, maxValue: T, paramName: String? = null) = - Field(name, BetweenComparison(value = Pair(minValue, maxValue)), paramName) + Field(name, ComparisonBetween(Pair(minValue, maxValue)), paramName) /** * Create a field where any values match (SQL `IN`) @@ -225,7 +225,7 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun any(name: String, values: Collection, paramName: String? = null) = - Field(name, InComparison(value = values), paramName) + Field(name, ComparisonIn(values), paramName) /** * Create a field where values should exist in a document's array @@ -237,16 +237,16 @@ class Field private constructor( * @return A `Field` with the given comparison */ fun inArray(name: String, tableName: String, values: Collection, paramName: String? = null) = - Field(name, InArrayComparison(value = Pair(tableName, values)), paramName) + Field(name, ComparisonInArray(Pair(tableName, values)), paramName) fun exists(name: String) = - Field(name, SingleComparison(Op.EXISTS, "")) + Field(name, ComparisonSingle(Op.EXISTS, "")) fun notExists(name: String) = - Field(name, SingleComparison(Op.NOT_EXISTS, "")) + Field(name, ComparisonSingle(Op.NOT_EXISTS, "")) fun named(name: String) = - Field(name, SingleComparison(Op.EQUAL, "")) + Field(name, ComparisonSingle(Op.EQUAL, "")) fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String { val path = StringBuilder("data") diff --git a/src/main/kotlin/Parameter.kt b/src/main/kotlin/Parameter.kt index 0503812..1bd707b 100644 --- a/src/main/kotlin/Parameter.kt +++ b/src/main/kotlin/Parameter.kt @@ -1,5 +1,8 @@ package solutions.bitbadger.documents +import java.sql.PreparedStatement +import java.sql.Types + /** * A parameter to use for a query * @@ -8,11 +11,45 @@ package solutions.bitbadger.documents * @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 + */ + 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/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index c428795..11ff18b 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -3,7 +3,6 @@ package solutions.bitbadger.documents import java.sql.Connection import java.sql.PreparedStatement import java.sql.SQLException -import java.sql.Types /** * Functions to assist with the creation and implementation of parameters for SQL queries @@ -58,7 +57,7 @@ object Parameters { */ fun replaceNamesInQuery(query: String, parameters: Collection>) = parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") } - .also(::println) + /** * Apply the given parameters to the given query, returning a prepared statement * @@ -69,6 +68,7 @@ object Parameters { * @throws DocumentException If parameter names are invalid or number value types are invalid */ fun apply(conn: Connection, query: String, parameters: Collection>): PreparedStatement { + if (parameters.isEmpty()) return try { conn.prepareStatement(query) } catch (ex: SQLException) { @@ -88,34 +88,9 @@ object Parameters { replaceNamesInQuery(query, parameters) .let { conn.prepareStatement(it) } .also { stmt -> - replacements.sortedBy { it.first }.map { it.second }.forEachIndexed { index, param -> - val idx = index + 1 - when (param.type) { - ParameterType.NUMBER -> { - when (param.value) { - null -> stmt.setNull(idx, Types.NULL) - is Byte -> stmt.setByte(idx, param.value) - is Short -> stmt.setShort(idx, param.value) - is Int -> stmt.setInt(idx, param.value) - is Long -> stmt.setLong(idx, param.value) - else -> throw DocumentException( - "Number parameter must be Byte, Short, Int, or Long " + - "(${param.value::class.simpleName})" - ) - } - } - - ParameterType.STRING -> { - when (param.value) { - null -> stmt.setNull(idx, Types.NULL) - is String -> stmt.setString(idx, param.value) - else -> stmt.setString(idx, param.value.toString()) - } - } - - ParameterType.JSON -> stmt.setObject(idx, param.value as String, Types.OTHER) - } - } + 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) diff --git a/src/test/kotlin/ComparisonTest.kt b/src/test/kotlin/ComparisonTest.kt index fb558df..f92fb68 100644 --- a/src/test/kotlin/ComparisonTest.kt +++ b/src/test/kotlin/ComparisonTest.kt @@ -2,94 +2,165 @@ package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue /** - * Unit tests for the `Comparison` class + * Unit tests for the `ComparisonBetween` class */ -@DisplayName("Comparison") -class ComparisonTest { +@DisplayName("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("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(InComparison(Op.IN, listOf()).isNumeric, "An IN with empty list should not be numeric") + assertFalse(ComparisonIn(listOf()).isNumeric, "An IN with empty list should not be numeric") @Test - @DisplayName("isNumeric is false for IN with strings") + @DisplayName("isNumeric is false with strings") fun isNumericFalseForStringsAndIn() = - assertFalse(InComparison(Op.IN, listOf("a", "b", "c")).isNumeric, "An IN with strings should not be numeric") + assertFalse(ComparisonIn(listOf("a", "b", "c")).isNumeric, "An IN with strings should not be numeric") @Test - @DisplayName("isNumeric is true for IN with bytes") + @DisplayName("isNumeric is true with bytes") fun isNumericTrueForByteAndIn() = - assertTrue(InComparison(Op.IN, listOf(4, 8)).isNumeric, "An IN with bytes should be numeric") + assertTrue(ComparisonIn(listOf(4, 8)).isNumeric, "An IN with bytes should be numeric") @Test - @DisplayName("isNumeric is true for IN with shorts") + @DisplayName("isNumeric is true with shorts") fun isNumericTrueForShortAndIn() = - assertTrue(InComparison(Op.IN, listOf(18, 22)).isNumeric, "An IN with shorts should be numeric") + assertTrue(ComparisonIn(listOf(18, 22)).isNumeric, "An IN with shorts should be numeric") @Test - @DisplayName("isNumeric is true for IN with ints") + @DisplayName("isNumeric is true with ints") fun isNumericTrueForIntAndIn() = - assertTrue(InComparison(Op.IN, listOf(7, 8, 9)).isNumeric, "An IN with ints should be numeric") + assertTrue(ComparisonIn(listOf(7, 8, 9)).isNumeric, "An IN with ints should be numeric") @Test - @DisplayName("isNumeric is true for IN with longs") + @DisplayName("isNumeric is true with longs") fun isNumericTrueForLongAndIn() = - assertTrue(InComparison(Op.IN, listOf(3L)).isNumeric, "An IN with longs should be numeric") + assertTrue(ComparisonIn(listOf(3L)).isNumeric, "An IN with longs should be numeric") +} + +/** + * Unit tests for the `ComparisonInArray` class + */ +@DisplayName("ComparisonInArray") +class ComparisonInArrayTest { @Test - @DisplayName("isNumeric is false for BETWEEN with strings") - fun isNumericFalseForStringsAndBetween() = - assertFalse(BetweenComparison(Op.BETWEEN, Pair("eh", "zed")).isNumeric, - "A BETWEEN with strings should not be numeric") + @DisplayName("op is set to IN_ARRAY") + fun op() = + assertEquals( + Op.IN_ARRAY, + ComparisonInArray(Pair(TEST_TABLE, listOf())).op, + "InArray comparison should have IN_ARRAY op" + ) @Test - @DisplayName("isNumeric is true for BETWEEN with bytes") - fun isNumericTrueForByteAndBetween() = - assertTrue(BetweenComparison(Op.BETWEEN, Pair(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric") + @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 true for BETWEEN with shorts") - fun isNumericTrueForShortAndBetween() = - assertTrue(BetweenComparison(Op.BETWEEN, Pair(0, 9)).isNumeric, - "A BETWEEN with shorts should be numeric") + @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 true for BETWEEN with ints") - fun isNumericTrueForIntAndBetween() = - assertTrue(BetweenComparison(Op.BETWEEN, Pair(15, 44)).isNumeric, "A BETWEEN with ints should be numeric") + @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 true for BETWEEN with longs") - fun isNumericTrueForLongAndBetween() = - assertTrue(BetweenComparison(Op.BETWEEN, Pair(9L, 12L)).isNumeric, "A BETWEEN with longs should be numeric") + @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("ComparisonSingle") +class ComparisonSingleTest { @Test @DisplayName("isNumeric is false for string value") fun isNumericFalseForString() = - assertFalse(SingleComparison(Op.EQUAL, "80").isNumeric, "A string should not be numeric") + assertFalse(ComparisonSingle(Op.EQUAL, "80").isNumeric, "A string should not be numeric") @Test @DisplayName("isNumeric is true for byte value") fun isNumericTrueForByte() = - assertTrue(SingleComparison(Op.EQUAL, 47.toByte()).isNumeric, "A byte should be numeric") + assertTrue(ComparisonSingle(Op.EQUAL, 47.toByte()).isNumeric, "A byte should be numeric") @Test @DisplayName("isNumeric is true for short value") fun isNumericTrueForShort() = - assertTrue(SingleComparison(Op.EQUAL, 2.toShort()).isNumeric, "A short should be numeric") + assertTrue(ComparisonSingle(Op.EQUAL, 2.toShort()).isNumeric, "A short should be numeric") @Test @DisplayName("isNumeric is true for int value") fun isNumericTrueForInt() = - assertTrue(SingleComparison(Op.EQUAL, 555).isNumeric, "An int should be numeric") + assertTrue(ComparisonSingle(Op.EQUAL, 555).isNumeric, "An int should be numeric") @Test @DisplayName("isNumeric is true for long value") fun isNumericTrueForLong() = - assertTrue(SingleComparison(Op.EQUAL, 82L).isNumeric, "A long should be numeric") + assertTrue(ComparisonSingle(Op.EQUAL, 82L).isNumeric, "A long should be numeric") } -- 2.47.2 From bc3b4bb01297c6513c5870625e8730aa0d0dbc8f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 1 Mar 2025 10:52:19 -0500 Subject: [PATCH 25/88] Add delete functions --- src/integration-test/kotlin/common/Delete.kt | 66 +++++++++++++++++++ .../kotlin/postgresql/DeleteIT.kt | 49 ++++++++++++++ .../kotlin/sqlite/DeleteIT.kt | 43 ++++++++++++ src/main/kotlin/ConnectionExtensions.kt | 22 ++++++- src/main/kotlin/Count.kt | 4 +- src/main/kotlin/Delete.kt | 44 ++++++++++++- 6 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 src/integration-test/kotlin/common/Delete.kt create mode 100644 src/integration-test/kotlin/postgresql/DeleteIT.kt create mode 100644 src/integration-test/kotlin/sqlite/DeleteIT.kt diff --git a/src/integration-test/kotlin/common/Delete.kt b/src/integration-test/kotlin/common/Delete.kt new file mode 100644 index 0000000..09b9f26 --- /dev/null +++ b/src/integration-test/kotlin/common/Delete.kt @@ -0,0 +1,66 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.* +import kotlin.test.assertEquals + +/** + * Integration tests for the `Delete` object + */ +object Delete { + + 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/integration-test/kotlin/postgresql/DeleteIT.kt b/src/integration-test/kotlin/postgresql/DeleteIT.kt new file mode 100644 index 0000000..6504711 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/DeleteIT.kt @@ -0,0 +1,49 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Delete +import kotlin.test.Test + +@DisplayName("PostgreSQL - Delete") +class DeleteIT { + + @Test + @DisplayName("byId deletes a matching ID") + fun byIdMatch() = + PgDB().use(Delete::byIdMatch) + + @Test + @DisplayName("byId succeeds when no ID matches") + fun byIdNoMatch() = + PgDB().use(Delete::byIdNoMatch) + + @Test + @DisplayName("byFields deletes matching documents") + fun byFieldsMatch() = + PgDB().use(Delete::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(Delete::byFieldsNoMatch) + + @Test + @DisplayName("byContains deletes matching documents") + fun byContainsMatch() = + PgDB().use(Delete::byContainsMatch) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(Delete::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath deletes matching documents") + fun byJsonPathMatch() = + PgDB().use(Delete::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(Delete::byJsonPathNoMatch) +} diff --git a/src/integration-test/kotlin/sqlite/DeleteIT.kt b/src/integration-test/kotlin/sqlite/DeleteIT.kt new file mode 100644 index 0000000..3a08270 --- /dev/null +++ b/src/integration-test/kotlin/sqlite/DeleteIT.kt @@ -0,0 +1,43 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.Delete +import kotlin.test.Test + +@DisplayName("SQLite - Delete") +class DeleteIT { + + @Test + @DisplayName("byId deletes a matching ID") + fun byIdMatch() = + SQLiteDB().use(Delete::byIdMatch) + + @Test + @DisplayName("byId succeeds when no ID matches") + fun byIdNoMatch() = + SQLiteDB().use(Delete::byIdNoMatch) + + @Test + @DisplayName("byFields deletes matching documents") + fun byFieldsMatch() = + SQLiteDB().use(Delete::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(Delete::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsMatch() { + assertThrows { SQLiteDB().use(Delete::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathMatch() { + assertThrows { SQLiteDB().use(Delete::byJsonPathMatch) } + } +} diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 2d4edf3..b347437 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -165,7 +165,7 @@ inline fun Connection.findAll(tableName: String, orderBy: Collect * @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.byId(tableName: String, docId: TKey) = +fun Connection.deleteById(tableName: String, docId: TKey) = Delete.byId(tableName, docId, this) /** @@ -177,3 +177,23 @@ fun Connection.byId(tableName: String, docId: TKey) = */ 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: T) = + Delete.byContains(tableName, criteria, this) + +/** + * Delete documents using a JSON containment 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/main/kotlin/Count.kt b/src/main/kotlin/Count.kt index ddf3010..2a3433a 100644 --- a/src/main/kotlin/Count.kt +++ b/src/main/kotlin/Count.kt @@ -66,7 +66,7 @@ object Count { * * @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 deletion should be executed + * @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 */ @@ -89,7 +89,7 @@ object Count { * * @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 deletion should be executed + * @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 */ diff --git a/src/main/kotlin/Delete.kt b/src/main/kotlin/Delete.kt index d9002b0..6d8f771 100644 --- a/src/main/kotlin/Delete.kt +++ b/src/main/kotlin/Delete.kt @@ -18,7 +18,7 @@ object Delete { fun byId(tableName: String, docId: TKey, conn: Connection) = conn.customNonQuery( Delete.byId(tableName, docId), - Parameters.addFields(listOf(Field.equal(Configuration.idField, docId))) + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))) ) /** @@ -52,4 +52,46 @@ object Delete { */ 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 + */ + inline fun byContains(tableName: String, criteria: T, conn: Connection) = + conn.customNonQuery(Delete.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: T) = + Configuration.dbConn().use { byContains(tableName, criteria, it) } + + /** + * Delete documents using a JSON containment 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) = + conn.customNonQuery(Delete.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) + + /** + * Delete documents using a JSON containment 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) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } } -- 2.47.2 From 4d4e1d0897892ba9399cf4545425beb7b5c3a416 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 1 Mar 2025 11:35:08 -0500 Subject: [PATCH 26/88] Add exists functions --- src/integration-test/kotlin/common/Exists.kt | 63 +++++++++ .../kotlin/postgresql/DeleteIT.kt | 3 + .../kotlin/postgresql/ExistsIT.kt | 52 ++++++++ .../kotlin/sqlite/DeleteIT.kt | 7 +- .../kotlin/sqlite/ExistsIT.kt | 46 +++++++ src/main/kotlin/ConnectionExtensions.kt | 49 ++++++- src/main/kotlin/Count.kt | 4 +- src/main/kotlin/Delete.kt | 4 +- src/main/kotlin/Exists.kt | 123 ++++++++++++++++++ 9 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 src/integration-test/kotlin/common/Exists.kt create mode 100644 src/integration-test/kotlin/postgresql/ExistsIT.kt create mode 100644 src/integration-test/kotlin/sqlite/ExistsIT.kt create mode 100644 src/main/kotlin/Exists.kt diff --git a/src/integration-test/kotlin/common/Exists.kt b/src/integration-test/kotlin/common/Exists.kt new file mode 100644 index 0000000..77525ff --- /dev/null +++ b/src/integration-test/kotlin/common/Exists.kt @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.* +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Exists` object + */ +object Exists { + + 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/integration-test/kotlin/postgresql/DeleteIT.kt b/src/integration-test/kotlin/postgresql/DeleteIT.kt index 6504711..0c62efe 100644 --- a/src/integration-test/kotlin/postgresql/DeleteIT.kt +++ b/src/integration-test/kotlin/postgresql/DeleteIT.kt @@ -4,6 +4,9 @@ import org.junit.jupiter.api.DisplayName import solutions.bitbadger.documents.common.Delete import kotlin.test.Test +/** + * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ @DisplayName("PostgreSQL - Delete") class DeleteIT { diff --git a/src/integration-test/kotlin/postgresql/ExistsIT.kt b/src/integration-test/kotlin/postgresql/ExistsIT.kt new file mode 100644 index 0000000..3ce135d --- /dev/null +++ b/src/integration-test/kotlin/postgresql/ExistsIT.kt @@ -0,0 +1,52 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Exists +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("PostgreSQL - Exists") +class ExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + fun byIdMatch() = + PgDB().use(Exists::byIdMatch) + + @Test + @DisplayName("byId returns false when no document matches the ID") + fun byIdNoMatch() = + PgDB().use(Exists::byIdNoMatch) + + @Test + @DisplayName("byFields returns true when documents match") + fun byFieldsMatch() = + PgDB().use(Exists::byFieldsMatch) + + @Test + @DisplayName("byFields returns false when no documents match") + fun byFieldsNoMatch() = + PgDB().use(Exists::byFieldsNoMatch) + + @Test + @DisplayName("byContains returns true when documents match") + fun byContainsMatch() = + PgDB().use(Exists::byContainsMatch) + + @Test + @DisplayName("byContains returns false when no documents match") + fun byContainsNoMatch() = + PgDB().use(Exists::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath returns true when documents match") + fun byJsonPathMatch() = + PgDB().use(Exists::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath returns false when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(Exists::byJsonPathNoMatch) +} diff --git a/src/integration-test/kotlin/sqlite/DeleteIT.kt b/src/integration-test/kotlin/sqlite/DeleteIT.kt index 3a08270..68649b9 100644 --- a/src/integration-test/kotlin/sqlite/DeleteIT.kt +++ b/src/integration-test/kotlin/sqlite/DeleteIT.kt @@ -6,6 +6,9 @@ import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.common.Delete import kotlin.test.Test +/** + * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions + */ @DisplayName("SQLite - Delete") class DeleteIT { @@ -31,13 +34,13 @@ class DeleteIT { @Test @DisplayName("byContains fails") - fun byContainsMatch() { + fun byContainsFails() { assertThrows { SQLiteDB().use(Delete::byContainsMatch) } } @Test @DisplayName("byJsonPath fails") - fun byJsonPathMatch() { + fun byJsonPathFails() { assertThrows { SQLiteDB().use(Delete::byJsonPathMatch) } } } diff --git a/src/integration-test/kotlin/sqlite/ExistsIT.kt b/src/integration-test/kotlin/sqlite/ExistsIT.kt new file mode 100644 index 0000000..87c56c7 --- /dev/null +++ b/src/integration-test/kotlin/sqlite/ExistsIT.kt @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.Exists +import kotlin.test.Test + +/** + * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions + */ +@DisplayName("SQLite - Exists") +class ExistsIT { + + @Test + @DisplayName("byId returns true when a document matches the ID") + fun byIdMatch() = + SQLiteDB().use(Exists::byIdMatch) + + @Test + @DisplayName("byId returns false when no document matches the ID") + fun byIdNoMatch() = + SQLiteDB().use(Exists::byIdNoMatch) + + @Test + @DisplayName("byFields returns true when documents match") + fun byFieldsMatch() = + SQLiteDB().use(Exists::byFieldsMatch) + + @Test + @DisplayName("byFields returns false when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(Exists::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(Exists::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(Exists::byJsonPathMatch) } + } +} diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index b347437..f46dde1 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -127,7 +127,7 @@ inline fun Connection.countByContains(tableName: String, criteria: T Count.byContains(tableName, criteria, this) /** - * Count documents using a JSON containment query (PostgreSQL only) + * 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 @@ -137,6 +137,51 @@ inline fun Connection.countByContains(tableName: String, criteria: T 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: T) = + 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 ~~~ /** @@ -189,7 +234,7 @@ inline fun Connection.deleteByContains(tableName: String, criteria: Delete.byContains(tableName, criteria, this) /** - * Delete documents using a JSON containment query (PostgreSQL only) + * 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 diff --git a/src/main/kotlin/Count.kt b/src/main/kotlin/Count.kt index 2a3433a..738c5c4 100644 --- a/src/main/kotlin/Count.kt +++ b/src/main/kotlin/Count.kt @@ -85,7 +85,7 @@ object Count { Configuration.dbConn().use { byContains(tableName, criteria, it) } /** - * Count documents using a JSON containment query (PostgreSQL only) + * 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 @@ -101,7 +101,7 @@ object Count { ) /** - * Count documents using a JSON containment query (PostgreSQL only) + * 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 diff --git a/src/main/kotlin/Delete.kt b/src/main/kotlin/Delete.kt index 6d8f771..d64321b 100644 --- a/src/main/kotlin/Delete.kt +++ b/src/main/kotlin/Delete.kt @@ -75,7 +75,7 @@ object Delete { Configuration.dbConn().use { byContains(tableName, criteria, it) } /** - * Delete documents using a JSON containment query (PostgreSQL only) + * 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 @@ -86,7 +86,7 @@ object Delete { conn.customNonQuery(Delete.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) /** - * Delete documents using a JSON containment query (PostgreSQL only) + * 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 diff --git a/src/main/kotlin/Exists.kt b/src/main/kotlin/Exists.kt new file mode 100644 index 0000000..3978f53 --- /dev/null +++ b/src/main/kotlin/Exists.kt @@ -0,0 +1,123 @@ +package solutions.bitbadger.documents + +import solutions.bitbadger.documents.query.Exists +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) = + conn.customScalar( + Exists.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + 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 + */ + 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 + */ + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ): Boolean { + val named = Parameters.nameFields(fields) + return conn.customScalar( + Exists.byFields(tableName, named, howMatched), + Parameters.addFields(named), + 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 + */ + 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 + */ + inline fun byContains(tableName: String, criteria: T, conn: Connection) = + conn.customScalar( + Exists.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: T) = + 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) = + conn.customScalar( + Exists.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path)), + 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 called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +} -- 2.47.2 From 864477f997d220424dd59321fe809d64f47ee22a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 1 Mar 2025 14:03:39 -0500 Subject: [PATCH 27/88] Add find functions (tests pending) --- src/main/kotlin/ConnectionExtensions.kt | 120 +++++++- src/main/kotlin/Count.kt | 4 +- src/main/kotlin/Delete.kt | 4 +- src/main/kotlin/Exists.kt | 4 +- src/main/kotlin/Find.kt | 389 ++++++++++++++++++++++-- 5 files changed, 484 insertions(+), 37 deletions(-) diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index f46dde1..ec0cbb9 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -123,7 +123,7 @@ fun Connection.countByFields(tableName: String, fields: Collection>, ho * @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: T) = +inline fun Connection.countByContains(tableName: String, criteria: TContains) = Count.byContains(tableName, criteria, this) /** @@ -168,7 +168,7 @@ fun Connection.existsByFields(tableName: String, fields: Collection>, h * @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: T) = +inline fun Connection.existsByContains(tableName: String, criteria: TContains) = Exists.byContains(tableName, criteria, this) /** @@ -185,22 +185,122 @@ fun Connection.existsByJsonPath(tableName: String, path: String) = // ~~~ DOCUMENT RETRIEVAL QUERIES ~~~ /** - * Retrieve all documents in the given table + * 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) = - Find.all(tableName, this) +inline fun Connection.findAll(tableName: String, orderBy: Collection>? = null) = + Find.all(tableName, orderBy, this) /** - * Retrieve all documents in the given table + * 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 - * @return A list of documents from the given table + * @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.findAll(tableName: String, orderBy: Collection>) = - Find.all(tableName, orderBy, this) +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 DELETION QUERIES ~~~ @@ -230,7 +330,7 @@ fun Connection.deleteByFields(tableName: String, fields: Collection>, h * @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: T) = +inline fun Connection.deleteByContains(tableName: String, criteria: TContains) = Delete.byContains(tableName, criteria, this) /** diff --git a/src/main/kotlin/Count.kt b/src/main/kotlin/Count.kt index 738c5c4..970c865 100644 --- a/src/main/kotlin/Count.kt +++ b/src/main/kotlin/Count.kt @@ -70,7 +70,7 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ - inline fun byContains(tableName: String, criteria: T, conn: Connection) = + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customScalar(Count.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Results::toCount) /** @@ -81,7 +81,7 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ - inline fun byContains(tableName: String, criteria: T) = + inline fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } /** diff --git a/src/main/kotlin/Delete.kt b/src/main/kotlin/Delete.kt index d64321b..e3ef705 100644 --- a/src/main/kotlin/Delete.kt +++ b/src/main/kotlin/Delete.kt @@ -61,7 +61,7 @@ object Delete { * @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: T, conn: Connection) = + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customNonQuery(Delete.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) /** @@ -71,7 +71,7 @@ object Delete { * @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: T) = + inline fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } /** diff --git a/src/main/kotlin/Exists.kt b/src/main/kotlin/Exists.kt index 3978f53..0b1e1ce 100644 --- a/src/main/kotlin/Exists.kt +++ b/src/main/kotlin/Exists.kt @@ -76,7 +76,7 @@ object Exists { * @return True if any matching documents exist, false if not * @throws DocumentException If called on a SQLite connection */ - inline fun byContains(tableName: String, criteria: T, conn: Connection) = + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customScalar( Exists.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), @@ -91,7 +91,7 @@ object Exists { * @return True if any matching documents exist, false if not * @throws DocumentException If called on a SQLite connection */ - inline fun byContains(tableName: String, criteria: T) = + inline fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } /** diff --git a/src/main/kotlin/Find.kt b/src/main/kotlin/Find.kt index b9131ea..898d910 100644 --- a/src/main/kotlin/Find.kt +++ b/src/main/kotlin/Find.kt @@ -9,6 +9,27 @@ import solutions.bitbadger.documents.query.orderBy */ 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(Find.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 * @@ -17,36 +38,362 @@ object Find { * @return A list of documents from the given table */ inline fun all(tableName: String, conn: Connection) = - conn.customList(Find.all(tableName), mapFunc = Results::fromData) + all(tableName, null, conn) /** - * Retrieve all documents in the given table + * Retrieve a document by its ID * - * @param tableName The table from which documents should be retrieved - * @return A list of documents from the given table - */ - inline fun all(tableName: String) = - Configuration.dbConn().use { all(tableName, it) } - - /** - * 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 + * @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 list of documents from the given table + * @return The document if it is found, `null` otherwise */ - inline fun all(tableName: String, orderBy: Collection>, conn: Connection) = - conn.customList(Find.all(tableName) + orderBy(orderBy), mapFunc = Results::fromData) + inline fun byId(tableName: String, docId: TKey, conn: Connection) = + conn.customSingle( + Find.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + Results::fromData + ) /** - * Retrieve all documents in the given table + * 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 orderBy Fields by which the query should be ordered - * @return A list of documents from the given table + * @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 all(tableName: String, orderBy: Collection>) = - Configuration.dbConn().use { all(tableName, orderBy, it) } + inline fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + orderBy: Collection>? = null, + conn: Connection + ): List { + val named = Parameters.nameFields(fields) + return conn.customList( + Find.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( + Find.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( + Find.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( + Find.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( + Find.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( + Find.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) } } -- 2.47.2 From 32f8db619684974ccc7c16a3c5dd46e829d0f530 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 1 Mar 2025 17:17:32 -0500 Subject: [PATCH 28/88] Add tests for Find functions --- src/integration-test/kotlin/common/Find.kt | 288 ++++++++++++++++++ .../kotlin/postgresql/FindIT.kt | 172 +++++++++++ src/integration-test/kotlin/sqlite/FindIT.kt | 128 ++++++++ src/main/kotlin/Op.kt | 2 +- src/main/kotlin/Parameters.kt | 1 + src/test/kotlin/FieldTest.kt | 2 +- src/test/kotlin/OpTest.kt | 2 +- 7 files changed, 592 insertions(+), 3 deletions(-) create mode 100644 src/integration-test/kotlin/common/Find.kt create mode 100644 src/integration-test/kotlin/postgresql/FindIT.kt create mode 100644 src/integration-test/kotlin/sqlite/FindIT.kt diff --git a/src/integration-test/kotlin/common/Find.kt b/src/integration-test/kotlin/common/Find.kt new file mode 100644 index 0000000..d0de8ea --- /dev/null +++ b/src/integration-test/kotlin/common/Find.kt @@ -0,0 +1,288 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.* +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Integration tests for the `Find` object + */ +object Find { + + 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/integration-test/kotlin/postgresql/FindIT.kt b/src/integration-test/kotlin/postgresql/FindIT.kt new file mode 100644 index 0000000..ed72e80 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/FindIT.kt @@ -0,0 +1,172 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Find +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("PostgreSQL - Find") +class FindIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + PgDB().use(Find::allDefault) + + @Test + @DisplayName("all sorts data ascending") + fun allAscending() = + PgDB().use(Find::allAscending) + + @Test + @DisplayName("all sorts data descending") + fun allDescending() = + PgDB().use(Find::allDescending) + + @Test + @DisplayName("all sorts data numerically") + fun allNumOrder() = + PgDB().use(Find::allNumOrder) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + PgDB().use(Find::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + PgDB().use(Find::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + PgDB().use(Find::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + PgDB().use(Find::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + PgDB().use(Find::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + PgDB().use(Find::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + PgDB().use(Find::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(Find::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + PgDB().use(Find::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + PgDB().use(Find::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains retrieves matching documents") + fun byContainsMatch() = + PgDB().use(Find::byContainsMatch) + + @Test + @DisplayName("byContains retrieves ordered matching documents") + fun byContainsMatchOrdered() = + PgDB().use(Find::byContainsMatchOrdered) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(Find::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath retrieves matching documents") + fun byJsonPathMatch() = + PgDB().use(Find::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath retrieves ordered matching documents") + fun byJsonPathMatchOrdered() = + PgDB().use(Find::byJsonPathMatchOrdered) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(Find::byJsonPathNoMatch) + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + PgDB().use(Find::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + PgDB().use(Find::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + PgDB().use(Find::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + PgDB().use(Find::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains retrieves a matching document") + fun firstByContainsMatchOne() = + PgDB().use(Find::firstByContainsMatchOne) + + @Test + @DisplayName("firstByContains retrieves a matching document among many") + fun firstByContainsMatchMany() = + PgDB().use(Find::firstByContainsMatchMany) + + @Test + @DisplayName("firstByContains retrieves a matching document among many (ordered)") + fun firstByContainsMatchOrdered() = + PgDB().use(Find::firstByContainsMatchOrdered) + + @Test + @DisplayName("firstByContains returns null when no document matches") + fun firstByContainsNoMatch() = + PgDB().use(Find::firstByContainsNoMatch) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document") + fun firstByJsonPathMatchOne() = + PgDB().use(Find::firstByJsonPathMatchOne) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many") + fun firstByJsonPathMatchMany() = + PgDB().use(Find::firstByJsonPathMatchMany) + + @Test + @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") + fun firstByJsonPathMatchOrdered() = + PgDB().use(Find::firstByJsonPathMatchOrdered) + + @Test + @DisplayName("firstByJsonPath returns null when no document matches") + fun firstByJsonPathNoMatch() = + PgDB().use(Find::firstByJsonPathNoMatch) +} diff --git a/src/integration-test/kotlin/sqlite/FindIT.kt b/src/integration-test/kotlin/sqlite/FindIT.kt new file mode 100644 index 0000000..fbb40b1 --- /dev/null +++ b/src/integration-test/kotlin/sqlite/FindIT.kt @@ -0,0 +1,128 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.Find +import kotlin.test.Test + +/** + * SQLite integration tests for the `Find` object / `find*` connection extension functions + */ +@DisplayName("SQLite - Find") +class FindIT { + + @Test + @DisplayName("all retrieves all documents") + fun allDefault() = + SQLiteDB().use(Find::allDefault) + + @Test + @DisplayName("all sorts data ascending") + fun allAscending() = + SQLiteDB().use(Find::allAscending) + + @Test + @DisplayName("all sorts data descending") + fun allDescending() = + SQLiteDB().use(Find::allDescending) + + @Test + @DisplayName("all sorts data numerically") + fun allNumOrder() = + SQLiteDB().use(Find::allNumOrder) + + @Test + @DisplayName("all succeeds with an empty table") + fun allEmpty() = + SQLiteDB().use(Find::allEmpty) + + @Test + @DisplayName("byId retrieves a document via a string ID") + fun byIdString() = + SQLiteDB().use(Find::byIdString) + + @Test + @DisplayName("byId retrieves a document via a numeric ID") + fun byIdNumber() = + SQLiteDB().use(Find::byIdNumber) + + @Test + @DisplayName("byId returns null when a matching ID is not found") + fun byIdNotFound() = + SQLiteDB().use(Find::byIdNotFound) + + @Test + @DisplayName("byFields retrieves matching documents") + fun byFieldsMatch() = + SQLiteDB().use(Find::byFieldsMatch) + + @Test + @DisplayName("byFields retrieves ordered matching documents") + fun byFieldsMatchOrdered() = + SQLiteDB().use(Find::byFieldsMatchOrdered) + + @Test + @DisplayName("byFields retrieves matching documents with a numeric IN clause") + fun byFieldsMatchNumIn() = + SQLiteDB().use(Find::byFieldsMatchNumIn) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(Find::byFieldsNoMatch) + + @Test + @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") + fun byFieldsMatchInArray() = + SQLiteDB().use(Find::byFieldsMatchInArray) + + @Test + @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") + fun byFieldsNoMatchInArray() = + SQLiteDB().use(Find::byFieldsNoMatchInArray) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(Find::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(Find::byJsonPathMatch) } + } + + @Test + @DisplayName("firstByFields retrieves a matching document") + fun firstByFieldsMatchOne() = + SQLiteDB().use(Find::firstByFieldsMatchOne) + + @Test + @DisplayName("firstByFields retrieves a matching document among many") + fun firstByFieldsMatchMany() = + SQLiteDB().use(Find::firstByFieldsMatchMany) + + @Test + @DisplayName("firstByFields retrieves a matching document among many (ordered)") + fun firstByFieldsMatchOrdered() = + SQLiteDB().use(Find::firstByFieldsMatchOrdered) + + @Test + @DisplayName("firstByFields returns null when no document matches") + fun firstByFieldsNoMatch() = + SQLiteDB().use(Find::firstByFieldsNoMatch) + + @Test + @DisplayName("firstByContains fails") + fun firstByContainsFails() { + assertThrows { SQLiteDB().use(Find::firstByContainsMatchOne) } + } + + @Test + @DisplayName("firstByJsonPath fails") + fun firstByJsonPathFails() { + assertThrows { SQLiteDB().use(Find::firstByJsonPathMatchOne) } + } +} diff --git a/src/main/kotlin/Op.kt b/src/main/kotlin/Op.kt index 0566f06..61723c9 100644 --- a/src/main/kotlin/Op.kt +++ b/src/main/kotlin/Op.kt @@ -21,7 +21,7 @@ enum class Op(val sql: String) { /** Compare existence in a list of values */ IN("IN"), /** Compare overlap between an array and a list of values */ - IN_ARRAY("?|"), + IN_ARRAY("??|"), /** Compare existence */ EXISTS("IS NOT NULL"), /** Compare nonexistence */ diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 11ff18b..3467ea2 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -86,6 +86,7 @@ object Parameters { return try { replaceNamesInQuery(query, parameters) + //.also(::println) .let { conn.prepareStatement(it) } .also { stmt -> replacements.sortedBy { it.first } diff --git a/src/test/kotlin/FieldTest.kt b/src/test/kotlin/FieldTest.kt index 56e54f9..5356893 100644 --- a/src/test/kotlin/FieldTest.kt +++ b/src/test/kotlin/FieldTest.kt @@ -233,7 +233,7 @@ class FieldTest { @DisplayName("toWhere generates for inArray (PostgreSQL)") fun toWhereInArrayPostgres() { Configuration.connectionString = ":postgresql:" - assertEquals("data->'even' ?| ARRAY[:it_0, :it_1, :it_2, :it_3]", + 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") } diff --git a/src/test/kotlin/OpTest.kt b/src/test/kotlin/OpTest.kt index f34099b..f30797a 100644 --- a/src/test/kotlin/OpTest.kt +++ b/src/test/kotlin/OpTest.kt @@ -61,7 +61,7 @@ class OpTest { @Test @DisplayName("IN_ARRAY uses proper SQL") fun inArraySQL() { - assertEquals("?|", Op.IN_ARRAY.sql, "The SQL for in-array is incorrect") + assertEquals("??|", Op.IN_ARRAY.sql, "The SQL for in-array is incorrect") } @Test -- 2.47.2 From d0375d14fc93a8590cf2568c155333921457230e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 1 Mar 2025 18:56:35 -0500 Subject: [PATCH 29/88] Add patch functions --- src/integration-test/kotlin/common/Patch.kt | 87 +++++++++++ .../kotlin/postgresql/PatchIT.kt | 52 +++++++ src/integration-test/kotlin/sqlite/PatchIT.kt | 46 ++++++ src/main/kotlin/ConnectionExtensions.kt | 54 +++++++ src/main/kotlin/Patch.kt | 135 ++++++++++++++++++ 5 files changed, 374 insertions(+) create mode 100644 src/integration-test/kotlin/common/Patch.kt create mode 100644 src/integration-test/kotlin/postgresql/PatchIT.kt create mode 100644 src/integration-test/kotlin/sqlite/PatchIT.kt create mode 100644 src/main/kotlin/Patch.kt diff --git a/src/integration-test/kotlin/common/Patch.kt b/src/integration-test/kotlin/common/Patch.kt new file mode 100644 index 0000000..bbacc48 --- /dev/null +++ b/src/integration-test/kotlin/common/Patch.kt @@ -0,0 +1,87 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.* +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Integration tests for the `Find` object + */ +object Patch { + + 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/integration-test/kotlin/postgresql/PatchIT.kt b/src/integration-test/kotlin/postgresql/PatchIT.kt new file mode 100644 index 0000000..225e8f9 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/PatchIT.kt @@ -0,0 +1,52 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Patch +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("PostgreSQL - Patch") +class PatchIT { + + @Test + @DisplayName("byId patches an existing document") + fun byIdMatch() = + PgDB().use(Patch::byIdMatch) + + @Test + @DisplayName("byId succeeds for a non-existent document") + fun byIdNoMatch() = + PgDB().use(Patch::byIdNoMatch) + + @Test + @DisplayName("byFields patches matching document") + fun byFieldsMatch() = + PgDB().use(Patch::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + PgDB().use(Patch::byFieldsNoMatch) + + @Test + @DisplayName("byContains patches matching document") + fun byContainsMatch() = + PgDB().use(Patch::byContainsMatch) + + @Test + @DisplayName("byContains succeeds when no documents match") + fun byContainsNoMatch() = + PgDB().use(Patch::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath patches matching document") + fun byJsonPathMatch() = + PgDB().use(Patch::byJsonPathMatch) + + @Test + @DisplayName("byJsonPath succeeds when no documents match") + fun byJsonPathNoMatch() = + PgDB().use(Patch::byJsonPathNoMatch) +} diff --git a/src/integration-test/kotlin/sqlite/PatchIT.kt b/src/integration-test/kotlin/sqlite/PatchIT.kt new file mode 100644 index 0000000..b6a905f --- /dev/null +++ b/src/integration-test/kotlin/sqlite/PatchIT.kt @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.Patch +import kotlin.test.Test + +/** + * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions + */ +@DisplayName("SQLite - Patch") +class PatchIT { + + @Test + @DisplayName("byId patches an existing document") + fun byIdMatch() = + SQLiteDB().use(Patch::byIdMatch) + + @Test + @DisplayName("byId succeeds for a non-existent document") + fun byIdNoMatch() = + SQLiteDB().use(Patch::byIdNoMatch) + + @Test + @DisplayName("byFields patches matching document") + fun byFieldsMatch() = + SQLiteDB().use(Patch::byFieldsMatch) + + @Test + @DisplayName("byFields succeeds when no documents match") + fun byFieldsNoMatch() = + SQLiteDB().use(Patch::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(Patch::byContainsMatch) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(Patch::byJsonPathMatch) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index ec0cbb9..9b7476d 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -302,6 +302,60 @@ inline fun Connection.findFirstByJsonPath( ) = Find.firstByJsonPath(tableName, 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 DELETION QUERIES ~~~ /** diff --git a/src/main/kotlin/Patch.kt b/src/main/kotlin/Patch.kt new file mode 100644 index 0000000..ea3128c --- /dev/null +++ b/src/main/kotlin/Patch.kt @@ -0,0 +1,135 @@ +package solutions.bitbadger.documents + +import solutions.bitbadger.documents.query.Patch +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( + Patch.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( + Patch.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( + Patch.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( + Patch.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) } +} -- 2.47.2 From aae7dfe11d08ae5ee3fbcffe1d89c46fbc45a4cf Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 1 Mar 2025 20:50:55 -0500 Subject: [PATCH 30/88] Add remove fields functions --- src/integration-test/kotlin/Types.kt | 7 +- .../kotlin/common/Document.kt | 6 +- src/integration-test/kotlin/common/Patch.kt | 2 +- .../kotlin/common/RemoveFields.kt | 106 ++++++++++++ .../kotlin/postgresql/RemoveFieldsIT.kt | 72 ++++++++ .../kotlin/sqlite/RemoveFieldsIT.kt | 56 +++++++ src/main/kotlin/Configuration.kt | 5 +- src/main/kotlin/ConnectionExtensions.kt | 54 ++++++ src/main/kotlin/Parameters.kt | 6 +- src/main/kotlin/RemoveFields.kt | 154 ++++++++++++++++++ src/main/kotlin/query/RemoveFields.kt | 12 +- src/test/kotlin/ConfigurationTest.kt | 1 + src/test/kotlin/ParametersTest.kt | 8 +- 13 files changed, 465 insertions(+), 24 deletions(-) create mode 100644 src/integration-test/kotlin/common/RemoveFields.kt create mode 100644 src/integration-test/kotlin/postgresql/RemoveFieldsIT.kt create mode 100644 src/integration-test/kotlin/sqlite/RemoveFieldsIT.kt create mode 100644 src/main/kotlin/RemoveFields.kt diff --git a/src/integration-test/kotlin/Types.kt b/src/integration-test/kotlin/Types.kt index c698ad5..b655427 100644 --- a/src/integration-test/kotlin/Types.kt +++ b/src/integration-test/kotlin/Types.kt @@ -23,13 +23,10 @@ data class ArrayDocument(val id: String, val values: List) { } @Serializable -data class JsonDocument(val id: String, val value: String, val numValue: Int, val sub: SubDocument?) { +data class JsonDocument(val id: String, val value: String = "", val numValue: Int = 0, val sub: SubDocument? = null) { companion object { - /** An empty JsonDocument */ - val emptyDoc = JsonDocument("", "", 0, null) - /** Documents to use for testing */ - val testDocuments = listOf( + private val testDocuments = listOf( JsonDocument("one", "FIRST!", 0, null), JsonDocument("two", "another", 10, SubDocument("green", "blue")), JsonDocument("three", "", 4, null), diff --git a/src/integration-test/kotlin/common/Document.kt b/src/integration-test/kotlin/common/Document.kt index 88a0f50..e17a091 100644 --- a/src/integration-test/kotlin/common/Document.kt +++ b/src/integration-test/kotlin/common/Document.kt @@ -56,7 +56,7 @@ object Document { 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.emptyDoc) + db.conn.insert(TEST_TABLE, JsonDocument("")) val after = db.conn.findAll(TEST_TABLE) assertEquals(1, after.size, "There should have been 1 document returned") @@ -71,10 +71,10 @@ object Document { 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.emptyDoc) + db.conn.insert(TEST_TABLE, JsonDocument("")) Configuration.idStringLength = 21 - db.conn.insert(TEST_TABLE, JsonDocument.emptyDoc) + db.conn.insert(TEST_TABLE, JsonDocument("")) val after = db.conn.findAll(TEST_TABLE) assertEquals(2, after.size, "There should have been 2 documents returned") diff --git a/src/integration-test/kotlin/common/Patch.kt b/src/integration-test/kotlin/common/Patch.kt index bbacc48..888054f 100644 --- a/src/integration-test/kotlin/common/Patch.kt +++ b/src/integration-test/kotlin/common/Patch.kt @@ -7,7 +7,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue /** - * Integration tests for the `Find` object + * Integration tests for the `Patch` object */ object Patch { diff --git a/src/integration-test/kotlin/common/RemoveFields.kt b/src/integration-test/kotlin/common/RemoveFields.kt new file mode 100644 index 0000000..20fb33f --- /dev/null +++ b/src/integration-test/kotlin/common/RemoveFields.kt @@ -0,0 +1,106 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.* +import kotlin.test.* + + +/** + * Integration tests for the `RemoveFields` object + */ +object RemoveFields { + + 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/integration-test/kotlin/postgresql/RemoveFieldsIT.kt b/src/integration-test/kotlin/postgresql/RemoveFieldsIT.kt new file mode 100644 index 0000000..d7beb63 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/RemoveFieldsIT.kt @@ -0,0 +1,72 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.RemoveFields +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("PostgreSQL - RemoveFields") +class RemoveFieldsIT { + + @Test + @DisplayName("byId removes fields from an existing document") + fun byIdMatchFields() = + PgDB().use(RemoveFields::byIdMatchFields) + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + fun byIdMatchNoFields() = + PgDB().use(RemoveFields::byIdMatchNoFields) + + @Test + @DisplayName("byId succeeds when no document exists") + fun byIdNoMatch() = + PgDB().use(RemoveFields::byIdNoMatch) + + @Test + @DisplayName("byFields removes fields from matching documents") + fun byFieldsMatchFields() = + PgDB().use(RemoveFields::byFieldsMatchFields) + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + fun byFieldsMatchNoFields() = + PgDB().use(RemoveFields::byFieldsMatchNoFields) + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + fun byFieldsNoMatch() = + PgDB().use(RemoveFields::byFieldsNoMatch) + + @Test + @DisplayName("byContains removes fields from matching documents") + fun byContainsMatchFields() = + PgDB().use(RemoveFields::byContainsMatchFields) + + @Test + @DisplayName("byContains succeeds when fields do not exist on matching documents") + fun byContainsMatchNoFields() = + PgDB().use(RemoveFields::byContainsMatchNoFields) + + @Test + @DisplayName("byContains succeeds when no matching documents exist") + fun byContainsNoMatch() = + PgDB().use(RemoveFields::byContainsNoMatch) + + @Test + @DisplayName("byJsonPath removes fields from matching documents") + fun byJsonPathMatchFields() = + PgDB().use(RemoveFields::byJsonPathMatchFields) + + @Test + @DisplayName("byJsonPath succeeds when fields do not exist on matching documents") + fun byJsonPathMatchNoFields() = + PgDB().use(RemoveFields::byJsonPathMatchNoFields) + + @Test + @DisplayName("byJsonPath succeeds when no matching documents exist") + fun byJsonPathNoMatch() = + PgDB().use(RemoveFields::byJsonPathNoMatch) +} diff --git a/src/integration-test/kotlin/sqlite/RemoveFieldsIT.kt b/src/integration-test/kotlin/sqlite/RemoveFieldsIT.kt new file mode 100644 index 0000000..149b66d --- /dev/null +++ b/src/integration-test/kotlin/sqlite/RemoveFieldsIT.kt @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.RemoveFields +import kotlin.test.Test + +/** + * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions + */ +@DisplayName("SQLite - RemoveFields") +class RemoveFieldsIT { + + @Test + @DisplayName("byId removes fields from an existing document") + fun byIdMatchFields() = + SQLiteDB().use(RemoveFields::byIdMatchFields) + + @Test + @DisplayName("byId succeeds when fields do not exist on an existing document") + fun byIdMatchNoFields() = + SQLiteDB().use(RemoveFields::byIdMatchNoFields) + + @Test + @DisplayName("byId succeeds when no document exists") + fun byIdNoMatch() = + SQLiteDB().use(RemoveFields::byIdNoMatch) + + @Test + @DisplayName("byFields removes fields from matching documents") + fun byFieldsMatchFields() = + SQLiteDB().use(RemoveFields::byFieldsMatchFields) + + @Test + @DisplayName("byFields succeeds when fields do not exist on matching documents") + fun byFieldsMatchNoFields() = + SQLiteDB().use(RemoveFields::byFieldsMatchNoFields) + + @Test + @DisplayName("byFields succeeds when no matching documents exist") + fun byFieldsNoMatch() = + SQLiteDB().use(RemoveFields::byFieldsNoMatch) + + @Test + @DisplayName("byContains fails") + fun byContainsFails() { + assertThrows { SQLiteDB().use(RemoveFields::byContainsMatchFields) } + } + + @Test + @DisplayName("byJsonPath fails") + fun byJsonPathFails() { + assertThrows { SQLiteDB().use(RemoveFields::byJsonPathMatchFields) } + } +} diff --git a/src/main/kotlin/Configuration.kt b/src/main/kotlin/Configuration.kt index fa473c4..8c2b484 100644 --- a/src/main/kotlin/Configuration.kt +++ b/src/main/kotlin/Configuration.kt @@ -13,8 +13,9 @@ object Configuration { * https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md for all configuration options */ var json = Json { - encodeDefaults = true - explicitNulls = false + encodeDefaults = true + explicitNulls = false + coerceInputValues = true } /** The field in which a document's ID is stored */ diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 9b7476d..9db989c 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -356,6 +356,60 @@ inline fun Connection.patchByContains( 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 ~~~ /** diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 3467ea2..4da353b 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -105,14 +105,14 @@ object Parameters { * @param parameterName The parameter name to use for the query * @return A list of parameters to use for building the query */ - fun fieldNames(names: Collection, parameterName: String = ":name") = + fun fieldNames(names: Collection, parameterName: String = ":name"): MutableCollection> = when (Configuration.dialect("generate field name parameters")) { - Dialect.POSTGRESQL -> listOf( + 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/main/kotlin/RemoveFields.kt b/src/main/kotlin/RemoveFields.kt new file mode 100644 index 0000000..7ce2c21 --- /dev/null +++ b/src/main/kotlin/RemoveFields.kt @@ -0,0 +1,154 @@ +package solutions.bitbadger.documents + +import solutions.bitbadger.documents.query.RemoveFields +import java.sql.Connection + +/** + * 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 + */ + fun byId(tableName: String, docId: TKey, toRemove: Collection, conn: Connection) { + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + RemoveFields.byId(tableName, nameParams, docId), + Parameters.addFields( + listOf(Field.equal(Configuration.idField, docId, ":id")), + translatePath(nameParams) + ) + ) + } + + /** + * 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) = + 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 + */ + fun byFields( + tableName: String, + fields: Collection>, + toRemove: Collection, + howMatched: FieldMatch? = null, + conn: Connection + ) { + val named = Parameters.nameFields(fields) + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + RemoveFields.byFields(tableName, nameParams, named, howMatched), + Parameters.addFields(named, translatePath(nameParams)) + ) + } + + /** + * 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 + ) = + 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 + */ + inline fun byContains( + tableName: String, + criteria: TContains, + toRemove: Collection, + conn: Connection + ) { + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + RemoveFields.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) { + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + RemoveFields.byJsonPath(tableName, nameParams), + listOf(Parameter(":path", ParameterType.STRING, path), *nameParams.toTypedArray()) + ) + } + + /** + * 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) = + Configuration.dbConn().use { byJsonPath(tableName, path, toRemove, it) } +} diff --git a/src/main/kotlin/query/RemoveFields.kt b/src/main/kotlin/query/RemoveFields.kt index 1077e78..b3842c4 100644 --- a/src/main/kotlin/query/RemoveFields.kt +++ b/src/main/kotlin/query/RemoveFields.kt @@ -16,9 +16,9 @@ object RemoveFields { * @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: List>) = + private fun removeFields(tableName: String, toRemove: Collection>) = when (Configuration.dialect("generate field removal query")) { - Dialect.POSTGRESQL -> "UPDATE $tableName SET data = data - ${toRemove[0].name}::text[]" + 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)" } @@ -32,7 +32,7 @@ object RemoveFields { * @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 */ - fun byId(tableName: String, toRemove: List>, docId: TKey? = null) = + fun byId(tableName: String, toRemove: Collection>, docId: TKey? = null) = byIdBase(removeFields(tableName, toRemove), docId) /** @@ -46,7 +46,7 @@ object RemoveFields { */ fun byFields( tableName: String, - toRemove: List>, + toRemove: Collection>, fields: Collection>, howMatched: FieldMatch? = null ) = @@ -59,7 +59,7 @@ object RemoveFields { * @param toRemove The parameters for the fields to be removed * @return A query to patch JSON documents by JSON containment */ - fun byContains(tableName: String, toRemove: List>) = + fun byContains(tableName: String, toRemove: Collection>) = statementWhere(removeFields(tableName, toRemove), Where.jsonContains()) /** @@ -69,6 +69,6 @@ object RemoveFields { * @param toRemove The parameters for the fields to be removed * @return A query to patch JSON documents by JSON path match */ - fun byJsonPath(tableName: String, toRemove: List>) = + fun byJsonPath(tableName: String, toRemove: Collection>) = statementWhere(removeFields(tableName, toRemove), Where.jsonPathMatches()) } diff --git a/src/test/kotlin/ConfigurationTest.kt b/src/test/kotlin/ConfigurationTest.kt index acb8981..9e83235 100644 --- a/src/test/kotlin/ConfigurationTest.kt +++ b/src/test/kotlin/ConfigurationTest.kt @@ -18,6 +18,7 @@ class ConfigurationTest { fun defaultJsonOptions() { assertTrue(Configuration.json.configuration.encodeDefaults, "Encode Defaults should have been set") assertFalse(Configuration.json.configuration.explicitNulls, "Explicit Nulls should not have been set") + assertTrue(Configuration.json.configuration.coerceInputValues, "Coerce Input Values should have been set") } @Test diff --git a/src/test/kotlin/ParametersTest.kt b/src/test/kotlin/ParametersTest.kt index 154f872..e3d64a2 100644 --- a/src/test/kotlin/ParametersTest.kt +++ b/src/test/kotlin/ParametersTest.kt @@ -62,7 +62,7 @@ class ParametersTest { @DisplayName("fieldNames generates a single parameter (PostgreSQL)") fun fieldNamesSinglePostgres() { Configuration.dialectValue = Dialect.POSTGRESQL - val nameParams = Parameters.fieldNames(listOf("test")) + 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") @@ -73,7 +73,7 @@ class ParametersTest { @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") fun fieldNamesMultiplePostgres() { Configuration.dialectValue = Dialect.POSTGRESQL - val nameParams = Parameters.fieldNames(listOf("test", "this", "today")) + 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") @@ -84,7 +84,7 @@ class ParametersTest { @DisplayName("fieldNames generates a single parameter (SQLite)") fun fieldNamesSingleSQLite() { Configuration.dialectValue = Dialect.SQLITE - val nameParams = Parameters.fieldNames(listOf("test")) + 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") @@ -95,7 +95,7 @@ class ParametersTest { @DisplayName("fieldNames generates multiple parameters (SQLite)") fun fieldNamesMultipleSQLite() { Configuration.dialectValue = Dialect.SQLITE - val nameParams = Parameters.fieldNames(listOf("test", "this", "today")) + 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") -- 2.47.2 From 97be70cfafce93b6b12b599470b00570a702b985 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 1 Mar 2025 21:28:07 -0500 Subject: [PATCH 31/88] Add remaining document functions --- .../kotlin/common/Document.kt | 43 +++++++++++- .../kotlin/postgresql/DocumentIT.kt | 20 ++++++ .../kotlin/sqlite/DocumentIT.kt | 20 ++++++ src/main/kotlin/ConnectionExtensions.kt | 19 ++++++ src/main/kotlin/Document.kt | 65 ++++++++++++++++--- 5 files changed, 157 insertions(+), 10 deletions(-) diff --git a/src/integration-test/kotlin/common/Document.kt b/src/integration-test/kotlin/common/Document.kt index e17a091..c0fdc51 100644 --- a/src/integration-test/kotlin/common/Document.kt +++ b/src/integration-test/kotlin/common/Document.kt @@ -1,8 +1,7 @@ package solutions.bitbadger.documents.common import solutions.bitbadger.documents.* -import kotlin.test.assertEquals -import kotlin.test.fail +import kotlin.test.* /** * Integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions @@ -85,4 +84,44 @@ object Document { 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/integration-test/kotlin/postgresql/DocumentIT.kt b/src/integration-test/kotlin/postgresql/DocumentIT.kt index ee318a7..df3ac17 100644 --- a/src/integration-test/kotlin/postgresql/DocumentIT.kt +++ b/src/integration-test/kotlin/postgresql/DocumentIT.kt @@ -34,4 +34,24 @@ class DocumentIT { @DisplayName("insert succeeds with random string auto ID") fun insertStringAutoId() = PgDB().use(Document::insertStringAutoId) + + @Test + @DisplayName("save updates an existing document") + fun saveMatch() = + PgDB().use(Document::saveMatch) + + @Test + @DisplayName("save inserts a new document") + fun saveNoMatch() = + PgDB().use(Document::saveNoMatch) + + @Test + @DisplayName("update replaces an existing document") + fun updateMatch() = + PgDB().use(Document::updateMatch) + + @Test + @DisplayName("update succeeds when no document exists") + fun updateNoMatch() = + PgDB().use(Document::updateNoMatch) } diff --git a/src/integration-test/kotlin/sqlite/DocumentIT.kt b/src/integration-test/kotlin/sqlite/DocumentIT.kt index f8c00d2..edb1dfb 100644 --- a/src/integration-test/kotlin/sqlite/DocumentIT.kt +++ b/src/integration-test/kotlin/sqlite/DocumentIT.kt @@ -34,4 +34,24 @@ class DocumentIT { @DisplayName("insert succeeds with random string auto ID") fun insertStringAutoId() = SQLiteDB().use(Document::insertStringAutoId) + + @Test + @DisplayName("save updates an existing document") + fun saveMatch() = + SQLiteDB().use(Document::saveMatch) + + @Test + @DisplayName("save inserts a new document") + fun saveNoMatch() = + SQLiteDB().use(Document::saveNoMatch) + + @Test + @DisplayName("update replaces an existing document") + fun updateMatch() = + SQLiteDB().use(Document::updateMatch) + + @Test + @DisplayName("update succeeds when no document exists") + fun updateNoMatch() = + SQLiteDB().use(Document::updateNoMatch) } diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 9db989c..841fb31 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -93,6 +93,25 @@ fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) 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 ~~~ /** diff --git a/src/main/kotlin/Document.kt b/src/main/kotlin/Document.kt index 0f21a75..fff4a3a 100644 --- a/src/main/kotlin/Document.kt +++ b/src/main/kotlin/Document.kt @@ -2,6 +2,8 @@ package solutions.bitbadger.documents import java.sql.Connection import solutions.bitbadger.documents.query.Document +import solutions.bitbadger.documents.query.Where +import solutions.bitbadger.documents.query.statementWhere /** * Functions for manipulating documents @@ -20,24 +22,25 @@ object Document { val query = if (strategy == AutoId.DISABLED) { Document.insert(tableName) } else { - val idField = Configuration.idField - val dialect = Configuration.dialect("Create auto-ID insert query") + 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.NUMBER -> "' || (SELECT coalesce(max(data->>'$idField')::numeric, 0) + 1 " + + "FROM $tableName) || '" + AutoId.UUID -> "\"${AutoId.generateUUID()}\"" AutoId.RANDOM_STRING -> "\"${AutoId.generateRandomString()}\"" - else -> "\"' || (:data)->>'$idField' || '\"" + 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.NUMBER -> "(SELECT coalesce(max(data->>'$idField'), 0) + 1 FROM $tableName)" + AutoId.UUID -> "'${AutoId.generateUUID()}'" AutoId.RANDOM_STRING -> "'${AutoId.generateRandomString()}'" - else -> "(:data)->>'$idField'" + else -> "(:data)->>'$idField'" }.let { "json_set(:data, '$.$idField', $it)" } } } else { @@ -57,4 +60,50 @@ object Document { */ 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(Document.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(Document.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) } } -- 2.47.2 From c184ea7e79889278c4c2f16aa5463073fb33a628 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 2 Mar 2025 16:17:48 -0500 Subject: [PATCH 32/88] pom updates, move integration tests to test dir --- .gitignore | 3 ++ .idea/jarRepositories.xml | 10 ++--- pom.xml | 45 ++++++++++++++++--- src/test/kotlin/ComparisonTest.kt | 1 + .../kotlin/integration}/ThrowawayDatabase.kt | 2 +- .../kotlin/integration}/Types.kt | 9 ++-- .../kotlin/integration}/common/Count.kt | 5 ++- .../kotlin/integration}/common/Custom.kt | 5 ++- .../kotlin/integration}/common/Definition.kt | 4 +- .../kotlin/integration}/common/Delete.kt | 5 ++- .../kotlin/integration}/common/Document.kt | 3 +- .../kotlin/integration}/common/Exists.kt | 5 ++- .../kotlin/integration}/common/Find.kt | 3 +- .../kotlin/integration}/common/Patch.kt | 5 ++- .../integration}/common/RemoveFields.kt | 5 ++- .../kotlin/integration}/postgresql/CountIT.kt | 4 +- .../integration}/postgresql/CustomIT.kt | 4 +- .../integration}/postgresql/DefinitionIT.kt | 4 +- .../integration}/postgresql/DeleteIT.kt | 4 +- .../integration}/postgresql/DocumentIT.kt | 4 +- .../integration}/postgresql/ExistsIT.kt | 4 +- .../kotlin/integration}/postgresql/FindIT.kt | 4 +- .../kotlin/integration}/postgresql/PatchIT.kt | 4 +- .../kotlin/integration}/postgresql/PgDB.kt | 4 +- .../integration}/postgresql/RemoveFieldsIT.kt | 4 +- .../kotlin/integration}/sqlite/CountIT.kt | 4 +- .../kotlin/integration}/sqlite/CustomIT.kt | 4 +- .../integration}/sqlite/DefinitionIT.kt | 4 +- .../kotlin/integration}/sqlite/DeleteIT.kt | 4 +- .../kotlin/integration}/sqlite/DocumentIT.kt | 4 +- .../kotlin/integration}/sqlite/ExistsIT.kt | 4 +- .../kotlin/integration}/sqlite/FindIT.kt | 4 +- .../kotlin/integration}/sqlite/PatchIT.kt | 4 +- .../integration}/sqlite/RemoveFieldsIT.kt | 4 +- .../kotlin/integration}/sqlite/SQLiteDB.kt | 4 +- 35 files changed, 129 insertions(+), 61 deletions(-) rename src/{integration-test/kotlin => test/kotlin/integration}/ThrowawayDatabase.kt (90%) rename src/{integration-test/kotlin => test/kotlin/integration}/Types.kt (84%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/Count.kt (88%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/Custom.kt (92%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/Definition.kt (92%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/Delete.kt (92%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/Document.kt (98%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/Exists.kt (89%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/Find.kt (99%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/Patch.kt (93%) rename src/{integration-test/kotlin => test/kotlin/integration}/common/RemoveFields.kt (95%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/CountIT.kt (91%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/CustomIT.kt (90%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/DefinitionIT.kt (87%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/DeleteIT.kt (91%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/DocumentIT.kt (92%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/ExistsIT.kt (91%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/FindIT.kt (97%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/PatchIT.kt (91%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/PgDB.kt (88%) rename src/{integration-test/kotlin => test/kotlin/integration}/postgresql/RemoveFieldsIT.kt (94%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/CountIT.kt (89%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/CustomIT.kt (90%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/DefinitionIT.kt (89%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/DeleteIT.kt (90%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/DocumentIT.kt (92%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/ExistsIT.kt (91%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/FindIT.kt (96%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/PatchIT.kt (90%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/RemoveFieldsIT.kt (93%) rename src/{integration-test/kotlin => test/kotlin/integration}/sqlite/SQLiteDB.kt (80%) diff --git a/.gitignore b/.gitignore index e36da4c..7fea862 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ replay_pid* # Temporary output directories **/target + +# Maven Central Repo settings +settings.xml diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index 4d27ef0..b301a31 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -1,16 +1,16 @@ - - + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 83150c0..19963a3 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ UTF-8 official - 1.8 + 11 2.1.0 1.8.0 @@ -111,6 +111,14 @@ MainKt + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + diff --git a/src/main/kotlin/AutoId.kt b/src/main/kotlin/AutoId.kt index a5bfa54..3e4c4c8 100644 --- a/src/main/kotlin/AutoId.kt +++ b/src/main/kotlin/AutoId.kt @@ -1,6 +1,8 @@ package solutions.bitbadger.documents +import kotlin.jvm.Throws import kotlin.reflect.full.* +import kotlin.reflect.jvm.isAccessible /** * Strategies for automatic document IDs @@ -22,7 +24,7 @@ enum class AutoId { * * @return A `UUID` string */ - fun generateUUID(): String = + @JvmStatic fun generateUUID(): String = java.util.UUID.randomUUID().toString().replace("-", "") /** @@ -31,7 +33,7 @@ enum class AutoId { * @param length The length of the string (optional; defaults to configured length) * @return A string of random hex characters of the requested length */ - fun generateRandomString(length: Int? = null): String = + @JvmStatic fun generateRandomString(length: Int? = null): String = (length ?: Configuration.idStringLength).let { len -> kotlin.random.Random.nextBytes((len + 2) / 2) .joinToString("") { String.format("%02x", it) } @@ -47,12 +49,13 @@ enum class AutoId { * @return `true` if the document needs an automatic ID, `false` if not * @throws DocumentException If bad input prevents the determination */ - fun needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean { + @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; + if (strategy == DISABLED) return false - val id = document!!::class.memberProperties.find { it.name == idProp } + 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) { @@ -65,11 +68,12 @@ enum class AutoId { } } - if (id.returnType == String::class.createType()) { + val typ = id.returnType.toString() + if (typ.endsWith("String") || typ.endsWith("String!")) { return id.call(document) == "" } - throw DocumentException("$idProp was not a string; cannot auto-generate UUID or random string") + throw DocumentException("$idProp was not a string ($typ); cannot auto-generate UUID or random string") } } } diff --git a/src/main/kotlin/Configuration.kt b/src/main/kotlin/Configuration.kt index 8c2b484..7313eea 100644 --- a/src/main/kotlin/Configuration.kt +++ b/src/main/kotlin/Configuration.kt @@ -3,6 +3,7 @@ package solutions.bitbadger.documents import kotlinx.serialization.json.Json import java.sql.Connection import java.sql.DriverManager +import kotlin.jvm.Throws object Configuration { @@ -12,25 +13,33 @@ object Configuration { * The default sets `encodeDefaults` to `true` and `explicitNulls` to `false`; see * https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md for all configuration options */ + @JvmField var json = Json { - encodeDefaults = true - explicitNulls = false + encodeDefaults = true + explicitNulls = false coerceInputValues = true } /** 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 JSON serializer to use for documents */ + var serializer: DocumentSerializer = DocumentSerializerKotlin() + /** The derived dialect value from the connection string */ internal var dialectValue: Dialect? = null /** The connection string for the JDBC connection */ + @JvmStatic var connectionString: String? = null set(value) { field = value @@ -43,6 +52,7 @@ object Configuration { * @return A new connection to the configured database * @throws IllegalArgumentException If the connection string is not set before calling this */ + @JvmStatic fun dbConn(): Connection { if (connectionString == null) { throw IllegalArgumentException("Please provide a connection string before attempting data access") @@ -57,7 +67,11 @@ object Configuration { * @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") + "Database mode not set" + if (process == null) "" else "; cannot $process" + ) } diff --git a/src/main/kotlin/Field.kt b/src/main/kotlin/Field.kt index 4a3d8e8..d15a838 100644 --- a/src/main/kotlin/Field.kt +++ b/src/main/kotlin/Field.kt @@ -69,18 +69,18 @@ class Field private constructor( 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 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 + 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.BETWEEN -> " ${parameterName}min AND ${parameterName}max" + Op.IN -> " ($inParameterNames)" Op.IN_ARRAY -> if (dialect == Dialect.POSTGRESQL) " ARRAY[$inParameterNames]" else "" - else -> " $parameterName" + else -> " $parameterName" } @Suppress("UNCHECKED_CAST") @@ -134,7 +134,7 @@ class Field private constructor( } override fun toString() = - "Field ${parameterName ?: ""} $comparison${qualifier?.let { " (qualifier $it)"} ?: ""}" + "Field ${parameterName ?: ""} $comparison${qualifier?.let { " (qualifier $it)" } ?: ""}" companion object { @@ -146,6 +146,8 @@ class Field private constructor( * @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) @@ -157,6 +159,8 @@ class Field private constructor( * @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) @@ -168,6 +172,8 @@ class Field private constructor( * @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) @@ -179,6 +185,8 @@ class Field private constructor( * @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) @@ -190,6 +198,8 @@ class Field private constructor( * @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) @@ -201,6 +211,8 @@ class Field private constructor( * @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) @@ -213,6 +225,8 @@ class Field private constructor( * @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) @@ -224,6 +238,8 @@ class Field private constructor( * @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) @@ -236,18 +252,50 @@ class Field private constructor( * @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 "" @@ -266,4 +314,4 @@ class Field private constructor( return path.toString() } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 4da353b..3bf15b2 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -3,6 +3,7 @@ package solutions.bitbadger.documents 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 @@ -17,6 +18,7 @@ object Parameters { * @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 { @@ -45,6 +47,7 @@ object Parameters { * @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) } @@ -55,6 +58,7 @@ object Parameters { * @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, "?") } @@ -67,6 +71,8 @@ object Parameters { * @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 { @@ -105,6 +111,8 @@ object Parameters { * @param parameterName The parameter name to use for the query * @return A list of parameters to use for building the query */ + @JvmStatic + @JvmOverloads fun fieldNames(names: Collection, parameterName: String = ":name"): MutableCollection> = when (Configuration.dialect("generate field name parameters")) { Dialect.POSTGRESQL -> mutableListOf( diff --git a/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java b/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java new file mode 100644 index 0000000..6375b24 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java @@ -0,0 +1,217 @@ +package solutions.bitbadger.documents.java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.AutoId; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.java.testDocs.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `AutoId` enum + */ +@DisplayName("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/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java b/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java new file mode 100644 index 0000000..ed6a71a --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.java; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `Configuration` object + */ +@DisplayName("Java | Configuration") +final public class ConfigurationTest { + + @Test + @DisplayName("Default JSON options are as expected") + public void defaultJsonOptions() { + assertTrue(Configuration.json.getConfiguration().getEncodeDefaults(), "Encode Defaults should have been set"); + assertFalse(Configuration.json.getConfiguration().getExplicitNulls(), + "Explicit Nulls should not have been set"); + assertTrue(Configuration.json.getConfiguration().getCoerceInputValues(), + "Coerce Input Values should have been set"); + } + + @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() { + try { + assertThrows(DocumentException.class, Configuration::dialect); + Configuration.setConnectionString("jdbc:postgresql:db"); + assertEquals(Dialect.POSTGRESQL, Configuration.dialect()); + } catch (DocumentException ex) { + fail(ex); + } finally { + Configuration.setConnectionString(null); + } + } +} diff --git a/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java b/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java new file mode 100644 index 0000000..c54f9cb --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.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("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/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java b/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java new file mode 100644 index 0000000..c974534 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.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("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/test/java/solutions/bitbadger/documents/java/FieldTest.java b/src/test/java/solutions/bitbadger/documents/java/FieldTest.java new file mode 100644 index 0000000..287b1ef --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/FieldTest.java @@ -0,0 +1,629 @@ +package solutions.bitbadger.documents.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 java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `Field` class + */ +@DisplayName("Java | Field") +final public class FieldTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + public void cleanUp() { + Configuration.setConnectionString(null); + } + + // ~~~ 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 PostgreSQL field") + 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 PostgreSQL field") + 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 PostgreSQL field") + 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 PostgreSQL field") + 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 SQLite field") + 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 SQLite field") + 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 SQLite field") + 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 SQLite field") + 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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":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() { + Configuration.setConnectionString(":postgresql:"); + 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() { + Configuration.setConnectionString(":sqlite:"); + assertEquals("q.data->>'le_field' <= :it", + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(), + "Field WHERE clause not generated correctly"); + } + + // ~~~ STATIC TESTS ~~~ + + @Test + @DisplayName("equal constructs a field w/o parameter name") + public void equalCtor() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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/test/java/solutions/bitbadger/documents/java/OpTest.java b/src/test/java/solutions/bitbadger/documents/java/OpTest.java new file mode 100644 index 0000000..3a319cf --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/OpTest.java @@ -0,0 +1,80 @@ +package solutions.bitbadger.documents.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("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/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java b/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java new file mode 100644 index 0000000..15d5498 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.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("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/test/java/solutions/bitbadger/documents/java/ParameterTest.java b/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java new file mode 100644 index 0000000..2e6556b --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java @@ -0,0 +1,40 @@ +package solutions.bitbadger.documents.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("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/test/java/solutions/bitbadger/documents/java/ParametersTest.java b/src/test/java/solutions/bitbadger/documents/java/ParametersTest.java new file mode 100644 index 0000000..6c011d1 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/ParametersTest.java @@ -0,0 +1,121 @@ +package solutions.bitbadger.documents.java; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.*; + +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the `Parameters` object + */ +@DisplayName("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() { + 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() { + 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() { + 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() { + 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/test/java/solutions/bitbadger/documents/java/testDocs/ByteIdClass.java b/src/test/java/solutions/bitbadger/documents/java/testDocs/ByteIdClass.java new file mode 100644 index 0000000..c0d41e7 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/testDocs/ByteIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.java.testDocs; + +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/test/java/solutions/bitbadger/documents/java/testDocs/IntIdClass.java b/src/test/java/solutions/bitbadger/documents/java/testDocs/IntIdClass.java new file mode 100644 index 0000000..1d7a0cc --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/testDocs/IntIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.java.testDocs; + +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/test/java/solutions/bitbadger/documents/java/testDocs/LongIdClass.java b/src/test/java/solutions/bitbadger/documents/java/testDocs/LongIdClass.java new file mode 100644 index 0000000..1ee8bc0 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/testDocs/LongIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.java.testDocs; + +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/test/java/solutions/bitbadger/documents/java/testDocs/ShortIdClass.java b/src/test/java/solutions/bitbadger/documents/java/testDocs/ShortIdClass.java new file mode 100644 index 0000000..83a2f3c --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/testDocs/ShortIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.java.testDocs; + +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/test/java/solutions/bitbadger/documents/java/testDocs/StringIdClass.java b/src/test/java/solutions/bitbadger/documents/java/testDocs/StringIdClass.java new file mode 100644 index 0000000..3013885 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/testDocs/StringIdClass.java @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.java.testDocs; + +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/test/kotlin/AutoIdTest.kt b/src/test/kotlin/AutoIdTest.kt index e67df54..58cc9aa 100644 --- a/src/test/kotlin/AutoIdTest.kt +++ b/src/test/kotlin/AutoIdTest.kt @@ -11,7 +11,7 @@ import kotlin.test.assertTrue /** * Unit tests for the `AutoId` enum */ -@DisplayName("AutoId") +@DisplayName("Kotlin | AutoId") class AutoIdTest { @Test @@ -66,8 +66,10 @@ class AutoIdTest { @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") + 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") @@ -77,8 +79,10 @@ class AutoIdTest { @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") + 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") @@ -98,7 +102,10 @@ class AutoIdTest { @Test @DisplayName("needsAutoId returns false for Number strategy and long ID of non-0") fun needsAutoIdFalseForLongWithNonZero() = - assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(2), "id"), "Number Auto ID with 2 should return false") + 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") @@ -109,14 +116,18 @@ class AutoIdTest { @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") + 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") + 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") @@ -127,14 +138,18 @@ class AutoIdTest { @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") + 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") + 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") diff --git a/src/test/kotlin/ConfigurationTest.kt b/src/test/kotlin/ConfigurationTest.kt index 9e83235..22754a5 100644 --- a/src/test/kotlin/ConfigurationTest.kt +++ b/src/test/kotlin/ConfigurationTest.kt @@ -10,7 +10,7 @@ import kotlin.test.assertTrue /** * Unit tests for the `Configuration` object */ -@DisplayName("Configuration") +@DisplayName("Kotlin | Configuration") class ConfigurationTest { @Test diff --git a/src/test/kotlin/DocumentIndexTest.kt b/src/test/kotlin/DocumentIndexTest.kt index 0947752..9747cde 100644 --- a/src/test/kotlin/DocumentIndexTest.kt +++ b/src/test/kotlin/DocumentIndexTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `DocumentIndex` enum */ -@DisplayName("Op") +@DisplayName("Kotlin | DocumentIndex") class DocumentIndexTest { @Test diff --git a/src/test/kotlin/FieldMatchTest.kt b/src/test/kotlin/FieldMatchTest.kt index 00ba366..ceb1725 100644 --- a/src/test/kotlin/FieldMatchTest.kt +++ b/src/test/kotlin/FieldMatchTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `FieldMatch` enum */ -@DisplayName("FieldMatch") +@DisplayName("Kotlin | FieldMatch") class FieldMatchTest { @Test diff --git a/src/test/kotlin/FieldTest.kt b/src/test/kotlin/FieldTest.kt index 5356893..783979b 100644 --- a/src/test/kotlin/FieldTest.kt +++ b/src/test/kotlin/FieldTest.kt @@ -11,7 +11,7 @@ import kotlin.test.assertNull /** * Unit tests for the `Field` class */ -@DisplayName("Field") +@DisplayName("Kotlin | Field") class FieldTest { /** diff --git a/src/test/kotlin/OpTest.kt b/src/test/kotlin/OpTest.kt index f30797a..5e6d6cd 100644 --- a/src/test/kotlin/OpTest.kt +++ b/src/test/kotlin/OpTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `Op` enum */ -@DisplayName("Op") +@DisplayName("Kotlin | Op") class OpTest { @Test diff --git a/src/test/kotlin/ParameterNameTest.kt b/src/test/kotlin/ParameterNameTest.kt index 44ed76b..b08f6bb 100644 --- a/src/test/kotlin/ParameterNameTest.kt +++ b/src/test/kotlin/ParameterNameTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `ParameterName` class */ -@DisplayName("ParameterName") +@DisplayName("Kotlin | ParameterName") class ParameterNameTest { @Test diff --git a/src/test/kotlin/ParameterTest.kt b/src/test/kotlin/ParameterTest.kt index 9f08515..959ad25 100644 --- a/src/test/kotlin/ParameterTest.kt +++ b/src/test/kotlin/ParameterTest.kt @@ -9,7 +9,7 @@ import kotlin.test.assertNull /** * Unit tests for the `Parameter` class */ -@DisplayName("Parameter") +@DisplayName("Kotlin | Parameter") class ParameterTest { @Test diff --git a/src/test/kotlin/ParametersTest.kt b/src/test/kotlin/ParametersTest.kt index e3d64a2..686ed67 100644 --- a/src/test/kotlin/ParametersTest.kt +++ b/src/test/kotlin/ParametersTest.kt @@ -11,7 +11,7 @@ import kotlin.test.assertSame /** * Unit tests for the `Parameters` object */ -@DisplayName("Parameters") +@DisplayName("Kotlin | Parameters") class ParametersTest { /** -- 2.47.2 From b17ef73ff8ae04d4c96f8694c56a58e83d1a2a9f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 10 Mar 2025 23:04:34 -0400 Subject: [PATCH 34/88] WIP on Java integration test shell --- src/main/kotlin/Count.kt | 10 +++ src/main/kotlin/Document.kt | 6 ++ .../java/integration/common/Count.java | 20 +++++ .../documents/java/testDocs/JsonDocument.java | 81 +++++++++++++++++++ .../documents/java/testDocs/SubDocument.java | 32 ++++++++ 5 files changed, 149 insertions(+) create mode 100644 src/test/java/solutions/bitbadger/documents/java/integration/common/Count.java create mode 100644 src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java create mode 100644 src/test/java/solutions/bitbadger/documents/java/testDocs/SubDocument.java diff --git a/src/main/kotlin/Count.kt b/src/main/kotlin/Count.kt index 970c865..3d4d26f 100644 --- a/src/main/kotlin/Count.kt +++ b/src/main/kotlin/Count.kt @@ -15,6 +15,7 @@ object Count { * @param conn The connection over which documents should be counted * @return A count of the documents in the table */ + @JvmStatic fun all(tableName: String, conn: Connection) = conn.customScalar(Count.all(tableName), mapFunc = Results::toCount) @@ -24,6 +25,7 @@ object Count { * @param tableName The name of the table in which documents should be counted * @return A count of the documents in the table */ + @JvmStatic fun all(tableName: String) = Configuration.dbConn().use { all(tableName, it) } @@ -36,6 +38,8 @@ object Count { * @param conn The connection on which the deletion should be executed * @return A count of the matching documents in the table */ + @JvmStatic + @JvmOverloads fun byFields( tableName: String, fields: Collection>, @@ -58,6 +62,8 @@ object Count { * @param howMatched How the fields should be matched * @return A count of the matching documents in the table */ + @JvmStatic + @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } @@ -70,6 +76,7 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ + @JvmStatic inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customScalar(Count.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Results::toCount) @@ -81,6 +88,7 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ + @JvmStatic inline fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } @@ -93,6 +101,7 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ + @JvmStatic fun byJsonPath(tableName: String, path: String, conn: Connection) = conn.customScalar( Count.byJsonPath(tableName), @@ -108,6 +117,7 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ + @JvmStatic fun byJsonPath(tableName: String, path: String) = Configuration.dbConn().use { byJsonPath(tableName, path, it) } } diff --git a/src/main/kotlin/Document.kt b/src/main/kotlin/Document.kt index fff4a3a..da06089 100644 --- a/src/main/kotlin/Document.kt +++ b/src/main/kotlin/Document.kt @@ -17,6 +17,7 @@ object Document { * @param document The document to be inserted * @param conn The connection on which the query should be executed */ + @JvmStatic inline fun insert(tableName: String, document: TDoc, conn: Connection) { val strategy = Configuration.autoIdStrategy val query = if (strategy == AutoId.DISABLED) { @@ -58,6 +59,7 @@ object Document { * @param tableName The table into which the document should be inserted (may include schema) * @param document The document to be inserted */ + @JvmStatic inline fun insert(tableName: String, document: TDoc) = Configuration.dbConn().use { insert(tableName, document, it) } @@ -68,6 +70,7 @@ object Document { * @param document The document to be saved * @param conn The connection on which the query should be executed */ + @JvmStatic inline fun save(tableName: String, document: TDoc, conn: Connection) = conn.customNonQuery(Document.save(tableName), listOf(Parameters.json(":data", document))) @@ -77,6 +80,7 @@ object Document { * @param tableName The table in which the document should be saved (may include schema) * @param document The document to be saved */ + @JvmStatic inline fun save(tableName: String, document: TDoc) = Configuration.dbConn().use { save(tableName, document, it) } @@ -88,6 +92,7 @@ object Document { * @param document The document to be replaced * @param conn The connection on which the query should be executed */ + @JvmStatic inline fun update(tableName: String, docId: TKey, document: TDoc, conn: Connection) = conn.customNonQuery( statementWhere(Document.update(tableName), Where.byId(":id", docId)), @@ -104,6 +109,7 @@ object Document { * @param docId The ID of the document to be replaced * @param document The document to be replaced */ + @JvmStatic inline fun update(tableName: String, docId: TKey, document: TDoc) = Configuration.dbConn().use { update(tableName, docId, document, it) } } diff --git a/src/test/java/solutions/bitbadger/documents/java/integration/common/Count.java b/src/test/java/solutions/bitbadger/documents/java/integration/common/Count.java new file mode 100644 index 0000000..8258386 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/integration/common/Count.java @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents.java.integration.common; + +import solutions.bitbadger.documents.integration.ThrowawayDatabase; +import solutions.bitbadger.documents.java.testDocs.JsonDocument; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static solutions.bitbadger.documents.integration.TypesKt.TEST_TABLE; + +final public class Count { + + public static void all(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(5L, solutions.bitbadger.documents.Count.all(TEST_TABLE, db.getConn()), + "There should have been 5 documents in the table"); + } + + + private Count() { + } +} diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java b/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java new file mode 100644 index 0000000..be54944 --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java @@ -0,0 +1,81 @@ +package solutions.bitbadger.documents.java.testDocs; + +import solutions.bitbadger.documents.Document; +import solutions.bitbadger.documents.integration.ThrowawayDatabase; + +import java.util.List; + +import static solutions.bitbadger.documents.integration.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); + } + + 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) { + for (JsonDocument doc : testDocuments) { + // TODO: inline reified generics cannot be called from Java :( + // Document.insert(tableName, doc, db.getConn()); + } + } + + public static void load(ThrowawayDatabase db) { + load(db, TEST_TABLE); + } +} diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/SubDocument.java b/src/test/java/solutions/bitbadger/documents/java/testDocs/SubDocument.java new file mode 100644 index 0000000..f0b46ec --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/testDocs/SubDocument.java @@ -0,0 +1,32 @@ +package solutions.bitbadger.documents.java.testDocs; + +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("", ""); + } +} -- 2.47.2 From e0ec37e8c249be5235dfe629cb673031653e4abd Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 11 Mar 2025 20:35:57 -0400 Subject: [PATCH 35/88] WIP on repo reorg --- documents.iml | 4 +- pom.xml | 2 +- src/common/pom.xml | 93 ++++++++++++ src/{main => common/src}/kotlin/AutoId.kt | 14 +- src/{main => common/src}/kotlin/Comparison.kt | 2 +- src/common/src/kotlin/Configuration.kt | 63 ++++++++ src/{main => common/src}/kotlin/Dialect.kt | 2 +- .../src}/kotlin/DocumentException.kt | 4 +- .../src}/kotlin/DocumentIndex.kt | 2 +- src/{main => common/src}/kotlin/Field.kt | 2 +- .../src}/kotlin/FieldFormat.kt | 2 +- src/{main => common/src}/kotlin/FieldMatch.kt | 2 +- src/{main => common/src}/kotlin/Op.kt | 2 +- src/{main => common/src}/kotlin/Parameter.kt | 2 +- .../src}/kotlin/ParameterName.kt | 2 +- .../src}/kotlin/ParameterType.kt | 4 +- .../src}/kotlin/query/Count.kt | 8 +- .../src}/kotlin/query/Definition.kt | 6 +- .../src}/kotlin/query/Delete.kt | 11 +- .../src}/kotlin/query/Document.kt | 8 +- .../src}/kotlin/query/Exists.kt | 6 +- src/{main => common/src}/kotlin/query/Find.kt | 10 +- .../src}/kotlin/query/Patch.kt | 14 +- .../src}/kotlin/query/Query.kt | 10 +- .../src}/kotlin/query/RemoveFields.kt | 8 +- .../src}/kotlin/query/Where.kt | 8 +- .../documents/common}/java/AutoIdTest.java | 8 +- .../common}/java/DocumentIndexTest.java | 6 +- .../common}/java/FieldMatchTest.java | 6 +- .../documents/common}/java/FieldTest.java | 6 +- .../documents/common}/java/OpTest.java | 6 +- .../common}/java/ParameterNameTest.java | 6 +- .../documents/common}/java/ParameterTest.java | 10 +- src/{ => common}/test/kotlin/AutoIdTest.kt | 4 +- .../test/kotlin/ComparisonTest.kt | 8 +- src/common/test/kotlin/ConfigurationTest.kt | 43 ++++++ src/{ => common}/test/kotlin/DialectTest.kt | 10 +- .../test/kotlin/DocumentIndexTest.kt | 4 +- .../test/kotlin/FieldMatchTest.kt | 4 +- src/{ => common}/test/kotlin/FieldTest.kt | 48 +++--- src/{ => common}/test/kotlin/OpTest.kt | 4 +- .../test/kotlin/ParameterNameTest.kt | 4 +- src/{ => common}/test/kotlin/ParameterTest.kt | 4 +- .../test/kotlin/query/CountTest.kt | 9 +- .../test/kotlin/query/DefinitionTest.kt | 12 +- .../test/kotlin/query/DeleteTest.kt | 12 +- .../test/kotlin/query/DocumentTest.kt | 9 +- .../test/kotlin/query/ExistsTest.kt | 9 +- .../test/kotlin/query/FindTest.kt | 12 +- .../test/kotlin/query/PatchTest.kt | 12 +- .../test/kotlin/query/QueryTest.kt | 9 +- .../test/kotlin/query/RemoveFieldsTest.kt | 8 +- .../test/kotlin/query/WhereTest.kt | 6 +- src/main/kotlin/Configuration.kt | 55 +------ src/main/kotlin/ConnectionExtensions.kt | 12 +- src/main/kotlin/Count.kt | 6 +- src/main/kotlin/Custom.kt | 48 +++--- src/main/kotlin/Definition.kt | 3 +- src/main/kotlin/Delete.kt | 6 +- src/main/kotlin/Document.kt | 21 +-- src/main/kotlin/Exists.kt | 6 +- src/main/kotlin/Find.kt | 8 +- src/main/kotlin/Parameters.kt | 9 +- src/main/kotlin/Patch.kt | 6 +- src/main/kotlin/RemoveFields.kt | 3 +- src/main/kotlin/Results.kt | 22 +-- src/main/kotlin/java/Custom.kt | 143 ++++++++++++++++++ src/main/kotlin/java/Results.kt | 91 +++++++++++ src/pom.xml | 92 +++++++++++ .../documents/java/ConfigurationTest.java | 2 + .../documents/java/ParametersTest.java | 21 +-- .../java/integration/postgresql/CountIT.java | 22 +++ .../documents/java/testDocs/JsonDocument.java | 5 +- src/test/kotlin/ConfigurationTest.kt | 32 ---- src/test/kotlin/ParametersTest.kt | 7 +- src/test/kotlin/integration/common/Count.kt | 1 + src/test/kotlin/integration/common/Custom.kt | 9 +- .../kotlin/integration/common/Definition.kt | 1 + src/test/kotlin/integration/common/Delete.kt | 1 + .../kotlin/integration/common/Document.kt | 1 + src/test/kotlin/integration/common/Exists.kt | 1 + src/test/kotlin/integration/common/Find.kt | 5 +- src/test/kotlin/integration/common/Patch.kt | 1 + .../kotlin/integration/common/RemoveFields.kt | 1 + .../kotlin/integration/postgresql/CountIT.kt | 2 +- .../kotlin/integration/postgresql/PgDB.kt | 2 + src/test/kotlin/integration/sqlite/CountIT.kt | 2 +- .../kotlin/integration/sqlite/DefinitionIT.kt | 2 +- .../kotlin/integration/sqlite/DeleteIT.kt | 2 +- .../kotlin/integration/sqlite/ExistsIT.kt | 2 +- src/test/kotlin/integration/sqlite/FindIT.kt | 2 +- src/test/kotlin/integration/sqlite/PatchIT.kt | 2 +- .../integration/sqlite/RemoveFieldsIT.kt | 2 +- .../kotlin/integration/sqlite/SQLiteDB.kt | 2 + 94 files changed, 903 insertions(+), 328 deletions(-) create mode 100644 src/common/pom.xml rename src/{main => common/src}/kotlin/AutoId.kt (90%) rename src/{main => common/src}/kotlin/Comparison.kt (97%) create mode 100644 src/common/src/kotlin/Configuration.kt rename src/{main => common/src}/kotlin/Dialect.kt (94%) rename src/{main => common/src}/kotlin/DocumentException.kt (76%) rename src/{main => common/src}/kotlin/DocumentIndex.kt (87%) rename src/{main => common/src}/kotlin/Field.kt (99%) rename src/{main => common/src}/kotlin/FieldFormat.kt (84%) rename src/{main => common/src}/kotlin/FieldMatch.kt (83%) rename src/{main => common/src}/kotlin/Op.kt (94%) rename src/{main => common/src}/kotlin/Parameter.kt (97%) rename src/{main => common/src}/kotlin/ParameterName.kt (91%) rename src/{main => common/src}/kotlin/ParameterType.kt (86%) rename src/{main => common/src}/kotlin/query/Count.kt (87%) rename src/{main => common/src}/kotlin/query/Definition.kt (96%) rename src/{main => common/src}/kotlin/query/Delete.kt (86%) rename src/{main => common/src}/kotlin/query/Document.kt (91%) rename src/{main => common/src}/kotlin/query/Exists.kt (93%) rename src/{main => common/src}/kotlin/query/Find.kt (87%) rename src/{main => common/src}/kotlin/query/Patch.kt (84%) rename src/{main => common/src}/kotlin/query/Query.kt (90%) rename src/{main => common/src}/kotlin/query/RemoveFields.kt (92%) rename src/{main => common/src}/kotlin/query/Where.kt (87%) rename src/{test/java/solutions/bitbadger/documents => common/test/java/solutions/bitbadger/documents/common}/java/AutoIdTest.java (97%) rename src/{test/java/solutions/bitbadger/documents => common/test/java/solutions/bitbadger/documents/common}/java/DocumentIndexTest.java (79%) rename src/{test/java/solutions/bitbadger/documents => common/test/java/solutions/bitbadger/documents/common}/java/FieldMatchTest.java (78%) rename src/{test/java/solutions/bitbadger/documents => common/test/java/solutions/bitbadger/documents/common}/java/FieldTest.java (99%) rename src/{test/java/solutions/bitbadger/documents => common/test/java/solutions/bitbadger/documents/common}/java/OpTest.java (94%) rename src/{test/java/solutions/bitbadger/documents => common/test/java/solutions/bitbadger/documents/common}/java/ParameterNameTest.java (88%) rename src/{test/java/solutions/bitbadger/documents => common/test/java/solutions/bitbadger/documents/common}/java/ParameterTest.java (82%) rename src/{ => common}/test/kotlin/AutoIdTest.kt (98%) rename src/{ => common}/test/kotlin/ComparisonTest.kt (96%) create mode 100644 src/common/test/kotlin/ConfigurationTest.kt rename src/{ => common}/test/kotlin/DialectTest.kt (77%) rename src/{ => common}/test/kotlin/DocumentIndexTest.kt (85%) rename src/{ => common}/test/kotlin/FieldMatchTest.kt (83%) rename src/{ => common}/test/kotlin/FieldTest.kt (95%) rename src/{ => common}/test/kotlin/OpTest.kt (96%) rename src/{ => common}/test/kotlin/ParameterNameTest.kt (91%) rename src/{ => common}/test/kotlin/ParameterTest.kt (93%) rename src/{ => common}/test/kotlin/query/CountTest.kt (88%) rename src/{ => common}/test/kotlin/query/DefinitionTest.kt (93%) rename src/{ => common}/test/kotlin/query/DeleteTest.kt (90%) rename src/{ => common}/test/kotlin/query/DocumentTest.kt (93%) rename src/{ => common}/test/kotlin/query/ExistsTest.kt (90%) rename src/{ => common}/test/kotlin/query/FindTest.kt (91%) rename src/{ => common}/test/kotlin/query/PatchTest.kt (90%) rename src/{ => common}/test/kotlin/query/QueryTest.kt (95%) rename src/{ => common}/test/kotlin/query/RemoveFieldsTest.kt (91%) rename src/{ => common}/test/kotlin/query/WhereTest.kt (97%) create mode 100644 src/main/kotlin/java/Custom.kt create mode 100644 src/main/kotlin/java/Results.kt create mode 100644 src/pom.xml create mode 100644 src/test/java/solutions/bitbadger/documents/java/integration/postgresql/CountIT.java diff --git a/documents.iml b/documents.iml index 7b09b3a..ab059ae 100644 --- a/documents.iml +++ b/documents.iml @@ -2,7 +2,9 @@ - + + + diff --git a/pom.xml b/pom.xml index 19963a3..3f86db7 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ compile - compile + process-sources compile diff --git a/src/common/pom.xml b/src/common/pom.xml new file mode 100644 index 0000000..cec57cb --- /dev/null +++ b/src/common/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + solutions.bitbadger.documents + common + 4.0.0-alpha1-SNAPSHOT + jar + + + solutions.bitbadger + documents + 4.0.0-alpha1-SNAPSHOT + + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (Common Library) + https://bitbadger.solutions/open-source/relational-documents/jvm/ + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + process-sources + + compile + + + + test-compile + test-compile + + test-compile + + + + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + + + maven-surefire-plugin + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + + integration-test + verify + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + MainKt + + + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + + + + diff --git a/src/main/kotlin/AutoId.kt b/src/common/src/kotlin/AutoId.kt similarity index 90% rename from src/main/kotlin/AutoId.kt rename to src/common/src/kotlin/AutoId.kt index 3e4c4c8..daa61c1 100644 --- a/src/main/kotlin/AutoId.kt +++ b/src/common/src/kotlin/AutoId.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import kotlin.jvm.Throws import kotlin.reflect.full.* @@ -10,10 +10,13 @@ import kotlin.reflect.jvm.isAccessible 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; @@ -24,7 +27,8 @@ enum class AutoId { * * @return A `UUID` string */ - @JvmStatic fun generateUUID(): String = + @JvmStatic + fun generateUUID(): String = java.util.UUID.randomUUID().toString().replace("-", "") /** @@ -33,7 +37,8 @@ enum class AutoId { * @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 = + @JvmStatic + fun generateRandomString(length: Int? = null): String = (length ?: Configuration.idStringLength).let { len -> kotlin.random.Random.nextBytes((len + 2) / 2) .joinToString("") { String.format("%02x", it) } @@ -50,7 +55,8 @@ enum class AutoId { * @throws DocumentException If bad input prevents the determination */ @Throws(DocumentException::class) - @JvmStatic fun needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean { + @JvmStatic + fun needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean { if (document == null) throw DocumentException("document cannot be null") if (strategy == DISABLED) return false diff --git a/src/main/kotlin/Comparison.kt b/src/common/src/kotlin/Comparison.kt similarity index 97% rename from src/main/kotlin/Comparison.kt rename to src/common/src/kotlin/Comparison.kt index f187882..5946aeb 100644 --- a/src/main/kotlin/Comparison.kt +++ b/src/common/src/kotlin/Comparison.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * Information required to generate a JSON field comparison diff --git a/src/common/src/kotlin/Configuration.kt b/src/common/src/kotlin/Configuration.kt new file mode 100644 index 0000000..683a386 --- /dev/null +++ b/src/common/src/kotlin/Configuration.kt @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.common + +import java.sql.Connection +import java.sql.DriverManager +import kotlin.jvm.Throws + +/** + * 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 */ + internal var dialectValue: Dialect? = null + + /** The connection string for the JDBC connection */ + @JvmStatic + var connectionString: String? = null + 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 IllegalArgumentException If the connection string is not set before calling this + */ + @JvmStatic + fun dbConn(): Connection { + if (connectionString == null) { + throw IllegalArgumentException("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/main/kotlin/Dialect.kt b/src/common/src/kotlin/Dialect.kt similarity index 94% rename from src/main/kotlin/Dialect.kt rename to src/common/src/kotlin/Dialect.kt index 986dddb..e950ba5 100644 --- a/src/main/kotlin/Dialect.kt +++ b/src/common/src/kotlin/Dialect.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * The SQL dialect to use when building queries diff --git a/src/main/kotlin/DocumentException.kt b/src/common/src/kotlin/DocumentException.kt similarity index 76% rename from src/main/kotlin/DocumentException.kt rename to src/common/src/kotlin/DocumentException.kt index bb0e0ff..c8ce19f 100644 --- a/src/main/kotlin/DocumentException.kt +++ b/src/common/src/kotlin/DocumentException.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * An exception caused by invalid operations in the document library @@ -6,4 +6,4 @@ package solutions.bitbadger.documents * @param message The message for the exception * @param cause The underlying exception (optional) */ -class DocumentException(message: String, cause: Throwable? = null) : Exception(message, cause) +class DocumentException(message: String, cause: Throwable? = null) : Exception(message, cause) \ No newline at end of file diff --git a/src/main/kotlin/DocumentIndex.kt b/src/common/src/kotlin/DocumentIndex.kt similarity index 87% rename from src/main/kotlin/DocumentIndex.kt rename to src/common/src/kotlin/DocumentIndex.kt index 3ce8743..45f1fac 100644 --- a/src/main/kotlin/DocumentIndex.kt +++ b/src/common/src/kotlin/DocumentIndex.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * The type of index to generate for the document diff --git a/src/main/kotlin/Field.kt b/src/common/src/kotlin/Field.kt similarity index 99% rename from src/main/kotlin/Field.kt rename to src/common/src/kotlin/Field.kt index d15a838..7b75702 100644 --- a/src/main/kotlin/Field.kt +++ b/src/common/src/kotlin/Field.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * A field and its comparison diff --git a/src/main/kotlin/FieldFormat.kt b/src/common/src/kotlin/FieldFormat.kt similarity index 84% rename from src/main/kotlin/FieldFormat.kt rename to src/common/src/kotlin/FieldFormat.kt index c804a87..02d4c20 100644 --- a/src/main/kotlin/FieldFormat.kt +++ b/src/common/src/kotlin/FieldFormat.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * The data format for a document field retrieval diff --git a/src/main/kotlin/FieldMatch.kt b/src/common/src/kotlin/FieldMatch.kt similarity index 83% rename from src/main/kotlin/FieldMatch.kt rename to src/common/src/kotlin/FieldMatch.kt index 3b1d3ff..e621dba 100644 --- a/src/main/kotlin/FieldMatch.kt +++ b/src/common/src/kotlin/FieldMatch.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * How fields should be matched in by-field queries diff --git a/src/main/kotlin/Op.kt b/src/common/src/kotlin/Op.kt similarity index 94% rename from src/main/kotlin/Op.kt rename to src/common/src/kotlin/Op.kt index 61723c9..dc93dfc 100644 --- a/src/main/kotlin/Op.kt +++ b/src/common/src/kotlin/Op.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * A comparison operator used for fields diff --git a/src/main/kotlin/Parameter.kt b/src/common/src/kotlin/Parameter.kt similarity index 97% rename from src/main/kotlin/Parameter.kt rename to src/common/src/kotlin/Parameter.kt index 1bd707b..9e63336 100644 --- a/src/main/kotlin/Parameter.kt +++ b/src/common/src/kotlin/Parameter.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import java.sql.PreparedStatement import java.sql.Types diff --git a/src/main/kotlin/ParameterName.kt b/src/common/src/kotlin/ParameterName.kt similarity index 91% rename from src/main/kotlin/ParameterName.kt rename to src/common/src/kotlin/ParameterName.kt index a090db0..566dca0 100644 --- a/src/main/kotlin/ParameterName.kt +++ b/src/common/src/kotlin/ParameterName.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * Derive parameter names; each instance wraps a counter to provide names for anonymous fields diff --git a/src/main/kotlin/ParameterType.kt b/src/common/src/kotlin/ParameterType.kt similarity index 86% rename from src/main/kotlin/ParameterType.kt rename to src/common/src/kotlin/ParameterType.kt index 77a88da..159bc14 100644 --- a/src/main/kotlin/ParameterType.kt +++ b/src/common/src/kotlin/ParameterType.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common /** * The types of parameters supported by the document library @@ -6,8 +6,10 @@ package solutions.bitbadger.documents 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/main/kotlin/query/Count.kt b/src/common/src/kotlin/query/Count.kt similarity index 87% rename from src/main/kotlin/query/Count.kt rename to src/common/src/kotlin/query/Count.kt index e3db117..28b726d 100644 --- a/src/main/kotlin/query/Count.kt +++ b/src/common/src/kotlin/query/Count.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.query.byFields as byFieldsBase; +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.query.byFields as byFieldsBase; /** * Functions to count documents diff --git a/src/main/kotlin/query/Definition.kt b/src/common/src/kotlin/query/Definition.kt similarity index 96% rename from src/main/kotlin/query/Definition.kt rename to src/common/src/kotlin/query/Definition.kt index e3c2ccd..c43ea3b 100644 --- a/src/main/kotlin/query/Definition.kt +++ b/src/common/src/kotlin/query/Definition.kt @@ -1,6 +1,6 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.* /** * Functions to create queries to define tables and indexes @@ -87,6 +87,6 @@ object Definition { 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})"; + return "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tableName USING GIN (data${indexType.sql})" } } diff --git a/src/main/kotlin/query/Delete.kt b/src/common/src/kotlin/query/Delete.kt similarity index 86% rename from src/main/kotlin/query/Delete.kt rename to src/common/src/kotlin/query/Delete.kt index 53882f5..e5fc511 100644 --- a/src/main/kotlin/query/Delete.kt +++ b/src/common/src/kotlin/query/Delete.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.query.byFields as byFieldsBase -import solutions.bitbadger.documents.query.byId as byIdBase +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.query.byFields as byFieldsBase +import solutions.bitbadger.documents.common.query.byId as byIdBase /** * Functions to delete documents @@ -14,7 +14,6 @@ object Delete { * Query to delete documents from a table * * @param tableName The table in which documents should be deleted (may include schema) - * @param where The WHERE clause for the delete statement * @return A query to delete documents */ private fun delete(tableName: String) = diff --git a/src/main/kotlin/query/Document.kt b/src/common/src/kotlin/query/Document.kt similarity index 91% rename from src/main/kotlin/query/Document.kt rename to src/common/src/kotlin/query/Document.kt index c60c986..6ee19b9 100644 --- a/src/main/kotlin/query/Document.kt +++ b/src/common/src/kotlin/query/Document.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.AutoId -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.common.AutoId +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect /** * Functions for document-level operations diff --git a/src/main/kotlin/query/Exists.kt b/src/common/src/kotlin/query/Exists.kt similarity index 93% rename from src/main/kotlin/query/Exists.kt rename to src/common/src/kotlin/query/Exists.kt index 54689d0..7e21c8e 100644 --- a/src/main/kotlin/query/Exists.kt +++ b/src/common/src/kotlin/query/Exists.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch /** * Functions to check for document existence diff --git a/src/main/kotlin/query/Find.kt b/src/common/src/kotlin/query/Find.kt similarity index 87% rename from src/main/kotlin/query/Find.kt rename to src/common/src/kotlin/query/Find.kt index 6d57232..c622485 100644 --- a/src/main/kotlin/query/Find.kt +++ b/src/common/src/kotlin/query/Find.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.query.byId as byIdBase -import solutions.bitbadger.documents.query.byFields as byFieldsBase +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.query.byId as byIdBase +import solutions.bitbadger.documents.common.query.byFields as byFieldsBase /** * Functions to retrieve documents diff --git a/src/main/kotlin/query/Patch.kt b/src/common/src/kotlin/query/Patch.kt similarity index 84% rename from src/main/kotlin/query/Patch.kt rename to src/common/src/kotlin/query/Patch.kt index dc1e699..3589154 100644 --- a/src/main/kotlin/query/Patch.kt +++ b/src/common/src/kotlin/query/Patch.kt @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.query.byFields as byFieldsBase -import solutions.bitbadger.documents.query.byId as byIdBase +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.query.byFields as byFieldsBase +import solutions.bitbadger.documents.common.query.byId as byIdBase /** * Functions to create queries to patch (partially update) JSON documents diff --git a/src/main/kotlin/query/Query.kt b/src/common/src/kotlin/query/Query.kt similarity index 90% rename from src/main/kotlin/query/Query.kt rename to src/common/src/kotlin/query/Query.kt index 39784bc..5d6cafd 100644 --- a/src/main/kotlin/query/Query.kt +++ b/src/common/src/kotlin/query/Query.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch // ~~~ TOP-LEVEL FUNCTIONS FOR THE QUERY PACKAGE ~~~ diff --git a/src/main/kotlin/query/RemoveFields.kt b/src/common/src/kotlin/query/RemoveFields.kt similarity index 92% rename from src/main/kotlin/query/RemoveFields.kt rename to src/common/src/kotlin/query/RemoveFields.kt index b3842c4..0190269 100644 --- a/src/main/kotlin/query/RemoveFields.kt +++ b/src/common/src/kotlin/query/RemoveFields.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.query.byFields as byFieldsBase -import solutions.bitbadger.documents.query.byId as byIdBase +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.byFields as byFieldsBase +import solutions.bitbadger.documents.common.query.byId as byIdBase /** * Functions to create queries to remove fields from documents diff --git a/src/main/kotlin/query/Where.kt b/src/common/src/kotlin/query/Where.kt similarity index 87% rename from src/main/kotlin/query/Where.kt rename to src/common/src/kotlin/query/Where.kt index d33f40b..7e49aa2 100644 --- a/src/main/kotlin/query/Where.kt +++ b/src/common/src/kotlin/query/Where.kt @@ -1,6 +1,10 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query -import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch /** * Functions to create `WHERE` clause fragments diff --git a/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java b/src/common/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java similarity index 97% rename from src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java rename to src/common/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java index 6375b24..1708332 100644 --- a/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java +++ b/src/common/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.AutoId; -import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.common.AutoId; +import solutions.bitbadger.documents.common.DocumentException; import solutions.bitbadger.documents.java.testDocs.*; import static org.junit.jupiter.api.Assertions.*; @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `AutoId` enum */ -@DisplayName("Java | AutoId") +@DisplayName("Java | Common | AutoId") final public class AutoIdTest { @Test diff --git a/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java b/src/common/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java similarity index 79% rename from src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java rename to src/common/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java index c54f9cb..95716e7 100644 --- a/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java +++ b/src/common/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java @@ -1,15 +1,15 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.DocumentIndex; +import solutions.bitbadger.documents.common.DocumentIndex; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `DocumentIndex` enum */ -@DisplayName("Java | DocumentIndex") +@DisplayName("Java | Common | DocumentIndex") final public class DocumentIndexTest { @Test diff --git a/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java b/src/common/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java similarity index 78% rename from src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java rename to src/common/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java index c974534..c8f02da 100644 --- a/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java +++ b/src/common/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java @@ -1,15 +1,15 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.FieldMatch; +import solutions.bitbadger.documents.common.FieldMatch; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `FieldMatch` enum */ -@DisplayName("Java | FieldMatch") +@DisplayName("Java | Common | FieldMatch") final public class FieldMatchTest { @Test diff --git a/src/test/java/solutions/bitbadger/documents/java/FieldTest.java b/src/common/test/java/solutions/bitbadger/documents/common/java/FieldTest.java similarity index 99% rename from src/test/java/solutions/bitbadger/documents/java/FieldTest.java rename to src/common/test/java/solutions/bitbadger/documents/common/java/FieldTest.java index 287b1ef..672ff2a 100644 --- a/src/test/java/solutions/bitbadger/documents/java/FieldTest.java +++ b/src/common/test/java/solutions/bitbadger/documents/common/java/FieldTest.java @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.common.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.common.*; import java.util.Collection; import java.util.List; @@ -14,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Field` class */ -@DisplayName("Java | Field") +@DisplayName("Java | Common | Field") final public class FieldTest { /** diff --git a/src/test/java/solutions/bitbadger/documents/java/OpTest.java b/src/common/test/java/solutions/bitbadger/documents/common/java/OpTest.java similarity index 94% rename from src/test/java/solutions/bitbadger/documents/java/OpTest.java rename to src/common/test/java/solutions/bitbadger/documents/common/java/OpTest.java index 3a319cf..2d70565 100644 --- a/src/test/java/solutions/bitbadger/documents/java/OpTest.java +++ b/src/common/test/java/solutions/bitbadger/documents/common/java/OpTest.java @@ -1,15 +1,15 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.Op; +import solutions.bitbadger.documents.common.Op; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `Op` enum */ -@DisplayName("Java | Op") +@DisplayName("Java | Common | Op") final public class OpTest { @Test diff --git a/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java b/src/common/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java similarity index 88% rename from src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java rename to src/common/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java index 15d5498..f8bdff9 100644 --- a/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java +++ b/src/common/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java @@ -1,15 +1,15 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.ParameterName; +import solutions.bitbadger.documents.common.ParameterName; import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `ParameterName` class */ -@DisplayName("Java | ParameterName") +@DisplayName("Java | Common | ParameterName") final public class ParameterNameTest { @Test diff --git a/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java b/src/common/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java similarity index 82% rename from src/test/java/solutions/bitbadger/documents/java/ParameterTest.java rename to src/common/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java index 2e6556b..aba9a4c 100644 --- a/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java +++ b/src/common/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java @@ -1,17 +1,17 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.common.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 solutions.bitbadger.documents.common.DocumentException; +import solutions.bitbadger.documents.common.Parameter; +import solutions.bitbadger.documents.common.ParameterType; import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Parameter` class */ -@DisplayName("Java | Parameter") +@DisplayName("Java | Common | Parameter") final public class ParameterTest { @Test diff --git a/src/test/kotlin/AutoIdTest.kt b/src/common/test/kotlin/AutoIdTest.kt similarity index 98% rename from src/test/kotlin/AutoIdTest.kt rename to src/common/test/kotlin/AutoIdTest.kt index 58cc9aa..540342e 100644 --- a/src/test/kotlin/AutoIdTest.kt +++ b/src/common/test/kotlin/AutoIdTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -11,7 +11,7 @@ import kotlin.test.assertTrue /** * Unit tests for the `AutoId` enum */ -@DisplayName("Kotlin | AutoId") +@DisplayName("Kotlin | Common | AutoId") class AutoIdTest { @Test diff --git a/src/test/kotlin/ComparisonTest.kt b/src/common/test/kotlin/ComparisonTest.kt similarity index 96% rename from src/test/kotlin/ComparisonTest.kt rename to src/common/test/kotlin/ComparisonTest.kt index af6d4d9..3cd54ad 100644 --- a/src/test/kotlin/ComparisonTest.kt +++ b/src/common/test/kotlin/ComparisonTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -21,7 +21,8 @@ class ComparisonBetweenTest { @Test @DisplayName("isNumeric is false with strings") fun isNumericFalseForStringsAndBetween() = - assertFalse(ComparisonBetween(Pair("eh", "zed")).isNumeric, + assertFalse( + ComparisonBetween(Pair("eh", "zed")).isNumeric, "A BETWEEN with strings should not be numeric") @Test @@ -32,7 +33,8 @@ class ComparisonBetweenTest { @Test @DisplayName("isNumeric is true with shorts") fun isNumericTrueForShortAndBetween() = - assertTrue(ComparisonBetween(Pair(0, 9)).isNumeric, + assertTrue( + ComparisonBetween(Pair(0, 9)).isNumeric, "A BETWEEN with shorts should be numeric") @Test diff --git a/src/common/test/kotlin/ConfigurationTest.kt b/src/common/test/kotlin/ConfigurationTest.kt new file mode 100644 index 0000000..cef0cb3 --- /dev/null +++ b/src/common/test/kotlin/ConfigurationTest.kt @@ -0,0 +1,43 @@ +package solutions.bitbadger.documents.common + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals + +/** + * Unit tests for the `Configuration` object + */ +@DisplayName("Kotlin | Common | 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/test/kotlin/DialectTest.kt b/src/common/test/kotlin/DialectTest.kt similarity index 77% rename from src/test/kotlin/DialectTest.kt rename to src/common/test/kotlin/DialectTest.kt index 29c6c51..b2fe4b2 100644 --- a/src/test/kotlin/DialectTest.kt +++ b/src/common/test/kotlin/DialectTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -9,19 +9,21 @@ import kotlin.test.assertTrue /** * Unit tests for the `Dialect` enum */ -@DisplayName("Dialect") +@DisplayName("Kotlin | Common | Dialect") class DialectTest { @Test @DisplayName("deriveFromConnectionString derives PostgreSQL correctly") fun derivesPostgres() = - assertEquals(Dialect.POSTGRESQL, Dialect.deriveFromConnectionString("jdbc:postgresql:db"), + assertEquals( + Dialect.POSTGRESQL, Dialect.deriveFromConnectionString("jdbc:postgresql:db"), "Dialect should have been PostgreSQL") @Test @DisplayName("deriveFromConnectionString derives PostgreSQL correctly") fun derivesSQLite() = - assertEquals(Dialect.SQLITE, Dialect.deriveFromConnectionString("jdbc:sqlite:memory"), + assertEquals( + Dialect.SQLITE, Dialect.deriveFromConnectionString("jdbc:sqlite:memory"), "Dialect should have been SQLite") @Test diff --git a/src/test/kotlin/DocumentIndexTest.kt b/src/common/test/kotlin/DocumentIndexTest.kt similarity index 85% rename from src/test/kotlin/DocumentIndexTest.kt rename to src/common/test/kotlin/DocumentIndexTest.kt index 9747cde..9840671 100644 --- a/src/test/kotlin/DocumentIndexTest.kt +++ b/src/common/test/kotlin/DocumentIndexTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `DocumentIndex` enum */ -@DisplayName("Kotlin | DocumentIndex") +@DisplayName("Kotlin | Common | DocumentIndex") class DocumentIndexTest { @Test diff --git a/src/test/kotlin/FieldMatchTest.kt b/src/common/test/kotlin/FieldMatchTest.kt similarity index 83% rename from src/test/kotlin/FieldMatchTest.kt rename to src/common/test/kotlin/FieldMatchTest.kt index ceb1725..2c3fdad 100644 --- a/src/test/kotlin/FieldMatchTest.kt +++ b/src/common/test/kotlin/FieldMatchTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `FieldMatch` enum */ -@DisplayName("Kotlin | FieldMatch") +@DisplayName("Kotlin | Common | FieldMatch") class FieldMatchTest { @Test diff --git a/src/test/kotlin/FieldTest.kt b/src/common/test/kotlin/FieldTest.kt similarity index 95% rename from src/test/kotlin/FieldTest.kt rename to src/common/test/kotlin/FieldTest.kt index 783979b..c32a2a9 100644 --- a/src/test/kotlin/FieldTest.kt +++ b/src/common/test/kotlin/FieldTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -11,7 +11,7 @@ import kotlin.test.assertNull /** * Unit tests for the `Field` class */ -@DisplayName("Kotlin | Field") +@DisplayName("Kotlin | Common | Field") class FieldTest { /** @@ -19,7 +19,7 @@ class FieldTest { */ @AfterEach fun cleanUp() { - Configuration.connectionString = null + Configuration.dialectValue = null } // ~~~ INSTANCE METHODS ~~~ @@ -122,7 +122,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for exists w/o qualifier (PostgreSQL)") fun toWhereExistsNoQualPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(), "Field WHERE clause not generated correctly") } @@ -130,7 +130,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for exists w/o qualifier (SQLite)") fun toWhereExistsNoQualSQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.SQLITE assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(), "Field WHERE clause not generated correctly") } @@ -138,7 +138,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for not-exists w/o qualifier (PostgreSQL)") fun toWhereNotExistsNoQualPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(), "Field WHERE clause not generated correctly") } @@ -146,7 +146,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for not-exists w/o qualifier (SQLite)") fun toWhereNotExistsNoQualSQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.SQLITE assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(), "Field WHERE clause not generated correctly") } @@ -154,7 +154,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range (PostgreSQL)") fun toWhereBetweenNoQualNumericPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").toWhere(), "Field WHERE clause not generated correctly") } @@ -162,7 +162,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range (PostgreSQL)") fun toWhereBetweenNoQualAlphaPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("data->>'city' BETWEEN :citymin AND :citymax", Field.between("city", "Atlanta", "Chicago", ":city").toWhere(), "Field WHERE clause not generated correctly") @@ -171,7 +171,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for BETWEEN w/o qualifier (SQLite)") fun toWhereBetweenNoQualSQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.SQLITE assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").toWhere(), "Field WHERE clause not generated correctly") } @@ -179,7 +179,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range (PostgreSQL)") fun toWhereBetweenQualNumericPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("(test.data->>'age')::numeric BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").withQualifier("test").toWhere(), "Field WHERE clause not generated correctly") @@ -188,7 +188,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range (PostgreSQL)") fun toWhereBetweenQualAlphaPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("unit.data->>'city' BETWEEN :citymin AND :citymax", Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere(), "Field WHERE clause not generated correctly") @@ -197,7 +197,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for BETWEEN w/ qualifier (SQLite)") fun toWhereBetweenQualSQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.SQLITE assertEquals("my.data->>'age' BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").withQualifier("my").toWhere(), "Field WHERE clause not generated correctly") @@ -206,7 +206,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for IN/any, numeric values (PostgreSQL)") fun toWhereAnyNumericPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL 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") } @@ -214,7 +214,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for IN/any, alphanumeric values (PostgreSQL)") fun toWhereAnyAlphaPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("data->>'test' IN (:city_0, :city_1)", Field.any("test", listOf("Atlanta", "Chicago"), ":city").toWhere(), "Field WHERE clause not generated correctly") @@ -223,7 +223,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for IN/any (SQLite)") fun toWhereAnySQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.SQLITE assertEquals("data->>'test' IN (:city_0, :city_1)", Field.any("test", listOf("Atlanta", "Chicago"), ":city").toWhere(), "Field WHERE clause not generated correctly") @@ -232,7 +232,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for inArray (PostgreSQL)") fun toWhereInArrayPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL 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") @@ -241,7 +241,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for inArray (SQLite)") fun toWhereInArraySQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.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") @@ -250,7 +250,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for others w/o qualifier (PostgreSQL)") fun toWhereOtherNoQualPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(), "Field WHERE clause not generated correctly") } @@ -258,7 +258,7 @@ class FieldTest { @Test @DisplayName("toWhere generates for others w/o qualifier (SQLite)") fun toWhereOtherNoQualSQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.SQLITE assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(), "Field WHERE clause not generated correctly") } @@ -266,7 +266,7 @@ class FieldTest { @Test @DisplayName("toWhere generates no-parameter w/ qualifier (PostgreSQL)") fun toWhereNoParamWithQualPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(), "Field WHERE clause not generated correctly") } @@ -274,7 +274,7 @@ class FieldTest { @Test @DisplayName("toWhere generates no-parameter w/ qualifier (SQLite)") fun toWhereNoParamWithQualSQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.SQLITE assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(), "Field WHERE clause not generated correctly") } @@ -282,7 +282,7 @@ class FieldTest { @Test @DisplayName("toWhere generates parameter w/ qualifier (PostgreSQL)") fun toWhereParamWithQualPostgres() { - Configuration.connectionString = ":postgresql:" + Configuration.dialectValue = Dialect.POSTGRESQL assertEquals("(q.data->>'le_field')::numeric <= :it", Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(), "Field WHERE clause not generated correctly") @@ -291,7 +291,7 @@ class FieldTest { @Test @DisplayName("toWhere generates parameter w/ qualifier (SQLite)") fun toWhereParamWithQualSQLite() { - Configuration.connectionString = ":sqlite:" + Configuration.dialectValue = Dialect.SQLITE assertEquals("q.data->>'le_field' <= :it", Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(), "Field WHERE clause not generated correctly") diff --git a/src/test/kotlin/OpTest.kt b/src/common/test/kotlin/OpTest.kt similarity index 96% rename from src/test/kotlin/OpTest.kt rename to src/common/test/kotlin/OpTest.kt index 5e6d6cd..6a272e9 100644 --- a/src/test/kotlin/OpTest.kt +++ b/src/common/test/kotlin/OpTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `Op` enum */ -@DisplayName("Kotlin | Op") +@DisplayName("Kotlin | Common | Op") class OpTest { @Test diff --git a/src/test/kotlin/ParameterNameTest.kt b/src/common/test/kotlin/ParameterNameTest.kt similarity index 91% rename from src/test/kotlin/ParameterNameTest.kt rename to src/common/test/kotlin/ParameterNameTest.kt index b08f6bb..93dd9c8 100644 --- a/src/test/kotlin/ParameterNameTest.kt +++ b/src/common/test/kotlin/ParameterNameTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `ParameterName` class */ -@DisplayName("Kotlin | ParameterName") +@DisplayName("Kotlin | Common | ParameterName") class ParameterNameTest { @Test diff --git a/src/test/kotlin/ParameterTest.kt b/src/common/test/kotlin/ParameterTest.kt similarity index 93% rename from src/test/kotlin/ParameterTest.kt rename to src/common/test/kotlin/ParameterTest.kt index 959ad25..ea8753a 100644 --- a/src/test/kotlin/ParameterTest.kt +++ b/src/common/test/kotlin/ParameterTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows @@ -9,7 +9,7 @@ import kotlin.test.assertNull /** * Unit tests for the `Parameter` class */ -@DisplayName("Kotlin | Parameter") +@DisplayName("Kotlin | Common | Parameter") class ParameterTest { @Test diff --git a/src/test/kotlin/query/CountTest.kt b/src/common/test/kotlin/query/CountTest.kt similarity index 88% rename from src/test/kotlin/query/CountTest.kt rename to src/common/test/kotlin/query/CountTest.kt index 3caab47..a664f21 100644 --- a/src/test/kotlin/query/CountTest.kt +++ b/src/common/test/kotlin/query/CountTest.kt @@ -1,16 +1,19 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.common.Field import kotlin.test.assertEquals /** * Unit tests for the `Count` object */ -@DisplayName("Count (Query)") +@DisplayName("Kotlin | Common | Query: Count") class CountTest { /** Test table name */ diff --git a/src/test/kotlin/query/DefinitionTest.kt b/src/common/test/kotlin/query/DefinitionTest.kt similarity index 93% rename from src/test/kotlin/query/DefinitionTest.kt rename to src/common/test/kotlin/query/DefinitionTest.kt index bcad33a..90343c9 100644 --- a/src/test/kotlin/query/DefinitionTest.kt +++ b/src/common/test/kotlin/query/DefinitionTest.kt @@ -1,19 +1,19 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.common.DocumentIndex import kotlin.test.assertEquals /** * Unit tests for the `Definition` object */ -@DisplayName("Definition (Query)") +@DisplayName("Kotlin | Common | Query: Definition") class DefinitionTest { /** Test table name */ diff --git a/src/test/kotlin/query/DeleteTest.kt b/src/common/test/kotlin/query/DeleteTest.kt similarity index 90% rename from src/test/kotlin/query/DeleteTest.kt rename to src/common/test/kotlin/query/DeleteTest.kt index 1fed573..54206d8 100644 --- a/src/test/kotlin/query/DeleteTest.kt +++ b/src/common/test/kotlin/query/DeleteTest.kt @@ -1,19 +1,19 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.common.Field import kotlin.test.assertEquals /** * Unit tests for the `Delete` object */ -@DisplayName("Delete (Query)") +@DisplayName("Kotlin | Common | Query: Delete") class DeleteTest { /** Test table name */ diff --git a/src/test/kotlin/query/DocumentTest.kt b/src/common/test/kotlin/query/DocumentTest.kt similarity index 93% rename from src/test/kotlin/query/DocumentTest.kt rename to src/common/test/kotlin/query/DocumentTest.kt index 5f67fde..1ee9277 100644 --- a/src/test/kotlin/query/DocumentTest.kt +++ b/src/common/test/kotlin/query/DocumentTest.kt @@ -1,17 +1,20 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.common.AutoId +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException import kotlin.test.assertEquals import kotlin.test.assertTrue /** * Unit tests for the `Document` object */ -@DisplayName("Document (Query)") +@DisplayName("Kotlin | Common | Query: Document") class DocumentTest { /** Test table name */ diff --git a/src/test/kotlin/query/ExistsTest.kt b/src/common/test/kotlin/query/ExistsTest.kt similarity index 90% rename from src/test/kotlin/query/ExistsTest.kt rename to src/common/test/kotlin/query/ExistsTest.kt index bbef7ce..a1ea2d9 100644 --- a/src/test/kotlin/query/ExistsTest.kt +++ b/src/common/test/kotlin/query/ExistsTest.kt @@ -1,16 +1,19 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.common.Field import kotlin.test.assertEquals /** * Unit tests for the `Exists` object */ -@DisplayName("Exists (Query)") +@DisplayName("Kotlin | Common | Query: Exists") class ExistsTest { /** Test table name */ diff --git a/src/test/kotlin/query/FindTest.kt b/src/common/test/kotlin/query/FindTest.kt similarity index 91% rename from src/test/kotlin/query/FindTest.kt rename to src/common/test/kotlin/query/FindTest.kt index 0526fa3..4b89e82 100644 --- a/src/test/kotlin/query/FindTest.kt +++ b/src/common/test/kotlin/query/FindTest.kt @@ -1,19 +1,19 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.common.Field import kotlin.test.assertEquals /** * Unit tests for the `Find` object */ -@DisplayName("Find (Query)") +@DisplayName("Kotlin | Common | Query: Find") class FindTest { /** Test table name */ diff --git a/src/test/kotlin/query/PatchTest.kt b/src/common/test/kotlin/query/PatchTest.kt similarity index 90% rename from src/test/kotlin/query/PatchTest.kt rename to src/common/test/kotlin/query/PatchTest.kt index 707f186..e7511b0 100644 --- a/src/test/kotlin/query/PatchTest.kt +++ b/src/common/test/kotlin/query/PatchTest.kt @@ -1,19 +1,19 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.common.Field import kotlin.test.assertEquals /** * Unit tests for the `Patch` object */ -@DisplayName("Patch (Query)") +@DisplayName("Kotlin | Common | Query: Patch") class PatchTest { /** Test table name */ diff --git a/src/test/kotlin/query/QueryTest.kt b/src/common/test/kotlin/query/QueryTest.kt similarity index 95% rename from src/test/kotlin/query/QueryTest.kt rename to src/common/test/kotlin/query/QueryTest.kt index d5344f3..82e5285 100644 --- a/src/test/kotlin/query/QueryTest.kt +++ b/src/common/test/kotlin/query/QueryTest.kt @@ -1,15 +1,18 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch import kotlin.test.assertEquals /** * Unit tests for the top-level query functions */ -@DisplayName("Query") +@DisplayName("Kotlin | Common | Query") class QueryTest { /** diff --git a/src/test/kotlin/query/RemoveFieldsTest.kt b/src/common/test/kotlin/query/RemoveFieldsTest.kt similarity index 91% rename from src/test/kotlin/query/RemoveFieldsTest.kt rename to src/common/test/kotlin/query/RemoveFieldsTest.kt index e06ff44..498a435 100644 --- a/src/test/kotlin/query/RemoveFieldsTest.kt +++ b/src/common/test/kotlin/query/RemoveFieldsTest.kt @@ -1,16 +1,20 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.common.Field import kotlin.test.assertEquals /** * Unit tests for the `RemoveFields` object */ -@DisplayName("RemoveFields (Query)") +@DisplayName("Kotlin | Common | Query: RemoveFields") class RemoveFieldsTest { /** Test table name */ diff --git a/src/test/kotlin/query/WhereTest.kt b/src/common/test/kotlin/query/WhereTest.kt similarity index 97% rename from src/test/kotlin/query/WhereTest.kt rename to src/common/test/kotlin/query/WhereTest.kt index c8ebfbc..aeed546 100644 --- a/src/test/kotlin/query/WhereTest.kt +++ b/src/common/test/kotlin/query/WhereTest.kt @@ -1,16 +1,16 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.common.query 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.common.* import kotlin.test.assertEquals /** * Unit tests for the `Where` object */ -@DisplayName("Where (Query)") +@DisplayName("Kotlin | Common | Query: Where") class WhereTest { /** diff --git a/src/main/kotlin/Configuration.kt b/src/main/kotlin/Configuration.kt index 7313eea..705a1d9 100644 --- a/src/main/kotlin/Configuration.kt +++ b/src/main/kotlin/Configuration.kt @@ -1,6 +1,9 @@ package solutions.bitbadger.documents import kotlinx.serialization.json.Json +import solutions.bitbadger.documents.common.AutoId +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException import java.sql.Connection import java.sql.DriverManager import kotlin.jvm.Throws @@ -20,58 +23,8 @@ object Configuration { coerceInputValues = true } - /** 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 JSON serializer to use for documents */ + @JvmStatic var serializer: DocumentSerializer = DocumentSerializerKotlin() - /** The derived dialect value from the connection string */ - internal var dialectValue: Dialect? = null - - /** The connection string for the JDBC connection */ - @JvmStatic - var connectionString: String? = null - 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 IllegalArgumentException If the connection string is not set before calling this - */ - @JvmStatic - fun dbConn(): Connection { - if (connectionString == null) { - throw IllegalArgumentException("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/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 841fb31..c63406b 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -1,5 +1,9 @@ package solutions.bitbadger.documents +import solutions.bitbadger.documents.common.DocumentIndex +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter import java.sql.Connection import java.sql.ResultSet @@ -14,7 +18,7 @@ import java.sql.ResultSet * @return A list of results for the given query */ inline fun Connection.customList( - query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), noinline mapFunc: (ResultSet, Class) -> TDoc ) = Custom.list(query, parameters, this, mapFunc) /** @@ -26,7 +30,7 @@ inline fun Connection.customList( * @return The document if one matches the query, `null` otherwise */ inline fun Connection.customSingle( - query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), noinline mapFunc: (ResultSet, Class) -> TDoc ) = Custom.single(query, parameters, this, mapFunc) /** @@ -46,10 +50,10 @@ fun Connection.customNonQuery(query: String, parameters: Collection * @param mapFunc The mapping function between the document and the domain item * @return The scalar value from the query */ -inline fun Connection.customScalar( +inline fun Connection.customScalar( query: String, parameters: Collection> = listOf(), - mapFunc: (ResultSet) -> T & Any + noinline mapFunc: (ResultSet, Class) -> T ) = Custom.scalar(query, parameters, this, mapFunc) // ~~~ DEFINITION QUERIES ~~~ diff --git a/src/main/kotlin/Count.kt b/src/main/kotlin/Count.kt index 3d4d26f..b865af0 100644 --- a/src/main/kotlin/Count.kt +++ b/src/main/kotlin/Count.kt @@ -1,6 +1,10 @@ package solutions.bitbadger.documents -import solutions.bitbadger.documents.query.Count +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.query.Count import java.sql.Connection /** diff --git a/src/main/kotlin/Custom.kt b/src/main/kotlin/Custom.kt index 1902aec..e3e8ca7 100644 --- a/src/main/kotlin/Custom.kt +++ b/src/main/kotlin/Custom.kt @@ -1,5 +1,7 @@ package solutions.bitbadger.documents +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.java.Custom import java.sql.Connection import java.sql.ResultSet @@ -13,13 +15,17 @@ object Custom { * * @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 */ inline fun list( - query: String, parameters: Collection> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc - ) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, mapFunc) } + query: String, + parameters: Collection> = listOf(), + conn: Connection, + noinline mapFunc: (ResultSet, Class) -> TDoc + ) = Custom.list(query, parameters, TDoc::class.java, conn, mapFunc) /** * Execute a query that returns a list of results (creates connection) @@ -30,7 +36,9 @@ object Custom { * @return A list of results for the given query */ inline fun list( - query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc + query: String, + parameters: Collection> = listOf(), + noinline mapFunc: (ResultSet, Class) -> TDoc ) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) } /** @@ -43,8 +51,11 @@ object Custom { * @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() + query: String, + parameters: Collection> = listOf(), + conn: Connection, + noinline mapFunc: (ResultSet, Class) -> TDoc + ) = Custom.single(query, parameters, TDoc::class.java, conn, mapFunc) /** * Execute a query that returns one or no results @@ -55,7 +66,9 @@ object Custom { * @return The document if one matches the query, `null` otherwise */ inline fun single( - query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc + query: String, + parameters: Collection> = listOf(), + noinline mapFunc: (ResultSet, Class) -> TDoc ) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) } /** @@ -65,9 +78,8 @@ object Custom { * @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) { - Parameters.apply(conn, query, parameters).use { it.executeUpdate() } - } + fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) = + Custom.nonQuery(query, parameters, conn) /** * Execute a query that returns no results @@ -87,14 +99,12 @@ object Custom { * @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 & Any - ) = Parameters.apply(conn, query, parameters).use { stmt -> - stmt.executeQuery().use { rs -> - rs.next() - mapFunc(rs) - } - } + inline fun scalar( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + noinline mapFunc: (ResultSet, Class) -> T + ) = Custom.scalar(query, parameters, T::class.java, conn, mapFunc) /** * Execute a query that returns a scalar result @@ -104,7 +114,7 @@ object Custom { * @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 & Any + inline fun scalar( + query: String, parameters: Collection> = listOf(), noinline mapFunc: (ResultSet, Class) -> T ) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) } } diff --git a/src/main/kotlin/Definition.kt b/src/main/kotlin/Definition.kt index d61712c..424d742 100644 --- a/src/main/kotlin/Definition.kt +++ b/src/main/kotlin/Definition.kt @@ -1,7 +1,8 @@ package solutions.bitbadger.documents +import solutions.bitbadger.documents.common.DocumentIndex import java.sql.Connection -import solutions.bitbadger.documents.query.Definition +import solutions.bitbadger.documents.common.query.Definition /** * Functions to define tables and indexes diff --git a/src/main/kotlin/Delete.kt b/src/main/kotlin/Delete.kt index e3ef705..eca0352 100644 --- a/src/main/kotlin/Delete.kt +++ b/src/main/kotlin/Delete.kt @@ -1,6 +1,10 @@ package solutions.bitbadger.documents -import solutions.bitbadger.documents.query.Delete +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.query.Delete import java.sql.Connection /** diff --git a/src/main/kotlin/Document.kt b/src/main/kotlin/Document.kt index da06089..6984e5d 100644 --- a/src/main/kotlin/Document.kt +++ b/src/main/kotlin/Document.kt @@ -1,9 +1,12 @@ package solutions.bitbadger.documents +import solutions.bitbadger.documents.common.AutoId +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.Field import java.sql.Connection -import solutions.bitbadger.documents.query.Document -import solutions.bitbadger.documents.query.Where -import solutions.bitbadger.documents.query.statementWhere +import solutions.bitbadger.documents.common.query.Document +import solutions.bitbadger.documents.common.query.Where +import solutions.bitbadger.documents.common.query.statementWhere /** * Functions for manipulating documents @@ -18,7 +21,7 @@ object Document { * @param conn The connection on which the query should be executed */ @JvmStatic - inline fun insert(tableName: String, document: TDoc, conn: Connection) { + fun insert(tableName: String, document: TDoc, conn: Connection) { val strategy = Configuration.autoIdStrategy val query = if (strategy == AutoId.DISABLED) { Document.insert(tableName) @@ -60,7 +63,7 @@ object Document { * @param document The document to be inserted */ @JvmStatic - inline fun insert(tableName: String, document: TDoc) = + fun insert(tableName: String, document: TDoc) = Configuration.dbConn().use { insert(tableName, document, it) } /** @@ -71,7 +74,7 @@ object Document { * @param conn The connection on which the query should be executed */ @JvmStatic - inline fun save(tableName: String, document: TDoc, conn: Connection) = + fun save(tableName: String, document: TDoc, conn: Connection) = conn.customNonQuery(Document.save(tableName), listOf(Parameters.json(":data", document))) /** @@ -81,7 +84,7 @@ object Document { * @param document The document to be saved */ @JvmStatic - inline fun save(tableName: String, document: TDoc) = + fun save(tableName: String, document: TDoc) = Configuration.dbConn().use { save(tableName, document, it) } /** @@ -93,7 +96,7 @@ object Document { * @param conn The connection on which the query should be executed */ @JvmStatic - inline fun update(tableName: String, docId: TKey, document: TDoc, conn: Connection) = + fun update(tableName: String, docId: TKey, document: TDoc, conn: Connection) = conn.customNonQuery( statementWhere(Document.update(tableName), Where.byId(":id", docId)), Parameters.addFields( @@ -110,6 +113,6 @@ object Document { * @param document The document to be replaced */ @JvmStatic - inline fun update(tableName: String, docId: TKey, document: TDoc) = + fun update(tableName: String, docId: TKey, document: TDoc) = Configuration.dbConn().use { update(tableName, docId, document, it) } } diff --git a/src/main/kotlin/Exists.kt b/src/main/kotlin/Exists.kt index 0b1e1ce..4b0072a 100644 --- a/src/main/kotlin/Exists.kt +++ b/src/main/kotlin/Exists.kt @@ -1,6 +1,10 @@ package solutions.bitbadger.documents -import solutions.bitbadger.documents.query.Exists +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.query.Exists import java.sql.Connection /** diff --git a/src/main/kotlin/Find.kt b/src/main/kotlin/Find.kt index 898d910..6135145 100644 --- a/src/main/kotlin/Find.kt +++ b/src/main/kotlin/Find.kt @@ -1,8 +1,12 @@ package solutions.bitbadger.documents +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType import java.sql.Connection -import solutions.bitbadger.documents.query.Find -import solutions.bitbadger.documents.query.orderBy +import solutions.bitbadger.documents.common.query.Find +import solutions.bitbadger.documents.common.query.orderBy /** * Functions to find and retrieve documents diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 3bf15b2..f31a5db 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -1,5 +1,7 @@ package solutions.bitbadger.documents +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.ParameterName import java.sql.Connection import java.sql.PreparedStatement import java.sql.SQLException @@ -37,8 +39,9 @@ object Parameters { * @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, Configuration.json.encodeToString(value)) + @JvmStatic + fun json(name: String, value: T) = + Parameter(name, ParameterType.JSON, Configuration.serializer.serialize(value)) /** * Add field parameters to the given set of parameters @@ -110,7 +113,9 @@ object Parameters { * @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> = diff --git a/src/main/kotlin/Patch.kt b/src/main/kotlin/Patch.kt index ea3128c..4b23b78 100644 --- a/src/main/kotlin/Patch.kt +++ b/src/main/kotlin/Patch.kt @@ -1,6 +1,10 @@ package solutions.bitbadger.documents -import solutions.bitbadger.documents.query.Patch +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.query.Patch import java.sql.Connection /** diff --git a/src/main/kotlin/RemoveFields.kt b/src/main/kotlin/RemoveFields.kt index 7ce2c21..486351a 100644 --- a/src/main/kotlin/RemoveFields.kt +++ b/src/main/kotlin/RemoveFields.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents -import solutions.bitbadger.documents.query.RemoveFields +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.RemoveFields import java.sql.Connection /** diff --git a/src/main/kotlin/Results.kt b/src/main/kotlin/Results.kt index 1ab07fc..e487f0a 100644 --- a/src/main/kotlin/Results.kt +++ b/src/main/kotlin/Results.kt @@ -1,5 +1,8 @@ package solutions.bitbadger.documents +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.java.Results import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.SQLException @@ -16,17 +19,18 @@ object Results { * @param rs A `ResultSet` set to the row with the document to be constructed * @return The constructed domain item */ - inline fun fromDocument(field: String, rs: ResultSet) = - Configuration.json.decodeFromString(rs.getString(field)) + inline fun fromDocument(field: String): (ResultSet, Class) -> TDoc = + { rs, _ -> Results.fromDocument(field, rs, TDoc::class.java) } /** * 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 */ - inline fun fromData(rs: ResultSet) = - fromDocument("data", rs) + inline fun fromData(rs: ResultSet, clazz: Class = TDoc::class.java) = + Results.fromDocument("data", rs, TDoc::class.java) /** * Create a list of items for the results of the given command, using the specified mapping function @@ -36,7 +40,7 @@ object Results { * @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) = + inline fun toCustomList(stmt: PreparedStatement, mapFunc: (ResultSet) -> TDoc) = try { stmt.executeQuery().use { val results = mutableListOf() @@ -55,10 +59,10 @@ object Results { * @param rs A `ResultSet` set to the row with the count to retrieve * @return The count from the row */ - fun toCount(rs: ResultSet) = + fun toCount(rs: ResultSet, clazz: Class = Long::class.java) = when (Configuration.dialect()) { Dialect.POSTGRESQL -> rs.getInt("it").toLong() - Dialect.SQLITE -> rs.getLong("it") + Dialect.SQLITE -> rs.getLong("it") } /** @@ -67,9 +71,9 @@ object Results { * @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) = + fun toExists(rs: ResultSet, clazz: Class = Boolean::class.java) = when (Configuration.dialect()) { Dialect.POSTGRESQL -> rs.getBoolean("it") - Dialect.SQLITE -> toCount(rs) > 0L + Dialect.SQLITE -> toCount(rs) > 0L } } diff --git a/src/main/kotlin/java/Custom.kt b/src/main/kotlin/java/Custom.kt new file mode 100644 index 0000000..7d5ee67 --- /dev/null +++ b/src/main/kotlin/java/Custom.kt @@ -0,0 +1,143 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.Parameters +import java.sql.Connection +import java.sql.ResultSet + +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 + */ + @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 + */ + @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 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 The document if one matches the query, `null` otherwise + */ + @JvmStatic + fun single( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + conn: Connection, + mapFunc: (ResultSet, Class) -> TDoc + ) = 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 + */ + @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 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 + */ + @JvmStatic + fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) { + Parameters.apply(conn, query, parameters).use { it.executeUpdate() } + } + + /** + * Execute a query that returns no results + * + * @param query The query to retrieve the results + * @param parameters Parameters to use for the query + */ + @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 + */ + @JvmStatic + fun scalar( + query: String, + parameters: Collection> = listOf(), + clazz: Class, + conn: Connection, + mapFunc: (ResultSet, Class) -> T + ) = Parameters.apply(conn, query, parameters).use { stmt -> + stmt.executeQuery().use { rs -> + rs.next() + mapFunc(rs, clazz) + } + } + + /** + * 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 + */ + 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/main/kotlin/java/Results.kt b/src/main/kotlin/java/Results.kt new file mode 100644 index 0000000..9b6f9eb --- /dev/null +++ b/src/main/kotlin/java/Results.kt @@ -0,0 +1,91 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.SQLException +import kotlin.jvm.Throws + +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) = + Configuration.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 + */ + @Throws(DocumentException::class) + @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 + * @return The count from the row + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + 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 + * @throws DocumentException If the dialect has not been set + */ + @Throws(DocumentException::class) + @JvmStatic + fun toExists(rs: ResultSet) = + when (Configuration.dialect()) { + Dialect.POSTGRESQL -> rs.getBoolean("it") + Dialect.SQLITE -> toCount(rs) > 0L + } +} diff --git a/src/pom.xml b/src/pom.xml new file mode 100644 index 0000000..928441f --- /dev/null +++ b/src/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + solutions.bitbadger + documents + 4.0.0-alpha1-SNAPSHOT + pom + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite + https://bitbadger.solutions/open-source/solutions.bitbadger.documents + + + + 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 + 11 + 2.1.0 + 1.8.0 + + + + common + + + + + org.junit.jupiter + junit-jupiter + 5.11.1 + test + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.jetbrains.kotlinx + kotlinx-serialization-json + ${serialization.version} + + + org.xerial + sqlite-jdbc + 3.46.1.2 + test + + + org.postgresql + postgresql + 42.7.5 + test + + + + \ No newline at end of file diff --git a/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java b/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java index ed6a71a..c24b49c 100644 --- a/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java +++ b/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java @@ -3,6 +3,8 @@ package solutions.bitbadger.documents.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import solutions.bitbadger.documents.*; +import solutions.bitbadger.documents.common.Dialect; +import solutions.bitbadger.documents.common.DocumentException; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/solutions/bitbadger/documents/java/ParametersTest.java b/src/test/java/solutions/bitbadger/documents/java/ParametersTest.java index 6c011d1..c3b98c8 100644 --- a/src/test/java/solutions/bitbadger/documents/java/ParametersTest.java +++ b/src/test/java/solutions/bitbadger/documents/java/ParametersTest.java @@ -4,8 +4,11 @@ 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.common.DocumentException; +import solutions.bitbadger.documents.common.Field; +import solutions.bitbadger.documents.common.Parameter; +import solutions.bitbadger.documents.common.ParameterType; -import java.util.Collection; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -63,9 +66,9 @@ final public class ParametersTest { @Test @DisplayName("fieldNames generates a single parameter (PostgreSQL)") - public void fieldNamesSinglePostgres() { + public void fieldNamesSinglePostgres() throws DocumentException { Configuration.setConnectionString(":postgresql:"); - Parameter[] nameParams = Parameters.fieldNames(List.of("test")).toArray(new Parameter[] { }); + 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"); @@ -74,10 +77,10 @@ final public class ParametersTest { @Test @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") - public void fieldNamesMultiplePostgres() { + public void fieldNamesMultiplePostgres() throws DocumentException { Configuration.setConnectionString(":postgresql:"); Parameter[] nameParams = Parameters.fieldNames(List.of("test", "this", "today")) - .toArray(new Parameter[] { }); + .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"); @@ -86,9 +89,9 @@ final public class ParametersTest { @Test @DisplayName("fieldNames generates a single parameter (SQLite)") - public void fieldNamesSingleSQLite() { + public void fieldNamesSingleSQLite() throws DocumentException { Configuration.setConnectionString(":sqlite:"); - Parameter[] nameParams = Parameters.fieldNames(List.of("test")).toArray(new Parameter[] { }); + 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"); @@ -97,10 +100,10 @@ final public class ParametersTest { @Test @DisplayName("fieldNames generates multiple parameters (SQLite)") - public void fieldNamesMultipleSQLite() { + public void fieldNamesMultipleSQLite() throws DocumentException { Configuration.setConnectionString(":sqlite:"); Parameter[] nameParams = Parameters.fieldNames(List.of("test", "this", "today")) - .toArray(new Parameter[] { }); + .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"); diff --git a/src/test/java/solutions/bitbadger/documents/java/integration/postgresql/CountIT.java b/src/test/java/solutions/bitbadger/documents/java/integration/postgresql/CountIT.java new file mode 100644 index 0000000..32fb88d --- /dev/null +++ b/src/test/java/solutions/bitbadger/documents/java/integration/postgresql/CountIT.java @@ -0,0 +1,22 @@ +package solutions.bitbadger.documents.java.integration.postgresql; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.integration.postgresql.PgDB; +import solutions.bitbadger.documents.java.integration.common.Count; + +/** + * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("Java | PostgreSQL: Count") +public class CountIT { + + @Test + @DisplayName("all counts all documents") + public void all() { + try (PgDB db = new PgDB()) { + Count.all(db); + } + } + +} diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java b/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java index be54944..7bd9906 100644 --- a/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java +++ b/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java @@ -1,5 +1,6 @@ package solutions.bitbadger.documents.java.testDocs; +import kotlinx.serialization.Serializable; import solutions.bitbadger.documents.Document; import solutions.bitbadger.documents.integration.ThrowawayDatabase; @@ -7,6 +8,7 @@ import java.util.List; import static solutions.bitbadger.documents.integration.TypesKt.TEST_TABLE; +@Serializable public class JsonDocument { private String id; @@ -70,8 +72,7 @@ public class JsonDocument { public static void load(ThrowawayDatabase db, String tableName) { for (JsonDocument doc : testDocuments) { - // TODO: inline reified generics cannot be called from Java :( - // Document.insert(tableName, doc, db.getConn()); + Document.insert(tableName, doc, db.getConn()); } } diff --git a/src/test/kotlin/ConfigurationTest.kt b/src/test/kotlin/ConfigurationTest.kt index 22754a5..d3689f7 100644 --- a/src/test/kotlin/ConfigurationTest.kt +++ b/src/test/kotlin/ConfigurationTest.kt @@ -2,8 +2,6 @@ package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -20,34 +18,4 @@ class ConfigurationTest { assertFalse(Configuration.json.configuration.explicitNulls, "Explicit Nulls should not have been set") assertTrue(Configuration.json.configuration.coerceInputValues, "Coerce Input Values should have been set") } - - @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/test/kotlin/ParametersTest.kt b/src/test/kotlin/ParametersTest.kt index 686ed67..566c267 100644 --- a/src/test/kotlin/ParametersTest.kt +++ b/src/test/kotlin/ParametersTest.kt @@ -3,6 +3,7 @@ package solutions.bitbadger.documents import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.common.* import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotSame @@ -51,8 +52,10 @@ class ParametersTest { @Test @DisplayName("replaceNamesInQuery replaces successfully") fun replaceNamesInQuery() { - val parameters = listOf(Parameter(":data", ParameterType.JSON, "{}"), - Parameter(":data_ext", ParameterType.STRING, "")) + 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") diff --git a/src/test/kotlin/integration/common/Count.kt b/src/test/kotlin/integration/common/Count.kt index f6cad25..e360977 100644 --- a/src/test/kotlin/integration/common/Count.kt +++ b/src/test/kotlin/integration/common/Count.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.integration.JsonDocument import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase diff --git a/src/test/kotlin/integration/common/Custom.kt b/src/test/kotlin/integration/common/Custom.kt index 8170e94..f8361c6 100644 --- a/src/test/kotlin/integration/common/Custom.kt +++ b/src/test/kotlin/integration/common/Custom.kt @@ -1,12 +1,15 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType import solutions.bitbadger.documents.integration.JsonDocument import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase -import solutions.bitbadger.documents.query.Count -import solutions.bitbadger.documents.query.Delete -import solutions.bitbadger.documents.query.Find +import solutions.bitbadger.documents.common.query.Count +import solutions.bitbadger.documents.common.query.Delete +import solutions.bitbadger.documents.common.query.Find import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull diff --git a/src/test/kotlin/integration/common/Definition.kt b/src/test/kotlin/integration/common/Definition.kt index d8f763d..e667ce8 100644 --- a/src/test/kotlin/integration/common/Definition.kt +++ b/src/test/kotlin/integration/common/Definition.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.DocumentIndex import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase import kotlin.test.assertFalse diff --git a/src/test/kotlin/integration/common/Delete.kt b/src/test/kotlin/integration/common/Delete.kt index 4a929b5..1e69426 100644 --- a/src/test/kotlin/integration/common/Delete.kt +++ b/src/test/kotlin/integration/common/Delete.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.integration.JsonDocument import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase diff --git a/src/test/kotlin/integration/common/Document.kt b/src/test/kotlin/integration/common/Document.kt index 9396776..bb3c41b 100644 --- a/src/test/kotlin/integration/common/Document.kt +++ b/src/test/kotlin/integration/common/Document.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.integration.* import kotlin.test.* diff --git a/src/test/kotlin/integration/common/Exists.kt b/src/test/kotlin/integration/common/Exists.kt index 05ba58e..2dc51b8 100644 --- a/src/test/kotlin/integration/common/Exists.kt +++ b/src/test/kotlin/integration/common/Exists.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.integration.JsonDocument import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase diff --git a/src/test/kotlin/integration/common/Find.kt b/src/test/kotlin/integration/common/Find.kt index 48cd857..839991d 100644 --- a/src/test/kotlin/integration/common/Find.kt +++ b/src/test/kotlin/integration/common/Find.kt @@ -1,6 +1,8 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch import solutions.bitbadger.documents.integration.* import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -209,7 +211,8 @@ object Find { 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"))) + 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") } diff --git a/src/test/kotlin/integration/common/Patch.kt b/src/test/kotlin/integration/common/Patch.kt index 77fc60a..37d7315 100644 --- a/src/test/kotlin/integration/common/Patch.kt +++ b/src/test/kotlin/integration/common/Patch.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.integration.JsonDocument import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase diff --git a/src/test/kotlin/integration/common/RemoveFields.kt b/src/test/kotlin/integration/common/RemoveFields.kt index cbfeeaf..e8381a1 100644 --- a/src/test/kotlin/integration/common/RemoveFields.kt +++ b/src/test/kotlin/integration/common/RemoveFields.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents.integration.common import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.integration.JsonDocument import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase diff --git a/src/test/kotlin/integration/postgresql/CountIT.kt b/src/test/kotlin/integration/postgresql/CountIT.kt index 9709cf9..9b4dc33 100644 --- a/src/test/kotlin/integration/postgresql/CountIT.kt +++ b/src/test/kotlin/integration/postgresql/CountIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("PostgreSQL - Count") +@DisplayName("Kotlin | PostgreSQL: Count") class CountIT { @Test diff --git a/src/test/kotlin/integration/postgresql/PgDB.kt b/src/test/kotlin/integration/postgresql/PgDB.kt index ae517d3..c72af32 100644 --- a/src/test/kotlin/integration/postgresql/PgDB.kt +++ b/src/test/kotlin/integration/postgresql/PgDB.kt @@ -1,6 +1,8 @@ package solutions.bitbadger.documents.integration.postgresql import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase diff --git a/src/test/kotlin/integration/sqlite/CountIT.kt b/src/test/kotlin/integration/sqlite/CountIT.kt index 0ebdeb7..7b08ddf 100644 --- a/src/test/kotlin/integration/sqlite/CountIT.kt +++ b/src/test/kotlin/integration/sqlite/CountIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.DocumentException import solutions.bitbadger.documents.integration.common.Count import kotlin.test.Test diff --git a/src/test/kotlin/integration/sqlite/DefinitionIT.kt b/src/test/kotlin/integration/sqlite/DefinitionIT.kt index 8e1a6cd..0cd15fa 100644 --- a/src/test/kotlin/integration/sqlite/DefinitionIT.kt +++ b/src/test/kotlin/integration/sqlite/DefinitionIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.DocumentException import solutions.bitbadger.documents.integration.common.Definition import kotlin.test.Test diff --git a/src/test/kotlin/integration/sqlite/DeleteIT.kt b/src/test/kotlin/integration/sqlite/DeleteIT.kt index b623e24..40c100e 100644 --- a/src/test/kotlin/integration/sqlite/DeleteIT.kt +++ b/src/test/kotlin/integration/sqlite/DeleteIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.DocumentException import solutions.bitbadger.documents.integration.common.Delete import kotlin.test.Test diff --git a/src/test/kotlin/integration/sqlite/ExistsIT.kt b/src/test/kotlin/integration/sqlite/ExistsIT.kt index d9c0c6d..42bf633 100644 --- a/src/test/kotlin/integration/sqlite/ExistsIT.kt +++ b/src/test/kotlin/integration/sqlite/ExistsIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.DocumentException import solutions.bitbadger.documents.integration.common.Exists import kotlin.test.Test diff --git a/src/test/kotlin/integration/sqlite/FindIT.kt b/src/test/kotlin/integration/sqlite/FindIT.kt index f0f05bd..6750c03 100644 --- a/src/test/kotlin/integration/sqlite/FindIT.kt +++ b/src/test/kotlin/integration/sqlite/FindIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.DocumentException import solutions.bitbadger.documents.integration.common.Find import kotlin.test.Test diff --git a/src/test/kotlin/integration/sqlite/PatchIT.kt b/src/test/kotlin/integration/sqlite/PatchIT.kt index 7878bdf..76e9c27 100644 --- a/src/test/kotlin/integration/sqlite/PatchIT.kt +++ b/src/test/kotlin/integration/sqlite/PatchIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.DocumentException import solutions.bitbadger.documents.integration.common.Patch import kotlin.test.Test diff --git a/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt b/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt index bbc26a3..096d1fb 100644 --- a/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt +++ b/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.common.DocumentException import solutions.bitbadger.documents.integration.common.RemoveFields import kotlin.test.Test diff --git a/src/test/kotlin/integration/sqlite/SQLiteDB.kt b/src/test/kotlin/integration/sqlite/SQLiteDB.kt index 55504a0..e6be940 100644 --- a/src/test/kotlin/integration/sqlite/SQLiteDB.kt +++ b/src/test/kotlin/integration/sqlite/SQLiteDB.kt @@ -1,6 +1,8 @@ package solutions.bitbadger.documents.integration.sqlite import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType import solutions.bitbadger.documents.integration.TEST_TABLE import solutions.bitbadger.documents.integration.ThrowawayDatabase import java.io.File -- 2.47.2 From 573d96c6aa329df7bb4d416e6f087320b60f3fcb Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 12 Mar 2025 09:57:52 -0400 Subject: [PATCH 36/88] Fix paths for common project --- .idea/misc.xml | 1 + src/common/pom.xml | 29 ++++++++++++------- src/common/src/{ => main}/kotlin/AutoId.kt | 0 .../src/{ => main}/kotlin/Comparison.kt | 0 .../src/{ => main}/kotlin/Configuration.kt | 0 src/common/src/{ => main}/kotlin/Dialect.kt | 0 .../{ => main}/kotlin/DocumentException.kt | 0 .../src/{ => main}/kotlin/DocumentIndex.kt | 0 src/common/src/{ => main}/kotlin/Field.kt | 0 .../src/{ => main}/kotlin/FieldFormat.kt | 0 .../src/{ => main}/kotlin/FieldMatch.kt | 0 src/common/src/{ => main}/kotlin/Op.kt | 0 src/common/src/{ => main}/kotlin/Parameter.kt | 0 .../src/{ => main}/kotlin/ParameterName.kt | 0 .../src/{ => main}/kotlin/ParameterType.kt | 0 .../src/{ => main}/kotlin/query/Count.kt | 0 .../src/{ => main}/kotlin/query/Definition.kt | 0 .../src/{ => main}/kotlin/query/Delete.kt | 0 .../src/{ => main}/kotlin/query/Document.kt | 0 .../src/{ => main}/kotlin/query/Exists.kt | 0 .../src/{ => main}/kotlin/query/Find.kt | 0 .../src/{ => main}/kotlin/query/Patch.kt | 0 .../src/{ => main}/kotlin/query/Query.kt | 0 .../{ => main}/kotlin/query/RemoveFields.kt | 0 .../src/{ => main}/kotlin/query/Where.kt | 0 .../documents/common/java/AutoIdTest.java | 0 .../common/java/DocumentIndexTest.java | 0 .../documents/common/java/FieldMatchTest.java | 0 .../documents/common/java/FieldTest.java | 0 .../documents/common/java/OpTest.java | 0 .../common/java/ParameterNameTest.java | 0 .../documents/common/java/ParameterTest.java | 0 .../{ => src}/test/kotlin/AutoIdTest.kt | 0 .../{ => src}/test/kotlin/ComparisonTest.kt | 0 .../test/kotlin/ConfigurationTest.kt | 0 .../{ => src}/test/kotlin/DialectTest.kt | 0 .../test/kotlin/DocumentIndexTest.kt | 0 .../{ => src}/test/kotlin/FieldMatchTest.kt | 0 src/common/{ => src}/test/kotlin/FieldTest.kt | 0 src/common/{ => src}/test/kotlin/OpTest.kt | 0 .../test/kotlin/ParameterNameTest.kt | 0 .../{ => src}/test/kotlin/ParameterTest.kt | 0 .../{ => src}/test/kotlin/query/CountTest.kt | 0 .../test/kotlin/query/DefinitionTest.kt | 0 .../{ => src}/test/kotlin/query/DeleteTest.kt | 0 .../test/kotlin/query/DocumentTest.kt | 0 .../{ => src}/test/kotlin/query/ExistsTest.kt | 0 .../{ => src}/test/kotlin/query/FindTest.kt | 0 .../{ => src}/test/kotlin/query/PatchTest.kt | 0 .../{ => src}/test/kotlin/query/QueryTest.kt | 0 .../test/kotlin/query/RemoveFieldsTest.kt | 0 .../{ => src}/test/kotlin/query/WhereTest.kt | 0 src/pom.xml | 5 ++-- 53 files changed, 23 insertions(+), 12 deletions(-) rename src/common/src/{ => main}/kotlin/AutoId.kt (100%) rename src/common/src/{ => main}/kotlin/Comparison.kt (100%) rename src/common/src/{ => main}/kotlin/Configuration.kt (100%) rename src/common/src/{ => main}/kotlin/Dialect.kt (100%) rename src/common/src/{ => main}/kotlin/DocumentException.kt (100%) rename src/common/src/{ => main}/kotlin/DocumentIndex.kt (100%) rename src/common/src/{ => main}/kotlin/Field.kt (100%) rename src/common/src/{ => main}/kotlin/FieldFormat.kt (100%) rename src/common/src/{ => main}/kotlin/FieldMatch.kt (100%) rename src/common/src/{ => main}/kotlin/Op.kt (100%) rename src/common/src/{ => main}/kotlin/Parameter.kt (100%) rename src/common/src/{ => main}/kotlin/ParameterName.kt (100%) rename src/common/src/{ => main}/kotlin/ParameterType.kt (100%) rename src/common/src/{ => main}/kotlin/query/Count.kt (100%) rename src/common/src/{ => main}/kotlin/query/Definition.kt (100%) rename src/common/src/{ => main}/kotlin/query/Delete.kt (100%) rename src/common/src/{ => main}/kotlin/query/Document.kt (100%) rename src/common/src/{ => main}/kotlin/query/Exists.kt (100%) rename src/common/src/{ => main}/kotlin/query/Find.kt (100%) rename src/common/src/{ => main}/kotlin/query/Patch.kt (100%) rename src/common/src/{ => main}/kotlin/query/Query.kt (100%) rename src/common/src/{ => main}/kotlin/query/RemoveFields.kt (100%) rename src/common/src/{ => main}/kotlin/query/Where.kt (100%) rename src/common/{ => src}/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java (100%) rename src/common/{ => src}/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java (100%) rename src/common/{ => src}/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java (100%) rename src/common/{ => src}/test/java/solutions/bitbadger/documents/common/java/FieldTest.java (100%) rename src/common/{ => src}/test/java/solutions/bitbadger/documents/common/java/OpTest.java (100%) rename src/common/{ => src}/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java (100%) rename src/common/{ => src}/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java (100%) rename src/common/{ => src}/test/kotlin/AutoIdTest.kt (100%) rename src/common/{ => src}/test/kotlin/ComparisonTest.kt (100%) rename src/common/{ => src}/test/kotlin/ConfigurationTest.kt (100%) rename src/common/{ => src}/test/kotlin/DialectTest.kt (100%) rename src/common/{ => src}/test/kotlin/DocumentIndexTest.kt (100%) rename src/common/{ => src}/test/kotlin/FieldMatchTest.kt (100%) rename src/common/{ => src}/test/kotlin/FieldTest.kt (100%) rename src/common/{ => src}/test/kotlin/OpTest.kt (100%) rename src/common/{ => src}/test/kotlin/ParameterNameTest.kt (100%) rename src/common/{ => src}/test/kotlin/ParameterTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/CountTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/DefinitionTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/DeleteTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/DocumentTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/ExistsTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/FindTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/PatchTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/QueryTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/RemoveFieldsTest.kt (100%) rename src/common/{ => src}/test/kotlin/query/WhereTest.kt (100%) diff --git a/.idea/misc.xml b/.idea/misc.xml index e56fe0c..85ecaa5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -7,6 +7,7 @@ test-compile @@ -41,6 +52,12 @@ test-compile + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + @@ -72,20 +89,12 @@ - - org.codehaus.mojo - exec-maven-plugin - 1.6.0 - - MainKt - - org.apache.maven.plugins maven-compiler-plugin - 9 - 9 + ${java.version} + ${java.version} diff --git a/src/common/src/kotlin/AutoId.kt b/src/common/src/main/kotlin/AutoId.kt similarity index 100% rename from src/common/src/kotlin/AutoId.kt rename to src/common/src/main/kotlin/AutoId.kt diff --git a/src/common/src/kotlin/Comparison.kt b/src/common/src/main/kotlin/Comparison.kt similarity index 100% rename from src/common/src/kotlin/Comparison.kt rename to src/common/src/main/kotlin/Comparison.kt diff --git a/src/common/src/kotlin/Configuration.kt b/src/common/src/main/kotlin/Configuration.kt similarity index 100% rename from src/common/src/kotlin/Configuration.kt rename to src/common/src/main/kotlin/Configuration.kt diff --git a/src/common/src/kotlin/Dialect.kt b/src/common/src/main/kotlin/Dialect.kt similarity index 100% rename from src/common/src/kotlin/Dialect.kt rename to src/common/src/main/kotlin/Dialect.kt diff --git a/src/common/src/kotlin/DocumentException.kt b/src/common/src/main/kotlin/DocumentException.kt similarity index 100% rename from src/common/src/kotlin/DocumentException.kt rename to src/common/src/main/kotlin/DocumentException.kt diff --git a/src/common/src/kotlin/DocumentIndex.kt b/src/common/src/main/kotlin/DocumentIndex.kt similarity index 100% rename from src/common/src/kotlin/DocumentIndex.kt rename to src/common/src/main/kotlin/DocumentIndex.kt diff --git a/src/common/src/kotlin/Field.kt b/src/common/src/main/kotlin/Field.kt similarity index 100% rename from src/common/src/kotlin/Field.kt rename to src/common/src/main/kotlin/Field.kt diff --git a/src/common/src/kotlin/FieldFormat.kt b/src/common/src/main/kotlin/FieldFormat.kt similarity index 100% rename from src/common/src/kotlin/FieldFormat.kt rename to src/common/src/main/kotlin/FieldFormat.kt diff --git a/src/common/src/kotlin/FieldMatch.kt b/src/common/src/main/kotlin/FieldMatch.kt similarity index 100% rename from src/common/src/kotlin/FieldMatch.kt rename to src/common/src/main/kotlin/FieldMatch.kt diff --git a/src/common/src/kotlin/Op.kt b/src/common/src/main/kotlin/Op.kt similarity index 100% rename from src/common/src/kotlin/Op.kt rename to src/common/src/main/kotlin/Op.kt diff --git a/src/common/src/kotlin/Parameter.kt b/src/common/src/main/kotlin/Parameter.kt similarity index 100% rename from src/common/src/kotlin/Parameter.kt rename to src/common/src/main/kotlin/Parameter.kt diff --git a/src/common/src/kotlin/ParameterName.kt b/src/common/src/main/kotlin/ParameterName.kt similarity index 100% rename from src/common/src/kotlin/ParameterName.kt rename to src/common/src/main/kotlin/ParameterName.kt diff --git a/src/common/src/kotlin/ParameterType.kt b/src/common/src/main/kotlin/ParameterType.kt similarity index 100% rename from src/common/src/kotlin/ParameterType.kt rename to src/common/src/main/kotlin/ParameterType.kt diff --git a/src/common/src/kotlin/query/Count.kt b/src/common/src/main/kotlin/query/Count.kt similarity index 100% rename from src/common/src/kotlin/query/Count.kt rename to src/common/src/main/kotlin/query/Count.kt diff --git a/src/common/src/kotlin/query/Definition.kt b/src/common/src/main/kotlin/query/Definition.kt similarity index 100% rename from src/common/src/kotlin/query/Definition.kt rename to src/common/src/main/kotlin/query/Definition.kt diff --git a/src/common/src/kotlin/query/Delete.kt b/src/common/src/main/kotlin/query/Delete.kt similarity index 100% rename from src/common/src/kotlin/query/Delete.kt rename to src/common/src/main/kotlin/query/Delete.kt diff --git a/src/common/src/kotlin/query/Document.kt b/src/common/src/main/kotlin/query/Document.kt similarity index 100% rename from src/common/src/kotlin/query/Document.kt rename to src/common/src/main/kotlin/query/Document.kt diff --git a/src/common/src/kotlin/query/Exists.kt b/src/common/src/main/kotlin/query/Exists.kt similarity index 100% rename from src/common/src/kotlin/query/Exists.kt rename to src/common/src/main/kotlin/query/Exists.kt diff --git a/src/common/src/kotlin/query/Find.kt b/src/common/src/main/kotlin/query/Find.kt similarity index 100% rename from src/common/src/kotlin/query/Find.kt rename to src/common/src/main/kotlin/query/Find.kt diff --git a/src/common/src/kotlin/query/Patch.kt b/src/common/src/main/kotlin/query/Patch.kt similarity index 100% rename from src/common/src/kotlin/query/Patch.kt rename to src/common/src/main/kotlin/query/Patch.kt diff --git a/src/common/src/kotlin/query/Query.kt b/src/common/src/main/kotlin/query/Query.kt similarity index 100% rename from src/common/src/kotlin/query/Query.kt rename to src/common/src/main/kotlin/query/Query.kt diff --git a/src/common/src/kotlin/query/RemoveFields.kt b/src/common/src/main/kotlin/query/RemoveFields.kt similarity index 100% rename from src/common/src/kotlin/query/RemoveFields.kt rename to src/common/src/main/kotlin/query/RemoveFields.kt diff --git a/src/common/src/kotlin/query/Where.kt b/src/common/src/main/kotlin/query/Where.kt similarity index 100% rename from src/common/src/kotlin/query/Where.kt rename to src/common/src/main/kotlin/query/Where.kt diff --git a/src/common/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java similarity index 100% rename from src/common/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java rename to src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java diff --git a/src/common/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java similarity index 100% rename from src/common/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java rename to src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java diff --git a/src/common/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java similarity index 100% rename from src/common/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java rename to src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java diff --git a/src/common/test/java/solutions/bitbadger/documents/common/java/FieldTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java similarity index 100% rename from src/common/test/java/solutions/bitbadger/documents/common/java/FieldTest.java rename to src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java diff --git a/src/common/test/java/solutions/bitbadger/documents/common/java/OpTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java similarity index 100% rename from src/common/test/java/solutions/bitbadger/documents/common/java/OpTest.java rename to src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java diff --git a/src/common/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java similarity index 100% rename from src/common/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java rename to src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java diff --git a/src/common/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java similarity index 100% rename from src/common/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java rename to src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java diff --git a/src/common/test/kotlin/AutoIdTest.kt b/src/common/src/test/kotlin/AutoIdTest.kt similarity index 100% rename from src/common/test/kotlin/AutoIdTest.kt rename to src/common/src/test/kotlin/AutoIdTest.kt diff --git a/src/common/test/kotlin/ComparisonTest.kt b/src/common/src/test/kotlin/ComparisonTest.kt similarity index 100% rename from src/common/test/kotlin/ComparisonTest.kt rename to src/common/src/test/kotlin/ComparisonTest.kt diff --git a/src/common/test/kotlin/ConfigurationTest.kt b/src/common/src/test/kotlin/ConfigurationTest.kt similarity index 100% rename from src/common/test/kotlin/ConfigurationTest.kt rename to src/common/src/test/kotlin/ConfigurationTest.kt diff --git a/src/common/test/kotlin/DialectTest.kt b/src/common/src/test/kotlin/DialectTest.kt similarity index 100% rename from src/common/test/kotlin/DialectTest.kt rename to src/common/src/test/kotlin/DialectTest.kt diff --git a/src/common/test/kotlin/DocumentIndexTest.kt b/src/common/src/test/kotlin/DocumentIndexTest.kt similarity index 100% rename from src/common/test/kotlin/DocumentIndexTest.kt rename to src/common/src/test/kotlin/DocumentIndexTest.kt diff --git a/src/common/test/kotlin/FieldMatchTest.kt b/src/common/src/test/kotlin/FieldMatchTest.kt similarity index 100% rename from src/common/test/kotlin/FieldMatchTest.kt rename to src/common/src/test/kotlin/FieldMatchTest.kt diff --git a/src/common/test/kotlin/FieldTest.kt b/src/common/src/test/kotlin/FieldTest.kt similarity index 100% rename from src/common/test/kotlin/FieldTest.kt rename to src/common/src/test/kotlin/FieldTest.kt diff --git a/src/common/test/kotlin/OpTest.kt b/src/common/src/test/kotlin/OpTest.kt similarity index 100% rename from src/common/test/kotlin/OpTest.kt rename to src/common/src/test/kotlin/OpTest.kt diff --git a/src/common/test/kotlin/ParameterNameTest.kt b/src/common/src/test/kotlin/ParameterNameTest.kt similarity index 100% rename from src/common/test/kotlin/ParameterNameTest.kt rename to src/common/src/test/kotlin/ParameterNameTest.kt diff --git a/src/common/test/kotlin/ParameterTest.kt b/src/common/src/test/kotlin/ParameterTest.kt similarity index 100% rename from src/common/test/kotlin/ParameterTest.kt rename to src/common/src/test/kotlin/ParameterTest.kt diff --git a/src/common/test/kotlin/query/CountTest.kt b/src/common/src/test/kotlin/query/CountTest.kt similarity index 100% rename from src/common/test/kotlin/query/CountTest.kt rename to src/common/src/test/kotlin/query/CountTest.kt diff --git a/src/common/test/kotlin/query/DefinitionTest.kt b/src/common/src/test/kotlin/query/DefinitionTest.kt similarity index 100% rename from src/common/test/kotlin/query/DefinitionTest.kt rename to src/common/src/test/kotlin/query/DefinitionTest.kt diff --git a/src/common/test/kotlin/query/DeleteTest.kt b/src/common/src/test/kotlin/query/DeleteTest.kt similarity index 100% rename from src/common/test/kotlin/query/DeleteTest.kt rename to src/common/src/test/kotlin/query/DeleteTest.kt diff --git a/src/common/test/kotlin/query/DocumentTest.kt b/src/common/src/test/kotlin/query/DocumentTest.kt similarity index 100% rename from src/common/test/kotlin/query/DocumentTest.kt rename to src/common/src/test/kotlin/query/DocumentTest.kt diff --git a/src/common/test/kotlin/query/ExistsTest.kt b/src/common/src/test/kotlin/query/ExistsTest.kt similarity index 100% rename from src/common/test/kotlin/query/ExistsTest.kt rename to src/common/src/test/kotlin/query/ExistsTest.kt diff --git a/src/common/test/kotlin/query/FindTest.kt b/src/common/src/test/kotlin/query/FindTest.kt similarity index 100% rename from src/common/test/kotlin/query/FindTest.kt rename to src/common/src/test/kotlin/query/FindTest.kt diff --git a/src/common/test/kotlin/query/PatchTest.kt b/src/common/src/test/kotlin/query/PatchTest.kt similarity index 100% rename from src/common/test/kotlin/query/PatchTest.kt rename to src/common/src/test/kotlin/query/PatchTest.kt diff --git a/src/common/test/kotlin/query/QueryTest.kt b/src/common/src/test/kotlin/query/QueryTest.kt similarity index 100% rename from src/common/test/kotlin/query/QueryTest.kt rename to src/common/src/test/kotlin/query/QueryTest.kt diff --git a/src/common/test/kotlin/query/RemoveFieldsTest.kt b/src/common/src/test/kotlin/query/RemoveFieldsTest.kt similarity index 100% rename from src/common/test/kotlin/query/RemoveFieldsTest.kt rename to src/common/src/test/kotlin/query/RemoveFieldsTest.kt diff --git a/src/common/test/kotlin/query/WhereTest.kt b/src/common/src/test/kotlin/query/WhereTest.kt similarity index 100% rename from src/common/test/kotlin/query/WhereTest.kt rename to src/common/src/test/kotlin/query/WhereTest.kt diff --git a/src/pom.xml b/src/pom.xml index 928441f..60ce46a 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -36,9 +36,10 @@ + 11 UTF-8 official - 11 + ${java.version} 2.1.0 1.8.0 @@ -89,4 +90,4 @@ - \ No newline at end of file + -- 2.47.2 From 14f0178b639bc96af975fb59b02a7309a6d5ad66 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 12 Mar 2025 16:28:12 -0400 Subject: [PATCH 37/88] WIP on reorg --- documents.iml | 12 +- .../src/main/kotlin/DocumentSerializer.kt | 24 + .../documents/common/java/AutoIdTest.java | 2 +- src/common/src/test/kotlin/ComparisonTest.kt | 3 +- .../src/test/kotlin/query/RemoveFieldsTest.kt | 32 +- src/java/pom.xml | 113 +++++ .../src/main/kotlin/ConnectionExtensions.kt | 477 ++++++++++++++++++ src/java/src/main/kotlin/Count.kt | 127 +++++ .../java => java/src/main/kotlin}/Custom.kt | 1 - src/{ => java/src}/main/kotlin/Definition.kt | 3 +- src/java/src/main/kotlin/Delete.kt | 100 ++++ src/{ => java/src}/main/kotlin/Document.kt | 3 +- src/java/src/main/kotlin/DocumentConfig.kt | 15 + src/java/src/main/kotlin/Exists.kt | 127 +++++ src/java/src/main/kotlin/Find.kt | 403 +++++++++++++++ .../src/main/kotlin/NullDocumentSerializer.kt | 21 + src/java/src/main/kotlin/Parameters.kt | 130 +++++ src/java/src/main/kotlin/Patch.kt | 138 +++++ src/java/src/main/kotlin/RemoveFields.kt | 154 ++++++ .../java => java/src/main/kotlin}/Results.kt | 4 +- .../documents/java}/java/ParametersTest.java | 11 +- .../java/java/integration/common/Count.java | 20 + .../java/integration/postgresql/CountIT.java | 6 +- .../java}/java/testDocs/ByteIdClass.java | 2 +- .../java}/java/testDocs/IntIdClass.java | 2 +- .../java}/java/testDocs/JsonDocument.java | 8 +- .../java}/java/testDocs/LongIdClass.java | 2 +- .../java}/java/testDocs/ShortIdClass.java | 2 +- .../java}/java/testDocs/StringIdClass.java | 2 +- .../java}/java/testDocs/SubDocument.java | 2 +- .../src}/test/kotlin/ParametersTest.kt | 4 +- .../kotlin/integration/ThrowawayDatabase.kt | 2 +- .../src}/test/kotlin/integration/Types.kt | 4 +- .../test/kotlin/integration/common/Count.kt | 13 +- .../test/kotlin/integration/common/Custom.kt | 17 +- .../kotlin/integration/common/Definition.kt | 10 +- .../test/kotlin/integration/common/Delete.kt | 10 +- .../kotlin/integration/common/Document.kt | 4 +- .../test/kotlin/integration/common/Exists.kt | 13 +- .../test/kotlin/integration/common/Find.kt | 7 +- .../test/kotlin/integration/common/Patch.kt | 9 +- .../kotlin/integration/common/RemoveFields.kt | 9 +- .../kotlin/integration/postgresql/CountIT.kt | 4 +- .../kotlin/integration/postgresql/CustomIT.kt | 4 +- .../integration/postgresql/DefinitionIT.kt | 4 +- .../kotlin/integration/postgresql/DeleteIT.kt | 4 +- .../integration/postgresql/DocumentIT.kt | 4 +- .../kotlin/integration/postgresql/ExistsIT.kt | 4 +- .../kotlin/integration/postgresql/FindIT.kt | 4 +- .../kotlin/integration/postgresql/PatchIT.kt | 4 +- .../kotlin/integration/postgresql/PgDB.kt | 6 +- .../integration/postgresql/RemoveFieldsIT.kt | 4 +- .../test/kotlin/integration/sqlite/CountIT.kt | 4 +- .../kotlin/integration/sqlite/CustomIT.kt | 4 +- .../kotlin/integration/sqlite/DefinitionIT.kt | 4 +- .../kotlin/integration/sqlite/DeleteIT.kt | 4 +- .../kotlin/integration/sqlite/DocumentIT.kt | 4 +- .../kotlin/integration/sqlite/ExistsIT.kt | 4 +- .../test/kotlin/integration/sqlite/FindIT.kt | 4 +- .../test/kotlin/integration/sqlite/PatchIT.kt | 4 +- .../integration/sqlite/RemoveFieldsIT.kt | 4 +- .../kotlin/integration/sqlite/SQLiteDB.kt | 6 +- src/kotlin/pom.xml | 119 +++++ .../src}/main/kotlin/Configuration.kt | 8 +- .../src}/main/kotlin/ConnectionExtensions.kt | 3 +- src/{ => kotlin/src}/main/kotlin/Count.kt | 4 +- src/kotlin/src/main/kotlin/Custom.kt | 121 +++++ src/kotlin/src/main/kotlin/Definition.kt | 70 +++ src/{ => kotlin/src}/main/kotlin/Delete.kt | 3 - src/{ => kotlin/src}/main/kotlin/Exists.kt | 3 - src/{ => kotlin/src}/main/kotlin/Find.kt | 3 - .../src}/main/kotlin/Parameters.kt | 3 - src/{ => kotlin/src}/main/kotlin/Patch.kt | 3 - .../src}/main/kotlin/RemoveFields.kt | 3 - src/kotlin/src/main/kotlin/Results.kt | 80 +++ src/pom.xml | 1 + .../documents/java/ConfigurationTest.java | 1 - .../java/integration/common/Count.java | 20 - 78 files changed, 2397 insertions(+), 180 deletions(-) create mode 100644 src/common/src/main/kotlin/DocumentSerializer.kt create mode 100644 src/java/pom.xml create mode 100644 src/java/src/main/kotlin/ConnectionExtensions.kt create mode 100644 src/java/src/main/kotlin/Count.kt rename src/{main/kotlin/java => java/src/main/kotlin}/Custom.kt (99%) rename src/{ => java/src}/main/kotlin/Definition.kt (96%) create mode 100644 src/java/src/main/kotlin/Delete.kt rename src/{ => java/src}/main/kotlin/Document.kt (98%) create mode 100644 src/java/src/main/kotlin/DocumentConfig.kt create mode 100644 src/java/src/main/kotlin/Exists.kt create mode 100644 src/java/src/main/kotlin/Find.kt create mode 100644 src/java/src/main/kotlin/NullDocumentSerializer.kt create mode 100644 src/java/src/main/kotlin/Parameters.kt create mode 100644 src/java/src/main/kotlin/Patch.kt create mode 100644 src/java/src/main/kotlin/RemoveFields.kt rename src/{main/kotlin/java => java/src/main/kotlin}/Results.kt (96%) rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/ParametersTest.java (94%) create mode 100644 src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/Count.java rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/integration/postgresql/CountIT.java (65%) rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/testDocs/ByteIdClass.java (79%) rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/testDocs/IntIdClass.java (79%) rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/testDocs/JsonDocument.java (87%) rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/testDocs/LongIdClass.java (79%) rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/testDocs/ShortIdClass.java (80%) rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/testDocs/StringIdClass.java (80%) rename src/{test/java/solutions/bitbadger/documents => java/src/test/java/solutions/bitbadger/documents/java}/java/testDocs/SubDocument.java (89%) rename src/{ => java/src}/test/kotlin/ParametersTest.kt (98%) rename src/{ => java/src}/test/kotlin/integration/ThrowawayDatabase.kt (89%) rename src/{ => java/src}/test/kotlin/integration/Types.kt (93%) rename src/{ => java/src}/test/kotlin/integration/common/Count.kt (80%) rename src/{ => java/src}/test/kotlin/integration/common/Custom.kt (81%) rename src/{ => java/src}/test/kotlin/integration/common/Definition.kt (85%) rename src/{ => java/src}/test/kotlin/integration/common/Delete.kt (90%) rename src/{ => java/src}/test/kotlin/integration/common/Document.kt (97%) rename src/{ => java/src}/test/kotlin/integration/common/Exists.kt (80%) rename src/{ => java/src}/test/kotlin/integration/common/Find.kt (97%) rename src/{ => java/src}/test/kotlin/integration/common/Patch.kt (92%) rename src/{ => java/src}/test/kotlin/integration/common/RemoveFields.kt (93%) rename src/{ => java/src}/test/kotlin/integration/postgresql/CountIT.kt (90%) rename src/{ => java/src}/test/kotlin/integration/postgresql/CustomIT.kt (89%) rename src/{ => java/src}/test/kotlin/integration/postgresql/DefinitionIT.kt (86%) rename src/{ => java/src}/test/kotlin/integration/postgresql/DeleteIT.kt (90%) rename src/{ => java/src}/test/kotlin/integration/postgresql/DocumentIT.kt (91%) rename src/{ => java/src}/test/kotlin/integration/postgresql/ExistsIT.kt (91%) rename src/{ => java/src}/test/kotlin/integration/postgresql/FindIT.kt (97%) rename src/{ => java/src}/test/kotlin/integration/postgresql/PatchIT.kt (90%) rename src/{ => java/src}/test/kotlin/integration/postgresql/PgDB.kt (88%) rename src/{ => java/src}/test/kotlin/integration/postgresql/RemoveFieldsIT.kt (94%) rename src/{ => java/src}/test/kotlin/integration/sqlite/CountIT.kt (89%) rename src/{ => java/src}/test/kotlin/integration/sqlite/CustomIT.kt (89%) rename src/{ => java/src}/test/kotlin/integration/sqlite/DefinitionIT.kt (88%) rename src/{ => java/src}/test/kotlin/integration/sqlite/DeleteIT.kt (90%) rename src/{ => java/src}/test/kotlin/integration/sqlite/DocumentIT.kt (91%) rename src/{ => java/src}/test/kotlin/integration/sqlite/ExistsIT.kt (90%) rename src/{ => java/src}/test/kotlin/integration/sqlite/FindIT.kt (96%) rename src/{ => java/src}/test/kotlin/integration/sqlite/PatchIT.kt (90%) rename src/{ => java/src}/test/kotlin/integration/sqlite/RemoveFieldsIT.kt (92%) rename src/{ => java/src}/test/kotlin/integration/sqlite/SQLiteDB.kt (81%) create mode 100644 src/kotlin/pom.xml rename src/{ => kotlin/src}/main/kotlin/Configuration.kt (68%) rename src/{ => kotlin/src}/main/kotlin/ConnectionExtensions.kt (99%) rename src/{ => kotlin/src}/main/kotlin/Count.kt (97%) create mode 100644 src/kotlin/src/main/kotlin/Custom.kt create mode 100644 src/kotlin/src/main/kotlin/Definition.kt rename src/{ => kotlin/src}/main/kotlin/Delete.kt (97%) rename src/{ => kotlin/src}/main/kotlin/Exists.kt (98%) rename src/{ => kotlin/src}/main/kotlin/Find.kt (99%) rename src/{ => kotlin/src}/main/kotlin/Parameters.kt (98%) rename src/{ => kotlin/src}/main/kotlin/Patch.kt (98%) rename src/{ => kotlin/src}/main/kotlin/RemoveFields.kt (98%) create mode 100644 src/kotlin/src/main/kotlin/Results.kt delete mode 100644 src/test/java/solutions/bitbadger/documents/java/integration/common/Count.java diff --git a/documents.iml b/documents.iml index ab059ae..f0b3e9b 100644 --- a/documents.iml +++ b/documents.iml @@ -2,10 +2,14 @@ - - - - + + + + + + + + \ No newline at end of file diff --git a/src/common/src/main/kotlin/DocumentSerializer.kt b/src/common/src/main/kotlin/DocumentSerializer.kt new file mode 100644 index 0000000..09cb1c9 --- /dev/null +++ b/src/common/src/main/kotlin/DocumentSerializer.kt @@ -0,0 +1,24 @@ +package solutions.bitbadger.documents.common + +/** + * 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/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java index 1708332..9321974 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java +++ b/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import solutions.bitbadger.documents.common.AutoId; import solutions.bitbadger.documents.common.DocumentException; -import solutions.bitbadger.documents.java.testDocs.*; +import solutions.bitbadger.documents.java.java.testDocs.*; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/common/src/test/kotlin/ComparisonTest.kt b/src/common/src/test/kotlin/ComparisonTest.kt index 3cd54ad..b40a89e 100644 --- a/src/common/src/test/kotlin/ComparisonTest.kt +++ b/src/common/src/test/kotlin/ComparisonTest.kt @@ -2,7 +2,6 @@ package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.integration.TEST_TABLE import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -101,7 +100,7 @@ class ComparisonInArrayTest { fun op() = assertEquals( Op.IN_ARRAY, - ComparisonInArray(Pair(TEST_TABLE, listOf())).op, + ComparisonInArray(Pair("tbl", listOf())).op, "InArray comparison should have IN_ARRAY op" ) diff --git a/src/common/src/test/kotlin/query/RemoveFieldsTest.kt b/src/common/src/test/kotlin/query/RemoveFieldsTest.kt index 498a435..1710e0b 100644 --- a/src/common/src/test/kotlin/query/RemoveFieldsTest.kt +++ b/src/common/src/test/kotlin/query/RemoveFieldsTest.kt @@ -4,11 +4,7 @@ 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.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.* import kotlin.test.assertEquals /** @@ -34,7 +30,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data - :name::text[] WHERE data->>'id' = :id", - RemoveFields.byId(tbl, Parameters.fieldNames(listOf("a", "z"))), + RemoveFields.byId(tbl, listOf(Parameter(":name", ParameterType.STRING, "{a,z}"))), "Remove Fields query not constructed correctly" ) } @@ -45,7 +41,13 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "UPDATE $tbl SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id", - RemoveFields.byId(tbl, Parameters.fieldNames(listOf("a", "z"))), + RemoveFields.byId( + tbl, + listOf( + Parameter(":name0", ParameterType.STRING, "a"), + Parameter(":name1", ParameterType.STRING, "z") + ) + ), "Remove Field query not constructed correctly" ) } @@ -56,7 +58,11 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data - :name::text[] WHERE data->>'f' > :g", - RemoveFields.byFields(tbl, Parameters.fieldNames(listOf("b", "c")), listOf(Field.greater("f", "", ":g"))), + RemoveFields.byFields( + tbl, + listOf(Parameter(":name", ParameterType.STRING, "{b,c}")), + listOf(Field.greater("f", "", ":g")) + ), "Remove Field query not constructed correctly" ) } @@ -67,7 +73,11 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "UPDATE $tbl SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g", - RemoveFields.byFields(tbl, Parameters.fieldNames(listOf("b", "c")), listOf(Field.greater("f", "", ":g"))), + RemoveFields.byFields( + tbl, + listOf(Parameter(":name0", ParameterType.STRING, "b"), Parameter(":name1", ParameterType.STRING, "c")), + listOf(Field.greater("f", "", ":g")) + ), "Remove Field query not constructed correctly" ) } @@ -78,7 +88,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data - :name::text[] WHERE data @> :criteria", - RemoveFields.byContains(tbl, Parameters.fieldNames(listOf("m", "n"))), + RemoveFields.byContains(tbl, listOf(Parameter(":name", ParameterType.STRING, "{m,n}"))), "Remove Field query not constructed correctly" ) } @@ -96,7 +106,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)", - RemoveFields.byJsonPath(tbl, Parameters.fieldNames(listOf("o", "p"))), + RemoveFields.byJsonPath(tbl, listOf(Parameter(":name", ParameterType.STRING, "{o,p}"))), "Remove Field query not constructed correctly" ) } diff --git a/src/java/pom.xml b/src/java/pom.xml new file mode 100644 index 0000000..0b272c0 --- /dev/null +++ b/src/java/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + solutions.bitbadger.documents + java + 4.0.0-alpha1-SNAPSHOT + jar + + + solutions.bitbadger + documents + 4.0.0-alpha1-SNAPSHOT + + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (Java Library) + https://bitbadger.solutions/open-source/relational-documents/jvm/ + + + 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 + common + 4.0.0-alpha1-SNAPSHOT + system + ${project.basedir}/../common/target/common-4.0.0-alpha1-SNAPSHOT.jar + jar + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + process-sources + + compile + + + + ${project.basedir}/src/main/kotlin + + + + + test-compile + test-compile + + 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 + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + + diff --git a/src/java/src/main/kotlin/ConnectionExtensions.kt b/src/java/src/main/kotlin/ConnectionExtensions.kt new file mode 100644 index 0000000..a5230c2 --- /dev/null +++ b/src/java/src/main/kotlin/ConnectionExtensions.kt @@ -0,0 +1,477 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.Custom +import solutions.bitbadger.documents.common.DocumentIndex +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.java.* +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(), noinline mapFunc: (ResultSet, Class) -> TDoc +) = Custom.list(query, parameters, 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(), noinline mapFunc: (ResultSet, Class) -> TDoc +) = Custom.single(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(), + noinline mapFunc: (ResultSet, Class) -> 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) = + solutions.bitbadger.documents.java.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) = + solutions.bitbadger.documents.java.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) = + solutions.bitbadger.documents.java.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 ~~~ + +/** + * 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) = + solutions.bitbadger.documents.java.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) = + solutions.bitbadger.documents.java.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 +) = + solutions.bitbadger.documents.java.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 +) = + solutions.bitbadger.documents.java.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 +) = + solutions.bitbadger.documents.java.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 +) = + solutions.bitbadger.documents.java.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 +) = + solutions.bitbadger.documents.java.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 +) = + solutions.bitbadger.documents.java.Find.firstByJsonPath(tableName, 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/java/src/main/kotlin/Count.kt b/src/java/src/main/kotlin/Count.kt new file mode 100644 index 0000000..5d42b38 --- /dev/null +++ b/src/java/src/main/kotlin/Count.kt @@ -0,0 +1,127 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.Results +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +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 + */ + @JvmStatic + fun all(tableName: String, conn: Connection) = + conn.customScalar(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 + */ + @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 + */ + @JvmStatic + @JvmOverloads + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ): Long { + val named = Parameters.nameFields(fields) + return conn.customScalar( + 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 + */ + @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 + */ + @JvmStatic + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = + conn.customScalar(Count.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 + */ + @JvmStatic + 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 + */ + @JvmStatic + fun byJsonPath(tableName: String, path: String, conn: Connection) = + conn.customScalar( + Count.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 + */ + @JvmStatic + fun byJsonPath(tableName: String, path: String) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +} diff --git a/src/main/kotlin/java/Custom.kt b/src/java/src/main/kotlin/Custom.kt similarity index 99% rename from src/main/kotlin/java/Custom.kt rename to src/java/src/main/kotlin/Custom.kt index 7d5ee67..5503022 100644 --- a/src/main/kotlin/java/Custom.kt +++ b/src/java/src/main/kotlin/Custom.kt @@ -2,7 +2,6 @@ package solutions.bitbadger.documents.java import solutions.bitbadger.documents.common.Configuration import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.Parameters import java.sql.Connection import java.sql.ResultSet diff --git a/src/main/kotlin/Definition.kt b/src/java/src/main/kotlin/Definition.kt similarity index 96% rename from src/main/kotlin/Definition.kt rename to src/java/src/main/kotlin/Definition.kt index 424d742..7cd749b 100644 --- a/src/main/kotlin/Definition.kt +++ b/src/java/src/main/kotlin/Definition.kt @@ -1,8 +1,7 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.common.DocumentIndex import java.sql.Connection -import solutions.bitbadger.documents.common.query.Definition /** * Functions to define tables and indexes diff --git a/src/java/src/main/kotlin/Delete.kt b/src/java/src/main/kotlin/Delete.kt new file mode 100644 index 0000000..6f9bd52 --- /dev/null +++ b/src/java/src/main/kotlin/Delete.kt @@ -0,0 +1,100 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +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) = + conn.customNonQuery( + byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))) + ) + + /** + * 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) = + 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 + */ + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { + val named = Parameters.nameFields(fields) + conn.customNonQuery(byFields(tableName, named, howMatched), Parameters.addFields(named)) + } + + /** + * 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) = + 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 + */ + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = + conn.customNonQuery(Delete.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) = + conn.customNonQuery(Delete.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) + + /** + * 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) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +} diff --git a/src/main/kotlin/Document.kt b/src/java/src/main/kotlin/Document.kt similarity index 98% rename from src/main/kotlin/Document.kt rename to src/java/src/main/kotlin/Document.kt index 6984e5d..6a29a7c 100644 --- a/src/main/kotlin/Document.kt +++ b/src/java/src/main/kotlin/Document.kt @@ -1,6 +1,7 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.common.AutoId +import solutions.bitbadger.documents.common.Configuration import solutions.bitbadger.documents.common.Dialect import solutions.bitbadger.documents.common.Field import java.sql.Connection diff --git a/src/java/src/main/kotlin/DocumentConfig.kt b/src/java/src/main/kotlin/DocumentConfig.kt new file mode 100644 index 0000000..81431d7 --- /dev/null +++ b/src/java/src/main/kotlin/DocumentConfig.kt @@ -0,0 +1,15 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.common.DocumentSerializer + +/** + * Configuration for document serialization + */ +object DocumentConfig { + + /** + * The serializer to use for documents + */ + @JvmStatic + var serializer: DocumentSerializer = NullDocumentSerializer() +} diff --git a/src/java/src/main/kotlin/Exists.kt b/src/java/src/main/kotlin/Exists.kt new file mode 100644 index 0000000..5f9deda --- /dev/null +++ b/src/java/src/main/kotlin/Exists.kt @@ -0,0 +1,127 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.Results +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +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) = + conn.customScalar( + byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + 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 + */ + 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 + */ + fun byFields( + tableName: String, + fields: Collection>, + howMatched: FieldMatch? = null, + conn: Connection + ): Boolean { + val named = Parameters.nameFields(fields) + return conn.customScalar( + byFields(tableName, named, howMatched), + Parameters.addFields(named), + 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 + */ + 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 + */ + inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = + conn.customScalar( + Exists.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) = + conn.customScalar( + Exists.byJsonPath(tableName), + listOf(Parameter(":path", ParameterType.STRING, path)), + 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 called on a SQLite connection + */ + fun byJsonPath(tableName: String, path: String) = + Configuration.dbConn().use { byJsonPath(tableName, path, it) } +} diff --git a/src/java/src/main/kotlin/Find.kt b/src/java/src/main/kotlin/Find.kt new file mode 100644 index 0000000..87f74a5 --- /dev/null +++ b/src/java/src/main/kotlin/Find.kt @@ -0,0 +1,403 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.Results +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +import java.sql.Connection +import solutions.bitbadger.documents.common.query.orderBy + +/** + * 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(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( + 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( + 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( + Find.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( + 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( + 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( + Find.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( + 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/java/src/main/kotlin/NullDocumentSerializer.kt b/src/java/src/main/kotlin/NullDocumentSerializer.kt new file mode 100644 index 0000000..0fd5ea4 --- /dev/null +++ b/src/java/src/main/kotlin/NullDocumentSerializer.kt @@ -0,0 +1,21 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.common.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/java/src/main/kotlin/Parameters.kt b/src/java/src/main/kotlin/Parameters.kt new file mode 100644 index 0000000..e8b94b6 --- /dev/null +++ b/src/java/src/main/kotlin/Parameters.kt @@ -0,0 +1,130 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.common.* +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/java/src/main/kotlin/Patch.kt b/src/java/src/main/kotlin/Patch.kt new file mode 100644 index 0000000..7b48698 --- /dev/null +++ b/src/java/src/main/kotlin/Patch.kt @@ -0,0 +1,138 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.common.ParameterType +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( + Patch.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( + 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( + Patch.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( + Patch.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/java/src/main/kotlin/RemoveFields.kt b/src/java/src/main/kotlin/RemoveFields.kt new file mode 100644 index 0000000..0bb10f0 --- /dev/null +++ b/src/java/src/main/kotlin/RemoveFields.kt @@ -0,0 +1,154 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.common.* +import java.sql.Connection + +/** + * 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 + */ + fun byId(tableName: String, docId: TKey, toRemove: Collection, conn: Connection) { + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + byId(tableName, nameParams, docId), + Parameters.addFields( + listOf(Field.equal(Configuration.idField, docId, ":id")), + translatePath(nameParams) + ) + ) + } + + /** + * 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) = + 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 + */ + fun byFields( + tableName: String, + fields: Collection>, + toRemove: Collection, + howMatched: FieldMatch? = null, + conn: Connection + ) { + val named = Parameters.nameFields(fields) + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + byFields(tableName, nameParams, named, howMatched), + Parameters.addFields(named, translatePath(nameParams)) + ) + } + + /** + * 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 + ) = + 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 + */ + inline fun byContains( + tableName: String, + criteria: TContains, + toRemove: Collection, + conn: Connection + ) { + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + RemoveFields.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) { + val nameParams = Parameters.fieldNames(toRemove) + conn.customNonQuery( + RemoveFields.byJsonPath(tableName, nameParams), + listOf(Parameter(":path", ParameterType.STRING, path), *nameParams.toTypedArray()) + ) + } + + /** + * 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) = + Configuration.dbConn().use { byJsonPath(tableName, path, toRemove, it) } +} diff --git a/src/main/kotlin/java/Results.kt b/src/java/src/main/kotlin/Results.kt similarity index 96% rename from src/main/kotlin/java/Results.kt rename to src/java/src/main/kotlin/Results.kt index 9b6f9eb..6438eaa 100644 --- a/src/main/kotlin/java/Results.kt +++ b/src/java/src/main/kotlin/Results.kt @@ -1,6 +1,6 @@ package solutions.bitbadger.documents.java -import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.common.Configuration import solutions.bitbadger.documents.common.Dialect import solutions.bitbadger.documents.common.DocumentException import java.sql.PreparedStatement @@ -20,7 +20,7 @@ object Results { */ @JvmStatic fun fromDocument(field: String, rs: ResultSet, clazz: Class) = - Configuration.serializer.deserialize(rs.getString(field), clazz) + DocumentConfig.serializer.deserialize(rs.getString(field), clazz) /** * Create a domain item from a document diff --git a/src/test/java/solutions/bitbadger/documents/java/ParametersTest.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java similarity index 94% rename from src/test/java/solutions/bitbadger/documents/java/ParametersTest.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java index c3b98c8..d2f2ee4 100644 --- a/src/test/java/solutions/bitbadger/documents/java/ParametersTest.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java @@ -1,13 +1,10 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.java.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.common.DocumentException; -import solutions.bitbadger.documents.common.Field; -import solutions.bitbadger.documents.common.Parameter; -import solutions.bitbadger.documents.common.ParameterType; +import solutions.bitbadger.documents.common.*; +import solutions.bitbadger.documents.java.Parameters; import java.util.List; @@ -16,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Parameters` object */ -@DisplayName("Java | Parameters") +@DisplayName("Java | Java | Parameters") final public class ParametersTest { /** diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/Count.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/Count.java new file mode 100644 index 0000000..4ffc4ce --- /dev/null +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/Count.java @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents.java.java.integration.common; + +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase; +import solutions.bitbadger.documents.java.java.testDocs.JsonDocument; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static solutions.bitbadger.documents.java.integration.TypesKt.TEST_TABLE; + +final public class Count { + + public static void all(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(5L, solutions.bitbadger.documents.java.Count.all(TEST_TABLE, db.getConn()), + "There should have been 5 documents in the table"); + } + + + private Count() { + } +} diff --git a/src/test/java/solutions/bitbadger/documents/java/integration/postgresql/CountIT.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java similarity index 65% rename from src/test/java/solutions/bitbadger/documents/java/integration/postgresql/CountIT.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java index 32fb88d..525aee5 100644 --- a/src/test/java/solutions/bitbadger/documents/java/integration/postgresql/CountIT.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.integration.postgresql; +package solutions.bitbadger.documents.java.java.integration.postgresql; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.integration.postgresql.PgDB; -import solutions.bitbadger.documents.java.integration.common.Count; +import solutions.bitbadger.documents.java.integration.postgresql.PgDB; +import solutions.bitbadger.documents.java.java.integration.common.Count; /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/ByteIdClass.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java similarity index 79% rename from src/test/java/solutions/bitbadger/documents/java/testDocs/ByteIdClass.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java index c0d41e7..4ffdf15 100644 --- a/src/test/java/solutions/bitbadger/documents/java/testDocs/ByteIdClass.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.testDocs; +package solutions.bitbadger.documents.java.java.testDocs; public class ByteIdClass { diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/IntIdClass.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java similarity index 79% rename from src/test/java/solutions/bitbadger/documents/java/testDocs/IntIdClass.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java index 1d7a0cc..25a5613 100644 --- a/src/test/java/solutions/bitbadger/documents/java/testDocs/IntIdClass.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.testDocs; +package solutions.bitbadger.documents.java.java.testDocs; public class IntIdClass { diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java similarity index 87% rename from src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java index 7bd9906..d1d02c6 100644 --- a/src/test/java/solutions/bitbadger/documents/java/testDocs/JsonDocument.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java @@ -1,12 +1,12 @@ -package solutions.bitbadger.documents.java.testDocs; +package solutions.bitbadger.documents.java.java.testDocs; import kotlinx.serialization.Serializable; -import solutions.bitbadger.documents.Document; -import solutions.bitbadger.documents.integration.ThrowawayDatabase; +import solutions.bitbadger.documents.java.Document; +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase; import java.util.List; -import static solutions.bitbadger.documents.integration.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.integration.TypesKt.TEST_TABLE; @Serializable public class JsonDocument { diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/LongIdClass.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java similarity index 79% rename from src/test/java/solutions/bitbadger/documents/java/testDocs/LongIdClass.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java index 1ee8bc0..9a56846 100644 --- a/src/test/java/solutions/bitbadger/documents/java/testDocs/LongIdClass.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.testDocs; +package solutions.bitbadger.documents.java.java.testDocs; public class LongIdClass { diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/ShortIdClass.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java similarity index 80% rename from src/test/java/solutions/bitbadger/documents/java/testDocs/ShortIdClass.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java index 83a2f3c..c5913cc 100644 --- a/src/test/java/solutions/bitbadger/documents/java/testDocs/ShortIdClass.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.testDocs; +package solutions.bitbadger.documents.java.java.testDocs; public class ShortIdClass { diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/StringIdClass.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java similarity index 80% rename from src/test/java/solutions/bitbadger/documents/java/testDocs/StringIdClass.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java index 3013885..35e4945 100644 --- a/src/test/java/solutions/bitbadger/documents/java/testDocs/StringIdClass.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.testDocs; +package solutions.bitbadger.documents.java.java.testDocs; public class StringIdClass { diff --git a/src/test/java/solutions/bitbadger/documents/java/testDocs/SubDocument.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java similarity index 89% rename from src/test/java/solutions/bitbadger/documents/java/testDocs/SubDocument.java rename to src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java index f0b46ec..fa4c730 100644 --- a/src/test/java/solutions/bitbadger/documents/java/testDocs/SubDocument.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.testDocs; +package solutions.bitbadger.documents.java.java.testDocs; public class SubDocument { diff --git a/src/test/kotlin/ParametersTest.kt b/src/java/src/test/kotlin/ParametersTest.kt similarity index 98% rename from src/test/kotlin/ParametersTest.kt rename to src/java/src/test/kotlin/ParametersTest.kt index 566c267..ac91104 100644 --- a/src/test/kotlin/ParametersTest.kt +++ b/src/java/src/test/kotlin/ParametersTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.java import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -12,7 +12,7 @@ import kotlin.test.assertSame /** * Unit tests for the `Parameters` object */ -@DisplayName("Kotlin | Parameters") +@DisplayName("Kotlin | Java | Parameters") class ParametersTest { /** diff --git a/src/test/kotlin/integration/ThrowawayDatabase.kt b/src/java/src/test/kotlin/integration/ThrowawayDatabase.kt similarity index 89% rename from src/test/kotlin/integration/ThrowawayDatabase.kt rename to src/java/src/test/kotlin/integration/ThrowawayDatabase.kt index 5138ba4..1e46251 100644 --- a/src/test/kotlin/integration/ThrowawayDatabase.kt +++ b/src/java/src/test/kotlin/integration/ThrowawayDatabase.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.integration +package solutions.bitbadger.documents.java.integration import java.sql.Connection diff --git a/src/test/kotlin/integration/Types.kt b/src/java/src/test/kotlin/integration/Types.kt similarity index 93% rename from src/test/kotlin/integration/Types.kt rename to src/java/src/test/kotlin/integration/Types.kt index 54adfb4..730caa0 100644 --- a/src/test/kotlin/integration/Types.kt +++ b/src/java/src/test/kotlin/integration/Types.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration +package solutions.bitbadger.documents.java.integration import kotlinx.serialization.Serializable -import solutions.bitbadger.documents.insert +import solutions.bitbadger.documents.java.insert /** The test table name to use for integration tests */ const val TEST_TABLE = "test_table" diff --git a/src/test/kotlin/integration/common/Count.kt b/src/java/src/test/kotlin/integration/common/Count.kt similarity index 80% rename from src/test/kotlin/integration/common/Count.kt rename to src/java/src/test/kotlin/integration/common/Count.kt index e360977..e8b3258 100644 --- a/src/test/kotlin/integration/common/Count.kt +++ b/src/java/src/test/kotlin/integration/common/Count.kt @@ -1,10 +1,13 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.integration.JsonDocument -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.countAll +import solutions.bitbadger.documents.java.countByContains +import solutions.bitbadger.documents.java.countByFields +import solutions.bitbadger.documents.java.countByJsonPath +import solutions.bitbadger.documents.java.integration.JsonDocument +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.assertEquals /** diff --git a/src/test/kotlin/integration/common/Custom.kt b/src/java/src/test/kotlin/integration/common/Custom.kt similarity index 81% rename from src/test/kotlin/integration/common/Custom.kt rename to src/java/src/test/kotlin/integration/common/Custom.kt index f8361c6..05073d3 100644 --- a/src/test/kotlin/integration/common/Custom.kt +++ b/src/java/src/test/kotlin/integration/common/Custom.kt @@ -1,15 +1,10 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType -import solutions.bitbadger.documents.integration.JsonDocument -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase -import solutions.bitbadger.documents.common.query.Count -import solutions.bitbadger.documents.common.query.Delete -import solutions.bitbadger.documents.common.query.Find +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.java.integration.JsonDocument +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.Results +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull diff --git a/src/test/kotlin/integration/common/Definition.kt b/src/java/src/test/kotlin/integration/common/Definition.kt similarity index 85% rename from src/test/kotlin/integration/common/Definition.kt rename to src/java/src/test/kotlin/integration/common/Definition.kt index e667ce8..6f36226 100644 --- a/src/test/kotlin/integration/common/Definition.kt +++ b/src/java/src/test/kotlin/integration/common/Definition.kt @@ -1,9 +1,11 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.DocumentIndex -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.ensureDocumentIndex +import solutions.bitbadger.documents.java.ensureFieldIndex +import solutions.bitbadger.documents.java.ensureTable +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.assertFalse import kotlin.test.assertTrue diff --git a/src/test/kotlin/integration/common/Delete.kt b/src/java/src/test/kotlin/integration/common/Delete.kt similarity index 90% rename from src/test/kotlin/integration/common/Delete.kt rename to src/java/src/test/kotlin/integration/common/Delete.kt index 1e69426..2ea4338 100644 --- a/src/test/kotlin/integration/common/Delete.kt +++ b/src/java/src/test/kotlin/integration/common/Delete.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.integration.JsonDocument -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.integration.JsonDocument +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.assertEquals /** diff --git a/src/test/kotlin/integration/common/Document.kt b/src/java/src/test/kotlin/integration/common/Document.kt similarity index 97% rename from src/test/kotlin/integration/common/Document.kt rename to src/java/src/test/kotlin/integration/common/Document.kt index bb3c41b..e698525 100644 --- a/src/test/kotlin/integration/common/Document.kt +++ b/src/java/src/test/kotlin/integration/common/Document.kt @@ -1,8 +1,10 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.integration.* +import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.integration.* import kotlin.test.* /** diff --git a/src/test/kotlin/integration/common/Exists.kt b/src/java/src/test/kotlin/integration/common/Exists.kt similarity index 80% rename from src/test/kotlin/integration/common/Exists.kt rename to src/java/src/test/kotlin/integration/common/Exists.kt index 2dc51b8..3d5513e 100644 --- a/src/test/kotlin/integration/common/Exists.kt +++ b/src/java/src/test/kotlin/integration/common/Exists.kt @@ -1,10 +1,13 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.integration.JsonDocument -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.existsByContains +import solutions.bitbadger.documents.java.existsByFields +import solutions.bitbadger.documents.java.existsById +import solutions.bitbadger.documents.java.existsByJsonPath +import solutions.bitbadger.documents.java.integration.JsonDocument +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.assertFalse import kotlin.test.assertTrue diff --git a/src/test/kotlin/integration/common/Find.kt b/src/java/src/test/kotlin/integration/common/Find.kt similarity index 97% rename from src/test/kotlin/integration/common/Find.kt rename to src/java/src/test/kotlin/integration/common/Find.kt index 839991d..d7f124e 100644 --- a/src/test/kotlin/integration/common/Find.kt +++ b/src/java/src/test/kotlin/integration/common/Find.kt @@ -1,9 +1,11 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.common.FieldMatch import solutions.bitbadger.documents.integration.* +import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.integration.* import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -211,7 +213,8 @@ object Find { fun firstByFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val doc = db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf( + 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") diff --git a/src/test/kotlin/integration/common/Patch.kt b/src/java/src/test/kotlin/integration/common/Patch.kt similarity index 92% rename from src/test/kotlin/integration/common/Patch.kt rename to src/java/src/test/kotlin/integration/common/Patch.kt index 37d7315..9a4144c 100644 --- a/src/test/kotlin/integration/common/Patch.kt +++ b/src/java/src/test/kotlin/integration/common/Patch.kt @@ -1,10 +1,11 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.integration.JsonDocument -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.integration.JsonDocument +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull diff --git a/src/test/kotlin/integration/common/RemoveFields.kt b/src/java/src/test/kotlin/integration/common/RemoveFields.kt similarity index 93% rename from src/test/kotlin/integration/common/RemoveFields.kt rename to src/java/src/test/kotlin/integration/common/RemoveFields.kt index e8381a1..022c484 100644 --- a/src/test/kotlin/integration/common/RemoveFields.kt +++ b/src/java/src/test/kotlin/integration/common/RemoveFields.kt @@ -1,10 +1,11 @@ -package solutions.bitbadger.documents.integration.common +package solutions.bitbadger.documents.java.integration.common import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.integration.JsonDocument -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.integration.JsonDocument +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.* diff --git a/src/test/kotlin/integration/postgresql/CountIT.kt b/src/java/src/test/kotlin/integration/postgresql/CountIT.kt similarity index 90% rename from src/test/kotlin/integration/postgresql/CountIT.kt rename to src/java/src/test/kotlin/integration/postgresql/CountIT.kt index 9b4dc33..bb6e416 100644 --- a/src/test/kotlin/integration/postgresql/CountIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/CountIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Count +import solutions.bitbadger.documents.java.integration.common.Count import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/postgresql/CustomIT.kt b/src/java/src/test/kotlin/integration/postgresql/CustomIT.kt similarity index 89% rename from src/test/kotlin/integration/postgresql/CustomIT.kt rename to src/java/src/test/kotlin/integration/postgresql/CustomIT.kt index 5729df9..803bfb1 100644 --- a/src/test/kotlin/integration/postgresql/CustomIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/CustomIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Custom +import solutions.bitbadger.documents.java.integration.common.Custom import kotlin.test.Test diff --git a/src/test/kotlin/integration/postgresql/DefinitionIT.kt b/src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt similarity index 86% rename from src/test/kotlin/integration/postgresql/DefinitionIT.kt rename to src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt index 363c92d..9c94abc 100644 --- a/src/test/kotlin/integration/postgresql/DefinitionIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Definition +import solutions.bitbadger.documents.java.integration.common.Definition import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/postgresql/DeleteIT.kt b/src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt similarity index 90% rename from src/test/kotlin/integration/postgresql/DeleteIT.kt rename to src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt index 707be57..803400c 100644 --- a/src/test/kotlin/integration/postgresql/DeleteIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Delete +import solutions.bitbadger.documents.java.integration.common.Delete import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/postgresql/DocumentIT.kt b/src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt similarity index 91% rename from src/test/kotlin/integration/postgresql/DocumentIT.kt rename to src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt index 36d15af..382d64b 100644 --- a/src/test/kotlin/integration/postgresql/DocumentIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Document +import solutions.bitbadger.documents.java.integration.common.Document import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/postgresql/ExistsIT.kt b/src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt similarity index 91% rename from src/test/kotlin/integration/postgresql/ExistsIT.kt rename to src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt index be02740..757ec75 100644 --- a/src/test/kotlin/integration/postgresql/ExistsIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Exists +import solutions.bitbadger.documents.java.integration.common.Exists import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/postgresql/FindIT.kt b/src/java/src/test/kotlin/integration/postgresql/FindIT.kt similarity index 97% rename from src/test/kotlin/integration/postgresql/FindIT.kt rename to src/java/src/test/kotlin/integration/postgresql/FindIT.kt index 6b00cc3..c6a95fe 100644 --- a/src/test/kotlin/integration/postgresql/FindIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/FindIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Find +import solutions.bitbadger.documents.java.integration.common.Find import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/postgresql/PatchIT.kt b/src/java/src/test/kotlin/integration/postgresql/PatchIT.kt similarity index 90% rename from src/test/kotlin/integration/postgresql/PatchIT.kt rename to src/java/src/test/kotlin/integration/postgresql/PatchIT.kt index df1c79d..d650355 100644 --- a/src/test/kotlin/integration/postgresql/PatchIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/PatchIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Patch +import solutions.bitbadger.documents.java.integration.common.Patch import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/postgresql/PgDB.kt b/src/java/src/test/kotlin/integration/postgresql/PgDB.kt similarity index 88% rename from src/test/kotlin/integration/postgresql/PgDB.kt rename to src/java/src/test/kotlin/integration/postgresql/PgDB.kt index c72af32..7fecb54 100644 --- a/src/test/kotlin/integration/postgresql/PgDB.kt +++ b/src/java/src/test/kotlin/integration/postgresql/PgDB.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Parameter import solutions.bitbadger.documents.common.ParameterType -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase /** * A wrapper for a throwaway PostgreSQL database diff --git a/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt b/src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt similarity index 94% rename from src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt rename to src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt index 9719447..3a10709 100644 --- a/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.postgresql +package solutions.bitbadger.documents.java.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.RemoveFields +import solutions.bitbadger.documents.java.integration.common.RemoveFields import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/CountIT.kt b/src/java/src/test/kotlin/integration/sqlite/CountIT.kt similarity index 89% rename from src/test/kotlin/integration/sqlite/CountIT.kt rename to src/java/src/test/kotlin/integration/sqlite/CountIT.kt index 7b08ddf..62fdb63 100644 --- a/src/test/kotlin/integration/sqlite/CountIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/CountIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.integration.common.Count +import solutions.bitbadger.documents.java.integration.common.Count import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/CustomIT.kt b/src/java/src/test/kotlin/integration/sqlite/CustomIT.kt similarity index 89% rename from src/test/kotlin/integration/sqlite/CustomIT.kt rename to src/java/src/test/kotlin/integration/sqlite/CustomIT.kt index ab37b5f..21adab3 100644 --- a/src/test/kotlin/integration/sqlite/CustomIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/CustomIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Custom +import solutions.bitbadger.documents.java.integration.common.Custom import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/DefinitionIT.kt b/src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt similarity index 88% rename from src/test/kotlin/integration/sqlite/DefinitionIT.kt rename to src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt index 0cd15fa..3e553ed 100644 --- a/src/test/kotlin/integration/sqlite/DefinitionIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.integration.common.Definition +import solutions.bitbadger.documents.java.integration.common.Definition import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/DeleteIT.kt b/src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt similarity index 90% rename from src/test/kotlin/integration/sqlite/DeleteIT.kt rename to src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt index 40c100e..b3d056a 100644 --- a/src/test/kotlin/integration/sqlite/DeleteIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.integration.common.Delete +import solutions.bitbadger.documents.java.integration.common.Delete import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/DocumentIT.kt b/src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt similarity index 91% rename from src/test/kotlin/integration/sqlite/DocumentIT.kt rename to src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt index 402cd6e..1745237 100644 --- a/src/test/kotlin/integration/sqlite/DocumentIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.integration.common.Document +import solutions.bitbadger.documents.java.integration.common.Document import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/ExistsIT.kt b/src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt similarity index 90% rename from src/test/kotlin/integration/sqlite/ExistsIT.kt rename to src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt index 42bf633..55933ac 100644 --- a/src/test/kotlin/integration/sqlite/ExistsIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.integration.common.Exists +import solutions.bitbadger.documents.java.integration.common.Exists import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/FindIT.kt b/src/java/src/test/kotlin/integration/sqlite/FindIT.kt similarity index 96% rename from src/test/kotlin/integration/sqlite/FindIT.kt rename to src/java/src/test/kotlin/integration/sqlite/FindIT.kt index 6750c03..aef0003 100644 --- a/src/test/kotlin/integration/sqlite/FindIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/FindIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.integration.common.Find +import solutions.bitbadger.documents.java.integration.common.Find import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/PatchIT.kt b/src/java/src/test/kotlin/integration/sqlite/PatchIT.kt similarity index 90% rename from src/test/kotlin/integration/sqlite/PatchIT.kt rename to src/java/src/test/kotlin/integration/sqlite/PatchIT.kt index 76e9c27..ea1b147 100644 --- a/src/test/kotlin/integration/sqlite/PatchIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/PatchIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.integration.common.Patch +import solutions.bitbadger.documents.java.integration.common.Patch import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt b/src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt similarity index 92% rename from src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt rename to src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt index 096d1fb..3fbad3c 100644 --- a/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.integration.common.RemoveFields +import solutions.bitbadger.documents.java.integration.common.RemoveFields import kotlin.test.Test /** diff --git a/src/test/kotlin/integration/sqlite/SQLiteDB.kt b/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt similarity index 81% rename from src/test/kotlin/integration/sqlite/SQLiteDB.kt rename to src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt index e6be940..34d977b 100644 --- a/src/test/kotlin/integration/sqlite/SQLiteDB.kt +++ b/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.integration.sqlite +package solutions.bitbadger.documents.java.integration.sqlite import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Parameter import solutions.bitbadger.documents.common.ParameterType -import solutions.bitbadger.documents.integration.TEST_TABLE -import solutions.bitbadger.documents.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.integration.TEST_TABLE +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import java.io.File /** diff --git a/src/kotlin/pom.xml b/src/kotlin/pom.xml new file mode 100644 index 0000000..88bafc0 --- /dev/null +++ b/src/kotlin/pom.xml @@ -0,0 +1,119 @@ + + + 4.0.0 + + solutions.bitbadger.documents + kotlin + 4.0.0-alpha1-SNAPSHOT + jar + + + solutions.bitbadger + documents + 4.0.0-alpha1-SNAPSHOT + + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (Kotlin Library) + https://bitbadger.solutions/open-source/relational-documents/jvm/ + + + 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 + common + system + ${project.basedir}/../common/target/common-4.0.0-alpha1-SNAPSHOT.jar + jar + + + solutions.bitbadger.documents + java + system + ${project.basedir}/../java/target/java-4.0.0-alpha1-SNAPSHOT.jar + jar + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + process-sources + + compile + + + + ${project.basedir}/src/main/kotlin + + + + + test-compile + test-compile + + 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 + 2.22.2 + + + maven-failsafe-plugin + 2.22.2 + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + + diff --git a/src/main/kotlin/Configuration.kt b/src/kotlin/src/main/kotlin/Configuration.kt similarity index 68% rename from src/main/kotlin/Configuration.kt rename to src/kotlin/src/main/kotlin/Configuration.kt index 705a1d9..f8becb9 100644 --- a/src/main/kotlin/Configuration.kt +++ b/src/kotlin/src/main/kotlin/Configuration.kt @@ -1,12 +1,6 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.kotlin import kotlinx.serialization.json.Json -import solutions.bitbadger.documents.common.AutoId -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import java.sql.Connection -import java.sql.DriverManager -import kotlin.jvm.Throws object Configuration { diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/kotlin/src/main/kotlin/ConnectionExtensions.kt similarity index 99% rename from src/main/kotlin/ConnectionExtensions.kt rename to src/kotlin/src/main/kotlin/ConnectionExtensions.kt index c63406b..28e951b 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/kotlin/src/main/kotlin/ConnectionExtensions.kt @@ -1,9 +1,10 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.kotlin import solutions.bitbadger.documents.common.DocumentIndex import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.common.FieldMatch import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.java.Document import java.sql.Connection import java.sql.ResultSet diff --git a/src/main/kotlin/Count.kt b/src/kotlin/src/main/kotlin/Count.kt similarity index 97% rename from src/main/kotlin/Count.kt rename to src/kotlin/src/main/kotlin/Count.kt index b865af0..e2ac2d5 100644 --- a/src/main/kotlin/Count.kt +++ b/src/kotlin/src/main/kotlin/Count.kt @@ -1,10 +1,12 @@ -package solutions.bitbadger.documents +package solutions.bitbadger.documents.kotlin +import solutions.bitbadger.documents.common.Configuration import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.common.FieldMatch import solutions.bitbadger.documents.common.Parameter import solutions.bitbadger.documents.common.ParameterType import solutions.bitbadger.documents.common.query.Count + import java.sql.Connection /** diff --git a/src/kotlin/src/main/kotlin/Custom.kt b/src/kotlin/src/main/kotlin/Custom.kt new file mode 100644 index 0000000..b3f75fd --- /dev/null +++ b/src/kotlin/src/main/kotlin/Custom.kt @@ -0,0 +1,121 @@ +package solutions.bitbadger.documents.kotlin + +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.java.Custom +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 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 + */ + inline fun list( + query: String, + parameters: Collection> = listOf(), + conn: Connection, + noinline mapFunc: (ResultSet, Class) -> TDoc + ) = Custom.list(query, parameters, TDoc::class.java, 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 + */ + inline fun list( + query: String, + parameters: Collection> = listOf(), + noinline mapFunc: (ResultSet, Class) -> TDoc + ) = Configuration.dbConn().use { list(query, parameters, 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 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, + noinline mapFunc: (ResultSet, Class) -> TDoc + ) = Custom.single(query, parameters, TDoc::class.java, conn, 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 single( + query: String, + parameters: Collection> = listOf(), + noinline mapFunc: (ResultSet, Class) -> TDoc + ) = Configuration.dbConn().use { single(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 + */ + fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) = + Custom.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, + noinline mapFunc: (ResultSet, Class) -> T + ) = Custom.scalar(query, parameters, T::class.java, conn, mapFunc) + + /** + * 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(), noinline mapFunc: (ResultSet, Class) -> T + ) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) } +} diff --git a/src/kotlin/src/main/kotlin/Definition.kt b/src/kotlin/src/main/kotlin/Definition.kt new file mode 100644 index 0000000..07af62a --- /dev/null +++ b/src/kotlin/src/main/kotlin/Definition.kt @@ -0,0 +1,70 @@ +import solutions.bitbadger.documents.common.DocumentIndex +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) = + Configuration.dialect("ensure $tableName exists").let { + conn.customNonQuery(Definition.ensureTable(tableName, it)) + conn.customNonQuery(Definition.ensureKey(tableName, it)) + } + + /** + * Create a document table if necessary + * + * @param tableName The table whose existence should be ensured (may include schema) + */ + 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 + */ + fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection, conn: Connection) = + conn.customNonQuery(Definition.ensureIndexOn(tableName, indexName, fields)) + + /** + * 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) = + 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 + */ + fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) = + conn.customNonQuery(Definition.ensureDocumentIndexOn(tableName, indexType)) + + /** + * 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) = + Configuration.dbConn().use { ensureDocumentIndex(tableName, indexType, it) } +} diff --git a/src/main/kotlin/Delete.kt b/src/kotlin/src/main/kotlin/Delete.kt similarity index 97% rename from src/main/kotlin/Delete.kt rename to src/kotlin/src/main/kotlin/Delete.kt index eca0352..07288a2 100644 --- a/src/main/kotlin/Delete.kt +++ b/src/kotlin/src/main/kotlin/Delete.kt @@ -1,10 +1,7 @@ -package solutions.bitbadger.documents - import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.common.FieldMatch import solutions.bitbadger.documents.common.Parameter import solutions.bitbadger.documents.common.ParameterType -import solutions.bitbadger.documents.common.query.Delete import java.sql.Connection /** diff --git a/src/main/kotlin/Exists.kt b/src/kotlin/src/main/kotlin/Exists.kt similarity index 98% rename from src/main/kotlin/Exists.kt rename to src/kotlin/src/main/kotlin/Exists.kt index 4b0072a..dfb1eb2 100644 --- a/src/main/kotlin/Exists.kt +++ b/src/kotlin/src/main/kotlin/Exists.kt @@ -1,10 +1,7 @@ -package solutions.bitbadger.documents - import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.common.FieldMatch import solutions.bitbadger.documents.common.Parameter import solutions.bitbadger.documents.common.ParameterType -import solutions.bitbadger.documents.common.query.Exists import java.sql.Connection /** diff --git a/src/main/kotlin/Find.kt b/src/kotlin/src/main/kotlin/Find.kt similarity index 99% rename from src/main/kotlin/Find.kt rename to src/kotlin/src/main/kotlin/Find.kt index 6135145..3e58e71 100644 --- a/src/main/kotlin/Find.kt +++ b/src/kotlin/src/main/kotlin/Find.kt @@ -1,11 +1,8 @@ -package solutions.bitbadger.documents - import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.common.FieldMatch import solutions.bitbadger.documents.common.Parameter import solutions.bitbadger.documents.common.ParameterType import java.sql.Connection -import solutions.bitbadger.documents.common.query.Find import solutions.bitbadger.documents.common.query.orderBy /** diff --git a/src/main/kotlin/Parameters.kt b/src/kotlin/src/main/kotlin/Parameters.kt similarity index 98% rename from src/main/kotlin/Parameters.kt rename to src/kotlin/src/main/kotlin/Parameters.kt index f31a5db..46cd843 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/kotlin/src/main/kotlin/Parameters.kt @@ -1,7 +1,4 @@ -package solutions.bitbadger.documents - import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.ParameterName import java.sql.Connection import java.sql.PreparedStatement import java.sql.SQLException diff --git a/src/main/kotlin/Patch.kt b/src/kotlin/src/main/kotlin/Patch.kt similarity index 98% rename from src/main/kotlin/Patch.kt rename to src/kotlin/src/main/kotlin/Patch.kt index 4b23b78..5217b4a 100644 --- a/src/main/kotlin/Patch.kt +++ b/src/kotlin/src/main/kotlin/Patch.kt @@ -1,10 +1,7 @@ -package solutions.bitbadger.documents - import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.common.FieldMatch import solutions.bitbadger.documents.common.Parameter import solutions.bitbadger.documents.common.ParameterType -import solutions.bitbadger.documents.common.query.Patch import java.sql.Connection /** diff --git a/src/main/kotlin/RemoveFields.kt b/src/kotlin/src/main/kotlin/RemoveFields.kt similarity index 98% rename from src/main/kotlin/RemoveFields.kt rename to src/kotlin/src/main/kotlin/RemoveFields.kt index 486351a..d75c63b 100644 --- a/src/main/kotlin/RemoveFields.kt +++ b/src/kotlin/src/main/kotlin/RemoveFields.kt @@ -1,7 +1,4 @@ -package solutions.bitbadger.documents - import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.RemoveFields import java.sql.Connection /** diff --git a/src/kotlin/src/main/kotlin/Results.kt b/src/kotlin/src/main/kotlin/Results.kt new file mode 100644 index 0000000..f961b00 --- /dev/null +++ b/src/kotlin/src/main/kotlin/Results.kt @@ -0,0 +1,80 @@ +package solutions.bitbadger.documents.kotlin + +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.java.Results +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 + * @param rs A `ResultSet` set to the row with the document to be constructed + * @return The constructed domain item + */ + inline fun fromDocument(field: String): (ResultSet, Class) -> TDoc = + { rs, _ -> Results.fromDocument(field, rs, TDoc::class.java) } + + /** + * 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 + */ + inline fun fromData(rs: ResultSet, clazz: Class = TDoc::class.java) = + Results.fromDocument("data", rs, TDoc::class.java) + + /** + * 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, clazz: Class = Long::class.java) = + 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, clazz: Class = Boolean::class.java) = + when (Configuration.dialect()) { + Dialect.POSTGRESQL -> rs.getBoolean("it") + Dialect.SQLITE -> toCount(rs) > 0L + } +} diff --git a/src/pom.xml b/src/pom.xml index 60ce46a..544ca00 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -46,6 +46,7 @@ common + java diff --git a/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java b/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java index c24b49c..fe67b44 100644 --- a/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java +++ b/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java @@ -2,7 +2,6 @@ package solutions.bitbadger.documents.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.*; import solutions.bitbadger.documents.common.Dialect; import solutions.bitbadger.documents.common.DocumentException; diff --git a/src/test/java/solutions/bitbadger/documents/java/integration/common/Count.java b/src/test/java/solutions/bitbadger/documents/java/integration/common/Count.java deleted file mode 100644 index 8258386..0000000 --- a/src/test/java/solutions/bitbadger/documents/java/integration/common/Count.java +++ /dev/null @@ -1,20 +0,0 @@ -package solutions.bitbadger.documents.java.integration.common; - -import solutions.bitbadger.documents.integration.ThrowawayDatabase; -import solutions.bitbadger.documents.java.testDocs.JsonDocument; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static solutions.bitbadger.documents.integration.TypesKt.TEST_TABLE; - -final public class Count { - - public static void all(ThrowawayDatabase db) { - JsonDocument.load(db); - assertEquals(5L, solutions.bitbadger.documents.Count.all(TEST_TABLE, db.getConn()), - "There should have been 5 documents in the table"); - } - - - private Count() { - } -} -- 2.47.2 From 5bcbc5cffc4a0e4420452c004333a02f3e0e84e2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 12 Mar 2025 23:08:52 -0400 Subject: [PATCH 38/88] Nearly all compile errors resolved --- src/common/src/main/kotlin/Configuration.kt | 1 - src/java/pom.xml | 12 - .../src/main/kotlin/ConnectionExtensions.kt | 133 ++++++---- src/java/src/main/kotlin/Count.kt | 24 +- src/java/src/main/kotlin/Definition.kt | 15 +- src/java/src/main/kotlin/Delete.kt | 28 ++- src/java/src/main/kotlin/Exists.kt | 33 ++- src/java/src/main/kotlin/Find.kt | 234 +++++++++++------- src/java/src/main/kotlin/Patch.kt | 42 ++-- src/java/src/main/kotlin/RemoveFields.kt | 24 +- src/java/src/main/kotlin/Results.kt | 8 +- .../test/kotlin/integration/common/Custom.kt | 28 ++- .../kotlin/integration/common/Document.kt | 18 +- .../test/kotlin/integration/common/Find.kt | 114 +++++---- .../test/kotlin/integration/common/Patch.kt | 7 +- .../kotlin/integration/common/RemoveFields.kt | 9 +- .../kotlin/integration/postgresql/CountIT.kt | 2 +- .../kotlin/integration/postgresql/CustomIT.kt | 2 +- .../integration/postgresql/DefinitionIT.kt | 2 +- .../kotlin/integration/postgresql/DeleteIT.kt | 2 +- .../integration/postgresql/DocumentIT.kt | 2 +- .../kotlin/integration/postgresql/ExistsIT.kt | 2 +- .../kotlin/integration/postgresql/FindIT.kt | 2 +- .../kotlin/integration/postgresql/PatchIT.kt | 2 +- .../kotlin/integration/postgresql/PgDB.kt | 7 +- .../integration/postgresql/RemoveFieldsIT.kt | 2 +- .../test/kotlin/integration/sqlite/CountIT.kt | 2 +- .../kotlin/integration/sqlite/CustomIT.kt | 2 +- .../kotlin/integration/sqlite/DefinitionIT.kt | 2 +- .../kotlin/integration/sqlite/DeleteIT.kt | 2 +- .../kotlin/integration/sqlite/DocumentIT.kt | 2 +- .../kotlin/integration/sqlite/ExistsIT.kt | 2 +- .../test/kotlin/integration/sqlite/FindIT.kt | 2 +- .../test/kotlin/integration/sqlite/PatchIT.kt | 2 +- .../integration/sqlite/RemoveFieldsIT.kt | 2 +- .../kotlin/integration/sqlite/SQLiteDB.kt | 7 +- src/main/kotlin/Results.kt | 79 ------ 37 files changed, 487 insertions(+), 372 deletions(-) delete mode 100644 src/main/kotlin/Results.kt diff --git a/src/common/src/main/kotlin/Configuration.kt b/src/common/src/main/kotlin/Configuration.kt index 683a386..c4969a1 100644 --- a/src/common/src/main/kotlin/Configuration.kt +++ b/src/common/src/main/kotlin/Configuration.kt @@ -2,7 +2,6 @@ package solutions.bitbadger.documents.common import java.sql.Connection import java.sql.DriverManager -import kotlin.jvm.Throws /** * Configuration for the document library diff --git a/src/java/pom.xml b/src/java/pom.xml index 0b272c0..b382510 100644 --- a/src/java/pom.xml +++ b/src/java/pom.xml @@ -71,18 +71,6 @@ - - - kotlinx-serialization - - - - - org.jetbrains.kotlin - kotlin-maven-serialization - ${kotlin.version} - - maven-surefire-plugin diff --git a/src/java/src/main/kotlin/ConnectionExtensions.kt b/src/java/src/main/kotlin/ConnectionExtensions.kt index a5230c2..f25e658 100644 --- a/src/java/src/main/kotlin/ConnectionExtensions.kt +++ b/src/java/src/main/kotlin/ConnectionExtensions.kt @@ -1,11 +1,8 @@ +@file:JvmName("ConnExt") + package solutions.bitbadger.documents.java -import solutions.bitbadger.documents.Custom -import solutions.bitbadger.documents.common.DocumentIndex -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.common.* import java.sql.Connection import java.sql.ResultSet @@ -16,24 +13,32 @@ import java.sql.ResultSet * * @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 */ -inline fun Connection.customList( - query: String, parameters: Collection> = listOf(), noinline mapFunc: (ResultSet, Class) -> TDoc -) = Custom.list(query, parameters, this, mapFunc) +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 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 */ -inline fun Connection.customSingle( - query: String, parameters: Collection> = listOf(), noinline mapFunc: (ResultSet, Class) -> TDoc -) = Custom.single(query, parameters, this, mapFunc) +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 no results @@ -49,14 +54,16 @@ fun Connection.customNonQuery(query: String, parameters: Collection * * @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 */ -inline fun Connection.customScalar( +fun Connection.customScalar( query: String, parameters: Collection> = listOf(), - noinline mapFunc: (ResultSet, Class) -> T -) = Custom.scalar(query, parameters, this, mapFunc) + clazz: Class, + mapFunc: (ResultSet, Class) -> T +) = Custom.scalar(query, parameters, clazz, this, mapFunc) // ~~~ DEFINITION QUERIES ~~~ @@ -66,7 +73,7 @@ inline fun Connection.customScalar( * @param tableName The table whose existence should be ensured (may include schema) */ fun Connection.ensureTable(tableName: String) = - solutions.bitbadger.documents.java.Definition.ensureTable(tableName, this) + Definition.ensureTable(tableName, this) /** * Create an index on field(s) within documents in the specified table if necessary @@ -76,7 +83,7 @@ fun Connection.ensureTable(tableName: String) = * @param fields One or more fields to be indexed< */ fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection) = - solutions.bitbadger.documents.java.Definition.ensureFieldIndex(tableName, indexName, fields, this) + Definition.ensureFieldIndex(tableName, indexName, fields, this) /** * Create a document index on a table (PostgreSQL only) @@ -85,8 +92,9 @@ fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Co * @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) = - solutions.bitbadger.documents.java.Definition.ensureDocumentIndex(tableName, indexType, this) + Definition.ensureDocumentIndex(tableName, indexType, this) // ~~~ DOCUMENT MANIPULATION QUERIES ~~~ @@ -96,7 +104,7 @@ fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) * @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) = +fun Connection.insert(tableName: String, document: TDoc) = Document.insert(tableName, document, this) /** @@ -105,7 +113,7 @@ inline fun Connection.insert(tableName: String, document: TDoc) = * @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) = +fun Connection.save(tableName: String, document: TDoc) = Document.save(tableName, document, this) /** @@ -115,7 +123,7 @@ inline fun Connection.save(tableName: String, document: TDoc) = * @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) = +fun Connection.update(tableName: String, docId: TKey, document: TDoc) = Document.update(tableName, docId, document, this) // ~~~ DOCUMENT COUNT QUERIES ~~~ @@ -148,7 +156,8 @@ fun Connection.countByFields(tableName: String, fields: Collection>, ho * @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) = +@Throws(DocumentException::class) +fun Connection.countByContains(tableName: String, criteria: TContains) = Count.byContains(tableName, criteria, this) /** @@ -159,6 +168,7 @@ inline fun Connection.countByContains(tableName: String, cri * @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) @@ -193,7 +203,8 @@ fun Connection.existsByFields(tableName: String, fields: Collection>, h * @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) = +@Throws(DocumentException::class) +fun Connection.existsByContains(tableName: String, criteria: TContains) = Exists.byContains(tableName, criteria, this) /** @@ -204,6 +215,7 @@ inline fun Connection.existsByContains(tableName: String, cr * @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) @@ -213,119 +225,145 @@ fun Connection.existsByJsonPath(tableName: String, path: String) = * 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 */ -inline fun Connection.findAll(tableName: String, orderBy: Collection>? = null) = - solutions.bitbadger.documents.java.Find.all(tableName, orderBy, this) +@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 The document if it is found, `null` otherwise */ -inline fun Connection.findById(tableName: String, docId: TKey) = - solutions.bitbadger.documents.java.Find.byId(tableName, docId, this) +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 */ -inline fun Connection.findByFields( +@JvmOverloads +fun Connection.findByFields( tableName: String, fields: Collection>, + clazz: Class, howMatched: FieldMatch? = null, orderBy: Collection>? = null ) = - solutions.bitbadger.documents.java.Find.byFields(tableName, fields, howMatched, orderBy, this) + 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 */ -inline fun Connection.findByContains( +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findByContains( tableName: String, criteria: TContains, + clazz: Class, orderBy: Collection>? = null ) = - solutions.bitbadger.documents.java.Find.byContains(tableName, criteria, orderBy, this) + 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 */ -inline fun Connection.findByJsonPath( +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findByJsonPath( tableName: String, path: String, + clazz: Class, orderBy: Collection>? = null ) = - solutions.bitbadger.documents.java.Find.byJsonPath(tableName, path, orderBy, this) + 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 The first document matching the field comparison, or `null` if no matches are found */ -inline fun Connection.findFirstByFields( +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findFirstByFields( tableName: String, fields: Collection>, + clazz: Class, howMatched: FieldMatch? = null, orderBy: Collection>? = null ) = - solutions.bitbadger.documents.java.Find.firstByFields(tableName, fields, howMatched, orderBy, this) + 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 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( +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findFirstByContains( tableName: String, criteria: TContains, + clazz: Class, orderBy: Collection>? = null ) = - solutions.bitbadger.documents.java.Find.firstByContains(tableName, criteria, orderBy, this) + 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 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( +@Throws(DocumentException::class) +@JvmOverloads +fun Connection.findFirstByJsonPath( tableName: String, path: String, + clazz: Class, orderBy: Collection>? = null ) = - solutions.bitbadger.documents.java.Find.firstByJsonPath(tableName, path, orderBy, this) + Find.firstByJsonPath(tableName, path, clazz, orderBy, this) // ~~~ DOCUMENT PATCH (PARTIAL UPDATE) QUERIES ~~~ @@ -336,7 +374,7 @@ inline fun Connection.findFirstByJsonPath( * @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) = +fun Connection.patchById(tableName: String, docId: TKey, patch: TPatch) = Patch.byId(tableName, docId, patch, this) /** @@ -347,7 +385,7 @@ inline fun Connection.patchById(tableName: String, docId: * @param patch The object whose properties should be replaced in the document * @param howMatched How the fields should be matched */ -inline fun Connection.patchByFields( +fun Connection.patchByFields( tableName: String, fields: Collection>, patch: TPatch, @@ -363,7 +401,8 @@ inline fun Connection.patchByFields( * @param patch The object whose properties should be replaced in the document * @throws DocumentException If called on a SQLite connection */ -inline fun Connection.patchByContains( +@Throws(DocumentException::class) +fun Connection.patchByContains( tableName: String, criteria: TContains, patch: TPatch @@ -378,7 +417,8 @@ inline fun Connection.patchByContains( * @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) = +@Throws(DocumentException::class) +fun Connection.patchByJsonPath(tableName: String, path: String, patch: TPatch) = Patch.byJsonPath(tableName, path, patch, this) // ~~~ DOCUMENT FIELD REMOVAL QUERIES ~~~ @@ -417,7 +457,8 @@ fun Connection.removeFieldsByFields( * @param toRemove The names of the fields to be removed * @throws DocumentException If called on a SQLite connection */ -inline fun Connection.removeFieldsByContains( +@Throws(DocumentException::class) +fun Connection.removeFieldsByContains( tableName: String, criteria: TContains, toRemove: Collection @@ -432,6 +473,7 @@ inline fun Connection.removeFieldsByContains( * @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) @@ -463,7 +505,8 @@ fun Connection.deleteByFields(tableName: String, fields: Collection>, h * @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) = +@Throws(DocumentException::class) +fun Connection.deleteByContains(tableName: String, criteria: TContains) = Delete.byContains(tableName, criteria, this) /** diff --git a/src/java/src/main/kotlin/Count.kt b/src/java/src/main/kotlin/Count.kt index 5d42b38..df9bc16 100644 --- a/src/java/src/main/kotlin/Count.kt +++ b/src/java/src/main/kotlin/Count.kt @@ -1,10 +1,7 @@ package solutions.bitbadger.documents.java -import solutions.bitbadger.documents.Results -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.Count import java.sql.Connection /** @@ -21,7 +18,7 @@ object Count { */ @JvmStatic fun all(tableName: String, conn: Connection) = - conn.customScalar(all(tableName), mapFunc = Results::toCount) + conn.customScalar(Count.all(tableName), listOf(), Long::class.java, Results::toCount) /** * Count all documents in the table @@ -52,8 +49,9 @@ object Count { ): Long { val named = Parameters.nameFields(fields) return conn.customScalar( - byFields(tableName, named, howMatched), + Count.byFields(tableName, named, howMatched), Parameters.addFields(named), + Long::class.java, Results::toCount ) } @@ -81,8 +79,13 @@ object Count { * @throws DocumentException If called on a SQLite connection */ @JvmStatic - inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = - conn.customScalar(Count.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Results::toCount) + fun byContains(tableName: String, criteria: TContains, conn: Connection) = + conn.customScalar( + Count.byContains(tableName), + listOf(Parameters.json(":criteria", criteria)), + Long::class.java, + Results::toCount + ) /** * Count documents using a JSON containment query (PostgreSQL only) @@ -93,7 +96,7 @@ object Count { * @throws DocumentException If called on a SQLite connection */ @JvmStatic - inline fun byContains(tableName: String, criteria: TContains) = + fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } /** @@ -110,6 +113,7 @@ object Count { conn.customScalar( Count.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), + Long::class.java, Results::toCount ) diff --git a/src/java/src/main/kotlin/Definition.kt b/src/java/src/main/kotlin/Definition.kt index 7cd749b..531415d 100644 --- a/src/java/src/main/kotlin/Definition.kt +++ b/src/java/src/main/kotlin/Definition.kt @@ -1,6 +1,9 @@ package solutions.bitbadger.documents.java +import solutions.bitbadger.documents.common.Configuration +import solutions.bitbadger.documents.common.DocumentException import solutions.bitbadger.documents.common.DocumentIndex +import solutions.bitbadger.documents.common.query.Definition import java.sql.Connection /** @@ -14,6 +17,7 @@ object Definition { * @param tableName The table whose existence should be ensured (may include schema) * @param conn The connection on which the query should be executed */ + @JvmStatic fun ensureTable(tableName: String, conn: Connection) = Configuration.dialect("ensure $tableName exists").let { conn.customNonQuery(Definition.ensureTable(tableName, it)) @@ -25,6 +29,7 @@ object Definition { * * @param tableName The table whose existence should be ensured (may include schema) */ + @JvmStatic fun ensureTable(tableName: String) = Configuration.dbConn().use { ensureTable(tableName, it) } @@ -33,9 +38,10 @@ object Definition { * * @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 fields One or more fields to be indexed * @param conn The connection on which the query should be executed */ + @JvmStatic fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection, conn: Connection) = conn.customNonQuery(Definition.ensureIndexOn(tableName, indexName, fields)) @@ -44,8 +50,9 @@ object Definition { * * @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 fields One or more fields to be indexed */ + @JvmStatic fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection) = Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) } @@ -57,6 +64,8 @@ object Definition { * @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) = conn.customNonQuery(Definition.ensureDocumentIndexOn(tableName, indexType)) @@ -67,6 +76,8 @@ object Definition { * @param indexType The type of index to ensure * @throws DocumentException 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/java/src/main/kotlin/Delete.kt b/src/java/src/main/kotlin/Delete.kt index 6f9bd52..0587fb7 100644 --- a/src/java/src/main/kotlin/Delete.kt +++ b/src/java/src/main/kotlin/Delete.kt @@ -1,9 +1,7 @@ package solutions.bitbadger.documents.java -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.Delete import java.sql.Connection /** @@ -18,9 +16,10 @@ object Delete { * @param docId The ID of the document to be deleted * @param conn The connection on which the deletion should be executed */ + @JvmStatic fun byId(tableName: String, docId: TKey, conn: Connection) = conn.customNonQuery( - byId(tableName, docId), + Delete.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))) ) @@ -30,6 +29,7 @@ object Delete { * @param tableName The name of the table from which documents should be deleted * @param docId The ID of the document to be deleted */ + @JvmStatic fun byId(tableName: String, docId: TKey) = Configuration.dbConn().use { byId(tableName, docId, it) } @@ -41,9 +41,11 @@ object Delete { * @param howMatched How the fields should be matched * @param conn The connection on which the deletion should be executed */ + @JvmStatic + @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { val named = Parameters.nameFields(fields) - conn.customNonQuery(byFields(tableName, named, howMatched), Parameters.addFields(named)) + conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named)) } /** @@ -53,6 +55,8 @@ object Delete { * @param fields The fields which should be compared * @param howMatched How the fields should be matched */ + @JvmStatic + @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } @@ -64,7 +68,9 @@ object Delete { * @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) = + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customNonQuery(Delete.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) /** @@ -74,7 +80,9 @@ object Delete { * @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) = + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } /** @@ -85,6 +93,8 @@ object Delete { * @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) = conn.customNonQuery(Delete.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) @@ -95,6 +105,8 @@ object Delete { * @param path The JSON path comparison to match * @throws DocumentException 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/java/src/main/kotlin/Exists.kt b/src/java/src/main/kotlin/Exists.kt index 5f9deda..7e55f89 100644 --- a/src/java/src/main/kotlin/Exists.kt +++ b/src/java/src/main/kotlin/Exists.kt @@ -1,10 +1,7 @@ package solutions.bitbadger.documents.java -import solutions.bitbadger.documents.Results -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.Exists import java.sql.Connection /** @@ -20,10 +17,12 @@ object Exists { * @param conn The connection on which the existence check should be executed * @return True if the document exists, false if not */ + @JvmStatic fun byId(tableName: String, docId: TKey, conn: Connection) = conn.customScalar( - byId(tableName, docId), + Exists.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + Boolean::class.java, Results::toExists ) @@ -34,6 +33,7 @@ object Exists { * @param docId The ID of the document to be checked * @return True if the document exists, false if not */ + @JvmStatic fun byId(tableName: String, docId: TKey) = Configuration.dbConn().use { byId(tableName, docId, it) } @@ -46,6 +46,8 @@ object Exists { * @param conn The connection on which the existence check should be executed * @return True if any matching documents exist, false if not */ + @JvmStatic + @JvmOverloads fun byFields( tableName: String, fields: Collection>, @@ -54,8 +56,9 @@ object Exists { ): Boolean { val named = Parameters.nameFields(fields) return conn.customScalar( - byFields(tableName, named, howMatched), + Exists.byFields(tableName, named, howMatched), Parameters.addFields(named), + Boolean::class.java, Results::toExists ) } @@ -68,6 +71,8 @@ object Exists { * @param howMatched How the fields should be matched * @return True if any matching documents exist, false if not */ + @JvmStatic + @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } @@ -80,10 +85,13 @@ object Exists { * @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) = + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customScalar( Exists.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), + Boolean::class.java, Results::toExists ) @@ -95,7 +103,9 @@ object Exists { * @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) = + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } /** @@ -107,10 +117,13 @@ object Exists { * @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) = conn.customScalar( Exists.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), + Boolean::class.java, Results::toExists ) @@ -122,6 +135,8 @@ object Exists { * @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) = Configuration.dbConn().use { byJsonPath(tableName, path, it) } } diff --git a/src/java/src/main/kotlin/Find.kt b/src/java/src/main/kotlin/Find.kt index 87f74a5..70cef9c 100644 --- a/src/java/src/main/kotlin/Find.kt +++ b/src/java/src/main/kotlin/Find.kt @@ -1,12 +1,9 @@ package solutions.bitbadger.documents.java -import solutions.bitbadger.documents.Results -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType -import java.sql.Connection +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.Find import solutions.bitbadger.documents.common.query.orderBy +import java.sql.Connection /** * Functions to find and retrieve documents @@ -17,45 +14,55 @@ 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 */ - inline fun all(tableName: String, orderBy: Collection>? = null, conn: Connection) = - conn.customList(all(tableName) + (orderBy?.let(::orderBy) ?: ""), mapFunc = Results::fromData) + @JvmStatic + fun all(tableName: String, clazz: Class, orderBy: Collection>? = null, conn: Connection) = + conn.customList(Find.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), clazz, 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 */ - inline fun all(tableName: String, orderBy: Collection>? = null) = - Configuration.dbConn().use { all(tableName, orderBy, it) } + @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 */ - inline fun all(tableName: String, conn: Connection) = - all(tableName, null, conn) + @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 The document if it is found, `null` otherwise */ - inline fun byId(tableName: String, docId: TKey, conn: Connection) = - conn.customSingle( - byId(tableName, docId), + @JvmStatic + fun byId(tableName: String, docId: TKey, clazz: Class, conn: Connection) = + conn.customSingle( + Find.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + clazz, Results::fromData ) @@ -64,32 +71,38 @@ object Find { * * @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 The document if it is found, `null` otherwise */ - inline fun byId(tableName: String, docId: TKey) = - Configuration.dbConn().use { byId(tableName, docId, it) } + @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 */ - inline fun byFields( + @JvmStatic + fun byFields( tableName: String, fields: Collection>, + clazz: Class, howMatched: FieldMatch? = null, orderBy: Collection>? = null, conn: Connection ): List { val named = Parameters.nameFields(fields) - return conn.customList( - byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + return conn.customList( + Find.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), Parameters.addFields(named), + clazz, Results::fromData ) } @@ -99,69 +112,66 @@ object Find { * * @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 */ - inline fun byFields( + @JvmStatic + @JvmOverloads + fun byFields( tableName: String, fields: Collection>, + clazz: Class, howMatched: FieldMatch? = null, orderBy: Collection>? = null ) = - Configuration.dbConn().use { byFields(tableName, fields, howMatched, orderBy, it) } + 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 */ - inline fun byFields( + @JvmStatic + fun byFields( tableName: String, fields: Collection>, + clazz: Class, 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) } + 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 */ - inline fun byContains( + @Throws(DocumentException::class) + @JvmStatic + fun byContains( tableName: String, criteria: TContains, + clazz: Class, orderBy: Collection>? = null, conn: Connection ) = - conn.customList( + conn.customList( Find.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameters.json(":criteria", criteria)), + clazz, Results::fromData ) @@ -170,59 +180,61 @@ object Find { * * @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 called on a SQLite connection */ - inline fun byContains( + @Throws(DocumentException::class) + @JvmStatic + @JvmOverloads + fun byContains( tableName: String, criteria: TContains, + clazz: Class, orderBy: Collection>? = null ) = - Configuration.dbConn().use { byContains(tableName, criteria, orderBy, it) } + 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 */ - 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) } + @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 */ - inline fun byJsonPath( + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath( tableName: String, path: String, + clazz: Class, orderBy: Collection>? = null, conn: Connection ) = - conn.customList( - byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + conn.customList( + Find.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameter(":path", ParameterType.STRING, path)), + clazz, Results::fromData ) @@ -231,46 +243,57 @@ object Find { * * @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 */ - inline fun byJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = - Configuration.dbConn().use { byJsonPath(tableName, path, orderBy, it) } + @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 */ - inline fun byJsonPath(tableName: String, path: String, conn: Connection) = - byJsonPath(tableName, path, null, conn) + @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 The first document matching the field comparison, or `null` if no matches are found */ - inline fun firstByFields( + @JvmStatic + fun firstByFields( tableName: String, fields: Collection>, + clazz: Class, howMatched: FieldMatch? = null, orderBy: Collection>? = null, conn: Connection ): TDoc? { val named = Parameters.nameFields(fields) - return conn.customSingle( - byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + return conn.customSingle( + Find.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), Parameters.addFields(named), + clazz, Results::fromData ) } @@ -280,34 +303,41 @@ object Find { * * @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 The first document matching the field comparison, or `null` if no matches are found */ - inline fun firstByFields( + @JvmStatic + @JvmOverloads + fun firstByFields( tableName: String, fields: Collection>, + clazz: Class, howMatched: FieldMatch? = null, orderBy: Collection>? = null ) = - Configuration.dbConn().use { firstByFields(tableName, fields, howMatched, orderBy, it) } + 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 The first document matching the field comparison, or `null` if no matches are found */ - inline fun firstByFields( + @JvmStatic + fun firstByFields( tableName: String, fields: Collection>, + clazz: Class, howMatched: FieldMatch? = null, conn: Connection ) = - firstByFields(tableName, fields, howMatched, null, conn) + firstByFields(tableName, fields, clazz, howMatched, null, conn) /** * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) @@ -319,15 +349,19 @@ object Find { * @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( + @Throws(DocumentException::class) + @JvmStatic + fun firstByContains( tableName: String, criteria: TContains, + clazz: Class, orderBy: Collection>? = null, conn: Connection ) = - conn.customSingle( + conn.customSingle( Find.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameters.json(":criteria", criteria)), + clazz, Results::fromData ) @@ -336,44 +370,66 @@ object Find { * * @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 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) + @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 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) } + @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 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( + @Throws(DocumentException::class) + @JvmStatic + fun firstByJsonPath( tableName: String, path: String, + clazz: Class, orderBy: Collection>? = null, conn: Connection ) = - conn.customSingle( - byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + conn.customSingle( + Find.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameter(":path", ParameterType.STRING, path)), + clazz, Results::fromData ) @@ -382,22 +438,34 @@ object Find { * * @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 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) + @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 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) } + @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/java/src/main/kotlin/Patch.kt b/src/java/src/main/kotlin/Patch.kt index 7b48698..a59fc2f 100644 --- a/src/java/src/main/kotlin/Patch.kt +++ b/src/java/src/main/kotlin/Patch.kt @@ -1,9 +1,7 @@ package solutions.bitbadger.documents.java -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.Patch import java.sql.Connection /** @@ -19,7 +17,8 @@ object Patch { * @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) = + @JvmStatic + fun byId(tableName: String, docId: TKey, patch: TPatch, conn: Connection) = conn.customNonQuery( Patch.byId(tableName, docId), Parameters.addFields( @@ -35,7 +34,8 @@ object Patch { * @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) = + @JvmStatic + fun byId(tableName: String, docId: TKey, patch: TPatch) = Configuration.dbConn().use { byId(tableName, docId, patch, it) } /** @@ -47,7 +47,8 @@ object Patch { * @param howMatched How the fields should be matched * @param conn The connection on which the update should be executed */ - inline fun byFields( + @JvmStatic + fun byFields( tableName: String, fields: Collection>, patch: TPatch, @@ -56,7 +57,7 @@ object Patch { ) { val named = Parameters.nameFields(fields) conn.customNonQuery( - byFields(tableName, named, howMatched), Parameters.addFields( + Patch.byFields(tableName, named, howMatched), Parameters.addFields( named, mutableListOf(Parameters.json(":data", patch)) ) @@ -71,7 +72,9 @@ object Patch { * @param patch The object whose properties should be replaced in the document * @param howMatched How the fields should be matched */ - inline fun byFields( + @JvmStatic + @JvmOverloads + fun byFields( tableName: String, fields: Collection>, patch: TPatch, @@ -88,12 +91,9 @@ object Patch { * @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 - ) = + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, patch: TPatch, conn: Connection) = conn.customNonQuery( Patch.byContains(tableName), listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch)) @@ -107,7 +107,9 @@ object Patch { * @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) = + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, patch: TPatch) = Configuration.dbConn().use { byContains(tableName, criteria, patch, it) } /** @@ -119,7 +121,9 @@ object Patch { * @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) = + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, patch: TPatch, conn: Connection) = conn.customNonQuery( Patch.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch)) @@ -133,6 +137,8 @@ object Patch { * @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) = + @Throws(DocumentException::class) + @JvmStatic + fun byJsonPath(tableName: String, path: String, patch: TPatch) = Configuration.dbConn().use { byJsonPath(tableName, path, patch, it) } } diff --git a/src/java/src/main/kotlin/RemoveFields.kt b/src/java/src/main/kotlin/RemoveFields.kt index 0bb10f0..3a52eba 100644 --- a/src/java/src/main/kotlin/RemoveFields.kt +++ b/src/java/src/main/kotlin/RemoveFields.kt @@ -1,6 +1,7 @@ package solutions.bitbadger.documents.java import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.RemoveFields import java.sql.Connection /** @@ -18,7 +19,7 @@ object RemoveFields { val dialect = Configuration.dialect("remove fields") return when (dialect) { Dialect.POSTGRESQL -> parameters - Dialect.SQLITE -> parameters.map { Parameter(it.name, it.type, "$.${it.value}") }.toMutableList() + Dialect.SQLITE -> parameters.map { Parameter(it.name, it.type, "$.${it.value}") }.toMutableList() } } @@ -30,10 +31,11 @@ object RemoveFields { * @param toRemove The names of the fields to be removed * @param conn The connection on which the update should be executed */ + @JvmStatic fun byId(tableName: String, docId: TKey, toRemove: Collection, conn: Connection) { val nameParams = Parameters.fieldNames(toRemove) conn.customNonQuery( - byId(tableName, nameParams, docId), + RemoveFields.byId(tableName, nameParams, docId), Parameters.addFields( listOf(Field.equal(Configuration.idField, docId, ":id")), translatePath(nameParams) @@ -48,6 +50,7 @@ object RemoveFields { * @param docId The ID of the document to have fields removed * @param toRemove The names of the fields to be removed */ + @JvmStatic fun byId(tableName: String, docId: TKey, toRemove: Collection) = Configuration.dbConn().use { byId(tableName, docId, toRemove, it) } @@ -60,6 +63,7 @@ object RemoveFields { * @param howMatched How the fields should be matched * @param conn The connection on which the update should be executed */ + @JvmStatic fun byFields( tableName: String, fields: Collection>, @@ -70,7 +74,7 @@ object RemoveFields { val named = Parameters.nameFields(fields) val nameParams = Parameters.fieldNames(toRemove) conn.customNonQuery( - byFields(tableName, nameParams, named, howMatched), + RemoveFields.byFields(tableName, nameParams, named, howMatched), Parameters.addFields(named, translatePath(nameParams)) ) } @@ -83,6 +87,8 @@ object RemoveFields { * @param toRemove The names of the fields to be removed * @param howMatched How the fields should be matched */ + @JvmStatic + @JvmOverloads fun byFields( tableName: String, fields: Collection>, @@ -100,7 +106,9 @@ object RemoveFields { * @param conn The connection on which the update should be executed * @throws DocumentException If called on a SQLite connection */ - inline fun byContains( + @Throws(DocumentException::class) + @JvmStatic + fun byContains( tableName: String, criteria: TContains, toRemove: Collection, @@ -121,7 +129,9 @@ object RemoveFields { * @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) = + @Throws(DocumentException::class) + @JvmStatic + fun byContains(tableName: String, criteria: TContains, toRemove: Collection) = Configuration.dbConn().use { byContains(tableName, criteria, toRemove, it) } /** @@ -133,6 +143,8 @@ object RemoveFields { * @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) conn.customNonQuery( @@ -149,6 +161,8 @@ object RemoveFields { * @param toRemove The names of the fields to be removed * @throws DocumentException 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/java/src/main/kotlin/Results.kt b/src/java/src/main/kotlin/Results.kt index 6438eaa..028007c 100644 --- a/src/java/src/main/kotlin/Results.kt +++ b/src/java/src/main/kotlin/Results.kt @@ -63,12 +63,13 @@ object Results { * 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 */ @Throws(DocumentException::class) @JvmStatic - fun toCount(rs: ResultSet) = + fun toCount(rs: ResultSet, clazz: Class<*>) = when (Configuration.dialect()) { Dialect.POSTGRESQL -> rs.getInt("it").toLong() Dialect.SQLITE -> rs.getLong("it") @@ -78,14 +79,15 @@ object Results { * 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 */ @Throws(DocumentException::class) @JvmStatic - fun toExists(rs: ResultSet) = + fun toExists(rs: ResultSet, clazz: Class<*>) = when (Configuration.dialect()) { Dialect.POSTGRESQL -> rs.getBoolean("it") - Dialect.SQLITE -> toCount(rs) > 0L + Dialect.SQLITE -> toCount(rs, Long::class.java) > 0L } } diff --git a/src/java/src/test/kotlin/integration/common/Custom.kt b/src/java/src/test/kotlin/integration/common/Custom.kt index 05073d3..a66e4e8 100644 --- a/src/java/src/test/kotlin/integration/common/Custom.kt +++ b/src/java/src/test/kotlin/integration/common/Custom.kt @@ -1,9 +1,12 @@ package solutions.bitbadger.documents.java.integration.common import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.common.query.Count +import solutions.bitbadger.documents.common.query.Delete +import solutions.bitbadger.documents.common.query.Find +import solutions.bitbadger.documents.java.* import solutions.bitbadger.documents.java.integration.JsonDocument import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.Results import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -17,26 +20,26 @@ object Custom { fun listEmpty(db: ThrowawayDatabase) { JsonDocument.load(db) db.conn.deleteByFields(TEST_TABLE, listOf(Field.exists(Configuration.idField))) - val result = db.conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) + val result = db.conn.customList(Find.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(Find.all(TEST_TABLE), mapFunc = Results::fromData) + val result = db.conn.customList(Find.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData) assertEquals(5, result.size, "There should have been 5 results") } fun singleNone(db: ThrowawayDatabase) = assertNull( - db.conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), + db.conn.customSingle(Find.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData), "There should not have been a document returned" ) fun singleOne(db: ThrowawayDatabase) { JsonDocument.load(db) assertNotNull( - db.conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), + db.conn.customSingle(Find.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData), "There should not have been a document returned" ) } @@ -44,12 +47,12 @@ object Custom { fun nonQueryChanges(db: ThrowawayDatabase) { JsonDocument.load(db) assertEquals( - 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.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(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 0L, db.conn.customScalar(Count.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), "There should have been no documents in the table" ) } @@ -57,7 +60,7 @@ object Custom { fun nonQueryNoChanges(db: ThrowawayDatabase) { JsonDocument.load(db) assertEquals( - 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), "There should have been 5 documents in the table" ) db.conn.customNonQuery( @@ -65,7 +68,7 @@ object Custom { listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) ) assertEquals( - 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), "There should still have been 5 documents in the table" ) } @@ -74,7 +77,12 @@ object Custom { JsonDocument.load(db) assertEquals( 3L, - db.conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount), + 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/java/src/test/kotlin/integration/common/Document.kt b/src/java/src/test/kotlin/integration/common/Document.kt index e698525..2a1eb98 100644 --- a/src/java/src/test/kotlin/integration/common/Document.kt +++ b/src/java/src/test/kotlin/integration/common/Document.kt @@ -1,8 +1,6 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.integration.* +import solutions.bitbadger.documents.common.* import solutions.bitbadger.documents.java.* import solutions.bitbadger.documents.java.integration.* import kotlin.test.* @@ -16,7 +14,7 @@ object Document { 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) + 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") } @@ -42,7 +40,7 @@ object Document { 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"))) + 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() }, @@ -61,7 +59,7 @@ object Document { db.conn.insert(TEST_TABLE, JsonDocument("")) - val after = db.conn.findAll(TEST_TABLE) + 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 { @@ -79,7 +77,7 @@ object Document { Configuration.idStringLength = 21 db.conn.insert(TEST_TABLE, JsonDocument("")) - val after = db.conn.findAll(TEST_TABLE) + 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") @@ -92,7 +90,7 @@ object Document { fun saveMatch(db: ThrowawayDatabase) { JsonDocument.load(db) db.conn.save(TEST_TABLE, JsonDocument("two", numValue = 44)) - val doc = db.conn.findById(TEST_TABLE, "two") + val doc = db.conn.findById(TEST_TABLE, "two", JsonDocument::class.java) 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") @@ -104,7 +102,7 @@ object Document { JsonDocument.load(db) db.conn.save(TEST_TABLE, JsonDocument("test", sub = SubDocument("a", "b"))) assertNotNull( - db.conn.findById(TEST_TABLE, "test"), + db.conn.findById(TEST_TABLE, "test", JsonDocument::class.java), "The test document should have been saved" ) } @@ -112,7 +110,7 @@ object Document { 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") + val doc = db.conn.findById(TEST_TABLE, "one", JsonDocument::class.java) 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") diff --git a/src/java/src/test/kotlin/integration/common/Find.kt b/src/java/src/test/kotlin/integration/common/Find.kt index d7f124e..3be673c 100644 --- a/src/java/src/test/kotlin/integration/common/Find.kt +++ b/src/java/src/test/kotlin/integration/common/Find.kt @@ -1,9 +1,6 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.integration.* +import solutions.bitbadger.documents.common.* import solutions.bitbadger.documents.java.* import solutions.bitbadger.documents.java.integration.* import kotlin.test.assertEquals @@ -18,12 +15,16 @@ object Find { fun allDefault(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals(5, db.conn.findAll(TEST_TABLE).size, "There should have been 5 documents returned") + 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, listOf(Field.named("id"))) + 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", @@ -34,7 +35,7 @@ object Find { fun allDescending(db: ThrowawayDatabase) { JsonDocument.load(db) - val docs = db.conn.findAll(TEST_TABLE, listOf(Field.named("id DESC"))) + 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", @@ -45,8 +46,9 @@ object Find { fun allNumOrder(db: ThrowawayDatabase) { JsonDocument.load(db) - val docs = db.conn.findAll( + 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") @@ -58,11 +60,15 @@ object Find { } fun allEmpty(db: ThrowawayDatabase) = - assertEquals(0, db.conn.findAll(TEST_TABLE).size, "There should have been no documents returned") + 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") + val doc = db.conn.findById(TEST_TABLE, "two", JsonDocument::class.java) assertNotNull(doc, "The document should have been returned") assertEquals("two", doc.id, "An incorrect document was returned") } @@ -71,7 +77,7 @@ object Find { Configuration.idField = "key" try { db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) - val doc = db.conn.findById(TEST_TABLE, 18) + val doc = db.conn.findById(TEST_TABLE, 18, NumIdDocument::class.java) assertNotNull(doc, "The document should have been returned") } finally { Configuration.idField = "id" @@ -81,16 +87,17 @@ object Find { fun byIdNotFound(db: ThrowawayDatabase) { JsonDocument.load(db) assertNull( - db.conn.findById(TEST_TABLE, "x"), + db.conn.findById(TEST_TABLE, "x", JsonDocument::class.java), "There should have been no document returned" ) } fun byFieldsMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - val docs = db.conn.findByFields( + 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") @@ -99,9 +106,10 @@ object Find { fun byFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val docs = db.conn.findByFields( + 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") @@ -110,7 +118,11 @@ object Find { fun byFieldsMatchNumIn(db: ThrowawayDatabase) { JsonDocument.load(db) - val docs = db.conn.findByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + 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") } @@ -119,7 +131,7 @@ object Find { JsonDocument.load(db) assertEquals( 0, - db.conn.findByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))).size, + db.conn.findByFields(TEST_TABLE, listOf(Field.greater("numValue", 100)), JsonDocument::class.java).size, "There should have been no documents returned" ) } @@ -127,7 +139,11 @@ object Find { 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")))) + 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}") @@ -137,9 +153,10 @@ object Find { ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } assertEquals( 0, - db.conn.findByFields( + db.conn.findByFields( TEST_TABLE, - listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) + listOf(Field.inArray("values", TEST_TABLE, listOf("j"))), + ArrayDocument::class.java ).size, "There should have been no documents returned" ) @@ -147,7 +164,7 @@ object Find { fun byContainsMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - val docs = db.conn.findByContains>(TEST_TABLE, mapOf("value" to "purple")) + 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}") @@ -155,9 +172,10 @@ object Find { fun byContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val docs = db.conn.findByContains>>( + 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") @@ -168,14 +186,14 @@ object Find { JsonDocument.load(db) assertEquals( 0, - db.conn.findByContains>(TEST_TABLE, mapOf("value" to "indigo")).size, + 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)") + 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}") @@ -183,7 +201,12 @@ object Find { fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val docs = db.conn.findByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + 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") } @@ -192,30 +215,34 @@ object Find { JsonDocument.load(db) assertEquals( 0, - db.conn.findByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)").size, + 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"))) + val doc = + db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another")), JsonDocument::class.java) 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"))) + val doc = + db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green")), JsonDocument::class.java) 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"))) + val doc = db.conn.findFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), JsonDocument::class.java, 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") } @@ -223,30 +250,31 @@ object Find { fun firstByFieldsNoMatch(db: ThrowawayDatabase) { JsonDocument.load(db) assertNull( - db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), + db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent")), JsonDocument::class.java), "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!")) + val doc = db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!"), JsonDocument::class.java) 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")) + val doc = db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "purple"), JsonDocument::class.java) 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>( + val doc = db.conn.findFirstByContains( TEST_TABLE, mapOf("value" to "purple"), + JsonDocument::class.java, listOf(Field.named("sub.bar NULLS FIRST")) ) assertNotNull(doc, "There should have been a document returned") @@ -256,32 +284,31 @@ object Find { fun firstByContainsNoMatch(db: ThrowawayDatabase) { JsonDocument.load(db) assertNull( - db.conn.findFirstByContains>( - TEST_TABLE, - mapOf("value" to "indigo") - ), "There should have been no document returned" + db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "indigo"), JsonDocument::class.java), + "There should have been no document returned" ) } fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - val doc = db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + val doc = db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)", JsonDocument::class.java) 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)") + val doc = db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", JsonDocument::class.java) 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( + val doc = db.conn.findFirstByJsonPath( TEST_TABLE, "$.numValue ? (@ > 10)", + JsonDocument::class.java, listOf(Field.named("id DESC")) ) assertNotNull(doc, "There should have been a document returned") @@ -290,6 +317,9 @@ object Find { fun firstByJsonPathNoMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - assertNull(db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), "There should have been no document returned") + assertNull( + db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument::class.java), + "There should have been no document returned" + ) } } diff --git a/src/java/src/test/kotlin/integration/common/Patch.kt b/src/java/src/test/kotlin/integration/common/Patch.kt index 9a4144c..05982ed 100644 --- a/src/java/src/test/kotlin/integration/common/Patch.kt +++ b/src/java/src/test/kotlin/integration/common/Patch.kt @@ -1,6 +1,5 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.java.* import solutions.bitbadger.documents.java.integration.JsonDocument @@ -19,7 +18,7 @@ object Patch { 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") + val doc = db.conn.findById(TEST_TABLE, "one", JsonDocument::class.java) 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") @@ -56,7 +55,7 @@ object Patch { 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) + val doc = db.conn.findFirstByContains(TEST_TABLE, contains, JsonDocument::class.java) 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") @@ -73,7 +72,7 @@ object Patch { 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) + 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})") diff --git a/src/java/src/test/kotlin/integration/common/RemoveFields.kt b/src/java/src/test/kotlin/integration/common/RemoveFields.kt index 022c484..d6f2387 100644 --- a/src/java/src/test/kotlin/integration/common/RemoveFields.kt +++ b/src/java/src/test/kotlin/integration/common/RemoveFields.kt @@ -1,6 +1,5 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.Field import solutions.bitbadger.documents.java.* import solutions.bitbadger.documents.java.integration.JsonDocument @@ -17,7 +16,7 @@ object RemoveFields { fun byIdMatchFields(db: ThrowawayDatabase) { JsonDocument.load(db) db.conn.removeFieldsById(TEST_TABLE, "two", listOf("sub", "value")) - val doc = db.conn.findById(TEST_TABLE, "two") + val doc = db.conn.findById(TEST_TABLE, "two", JsonDocument::class.java) 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") @@ -39,7 +38,7 @@ object RemoveFields { 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) + val doc = db.conn.findFirstByFields(TEST_TABLE, fields, JsonDocument::class.java) 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") @@ -62,7 +61,7 @@ object RemoveFields { 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) + 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})") @@ -88,7 +87,7 @@ object RemoveFields { JsonDocument.load(db) val path = "$.value ? (@ == \"purple\")" db.conn.removeFieldsByJsonPath(TEST_TABLE, path, listOf("sub")) - val docs = db.conn.findByJsonPath(TEST_TABLE, path) + 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})") diff --git a/src/java/src/test/kotlin/integration/postgresql/CountIT.kt b/src/java/src/test/kotlin/integration/postgresql/CountIT.kt index bb6e416..04afcc4 100644 --- a/src/java/src/test/kotlin/integration/postgresql/CountIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/CountIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("Kotlin | PostgreSQL: Count") +@DisplayName("Java | Kotlin | PostgreSQL: Count") class CountIT { @Test diff --git a/src/java/src/test/kotlin/integration/postgresql/CustomIT.kt b/src/java/src/test/kotlin/integration/postgresql/CustomIT.kt index 803bfb1..673d78d 100644 --- a/src/java/src/test/kotlin/integration/postgresql/CustomIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/CustomIT.kt @@ -8,7 +8,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions */ -@DisplayName("PostgreSQL - Custom") +@DisplayName("Java | Kotlin | PostgreSQL: Custom") class CustomIT { @Test diff --git a/src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt b/src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt index 9c94abc..ddf47a6 100644 --- a/src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions */ -@DisplayName("PostgreSQL - Definition") +@DisplayName("Java | Kotlin | PostgreSQL: Definition") class DefinitionIT { @Test diff --git a/src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt b/src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt index 803400c..f5332c7 100644 --- a/src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions */ -@DisplayName("PostgreSQL - Delete") +@DisplayName("Java | Kotlin | PostgreSQL: Delete") class DeleteIT { @Test diff --git a/src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt b/src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt index 382d64b..bb070d5 100644 --- a/src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions */ -@DisplayName("PostgreSQL - Document") +@DisplayName("Java | Kotlin | PostgreSQL: Document") class DocumentIT { @Test diff --git a/src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt b/src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt index 757ec75..4c358e3 100644 --- a/src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions */ -@DisplayName("PostgreSQL - Exists") +@DisplayName("Java | Kotlin | PostgreSQL: Exists") class ExistsIT { @Test diff --git a/src/java/src/test/kotlin/integration/postgresql/FindIT.kt b/src/java/src/test/kotlin/integration/postgresql/FindIT.kt index c6a95fe..9907a6c 100644 --- a/src/java/src/test/kotlin/integration/postgresql/FindIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/FindIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions */ -@DisplayName("PostgreSQL - Find") +@DisplayName("Java | Kotlin | PostgreSQL: Find") class FindIT { @Test diff --git a/src/java/src/test/kotlin/integration/postgresql/PatchIT.kt b/src/java/src/test/kotlin/integration/postgresql/PatchIT.kt index d650355..2bf5e63 100644 --- a/src/java/src/test/kotlin/integration/postgresql/PatchIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/PatchIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions */ -@DisplayName("PostgreSQL - Patch") +@DisplayName("Java | Kotlin | PostgreSQL: Patch") class PatchIT { @Test diff --git a/src/java/src/test/kotlin/integration/postgresql/PgDB.kt b/src/java/src/test/kotlin/integration/postgresql/PgDB.kt index 7fecb54..902acc1 100644 --- a/src/java/src/test/kotlin/integration/postgresql/PgDB.kt +++ b/src/java/src/test/kotlin/integration/postgresql/PgDB.kt @@ -1,8 +1,7 @@ package solutions.bitbadger.documents.java.integration.postgresql -import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.java.* import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase @@ -39,7 +38,7 @@ class PgDB : ThrowawayDatabase { 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) + listOf(Parameter(":name", ParameterType.STRING, name)), Boolean::class.java, Results::toExists) companion object { diff --git a/src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt b/src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt index 3a10709..4fa0fac 100644 --- a/src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt +++ b/src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions */ -@DisplayName("PostgreSQL - RemoveFields") +@DisplayName("Java | Kotlin | PostgreSQL: RemoveFields") class RemoveFieldsIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/CountIT.kt b/src/java/src/test/kotlin/integration/sqlite/CountIT.kt index 62fdb63..4b3581e 100644 --- a/src/java/src/test/kotlin/integration/sqlite/CountIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/CountIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("SQLite - Count") +@DisplayName("Java | Kotlin | SQLite: Count") class CountIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/CustomIT.kt b/src/java/src/test/kotlin/integration/sqlite/CustomIT.kt index 21adab3..047371f 100644 --- a/src/java/src/test/kotlin/integration/sqlite/CustomIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/CustomIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Custom` object / `custom*` connection extension functions */ -@DisplayName("SQLite - Custom") +@DisplayName("Java | Kotlin | SQLite: Custom") class CustomIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt b/src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt index 3e553ed..ca943f4 100644 --- a/src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions */ -@DisplayName("SQLite - Definition") +@DisplayName("Java | Kotlin | SQLite: Definition") class DefinitionIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt b/src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt index b3d056a..112711f 100644 --- a/src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions */ -@DisplayName("SQLite - Delete") +@DisplayName("Java | Kotlin | SQLite: Delete") class DeleteIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt b/src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt index 1745237..c5317e7 100644 --- a/src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions */ -@DisplayName("SQLite - Document") +@DisplayName("Java | Kotlin | SQLite: Document") class DocumentIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt b/src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt index 55933ac..c1502f8 100644 --- a/src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions */ -@DisplayName("SQLite - Exists") +@DisplayName("Java | Kotlin | SQLite: Exists") class ExistsIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/FindIT.kt b/src/java/src/test/kotlin/integration/sqlite/FindIT.kt index aef0003..dc893b6 100644 --- a/src/java/src/test/kotlin/integration/sqlite/FindIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/FindIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Find` object / `find*` connection extension functions */ -@DisplayName("SQLite - Find") +@DisplayName("Java | Kotlin | SQLite: Find") class FindIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/PatchIT.kt b/src/java/src/test/kotlin/integration/sqlite/PatchIT.kt index ea1b147..1a51dae 100644 --- a/src/java/src/test/kotlin/integration/sqlite/PatchIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/PatchIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions */ -@DisplayName("SQLite - Patch") +@DisplayName("Java | Kotlin | SQLite: Patch") class PatchIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt b/src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt index 3fbad3c..3cfeab8 100644 --- a/src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt +++ b/src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions */ -@DisplayName("SQLite - RemoveFields") +@DisplayName("Java | Kotlin | SQLite: RemoveFields") class RemoveFieldsIT { @Test diff --git a/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt b/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt index 34d977b..0b98253 100644 --- a/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt +++ b/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt @@ -1,8 +1,7 @@ package solutions.bitbadger.documents.java.integration.sqlite -import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.java.* import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import java.io.File @@ -32,5 +31,5 @@ class SQLiteDB : ThrowawayDatabase { 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) + listOf(Parameter(":name", ParameterType.STRING, name)), Boolean::class.java, Results::toExists) } diff --git a/src/main/kotlin/Results.kt b/src/main/kotlin/Results.kt deleted file mode 100644 index e487f0a..0000000 --- a/src/main/kotlin/Results.kt +++ /dev/null @@ -1,79 +0,0 @@ -package solutions.bitbadger.documents - -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.java.Results -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 - * @param rs A `ResultSet` set to the row with the document to be constructed - * @return The constructed domain item - */ - inline fun fromDocument(field: String): (ResultSet, Class) -> TDoc = - { rs, _ -> Results.fromDocument(field, rs, TDoc::class.java) } - - /** - * 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 - */ - inline fun fromData(rs: ResultSet, clazz: Class = TDoc::class.java) = - Results.fromDocument("data", rs, TDoc::class.java) - - /** - * 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, clazz: Class = Long::class.java) = - 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, clazz: Class = Boolean::class.java) = - when (Configuration.dialect()) { - Dialect.POSTGRESQL -> rs.getBoolean("it") - Dialect.SQLITE -> toCount(rs) > 0L - } -} -- 2.47.2 From 5f0a6e44342f17aa36a294b4a04a924bdae3aa37 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 13 Mar 2025 07:45:48 -0400 Subject: [PATCH 39/88] Java project compiles; need serializer for tests --- src/java/src/test/kotlin/ParametersTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/java/src/test/kotlin/ParametersTest.kt b/src/java/src/test/kotlin/ParametersTest.kt index ac91104..6e31030 100644 --- a/src/java/src/test/kotlin/ParametersTest.kt +++ b/src/java/src/test/kotlin/ParametersTest.kt @@ -20,7 +20,7 @@ class ParametersTest { */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + Configuration.connectionString = null } @Test @@ -64,7 +64,7 @@ class ParametersTest { @Test @DisplayName("fieldNames generates a single parameter (PostgreSQL)") fun fieldNamesSinglePostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + Configuration.connectionString = ":postgresql:" 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") @@ -75,7 +75,7 @@ class ParametersTest { @Test @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") fun fieldNamesMultiplePostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + Configuration.connectionString = ":postgresql:" 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") @@ -86,7 +86,7 @@ class ParametersTest { @Test @DisplayName("fieldNames generates a single parameter (SQLite)") fun fieldNamesSingleSQLite() { - Configuration.dialectValue = Dialect.SQLITE + Configuration.connectionString = ":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") @@ -97,7 +97,7 @@ class ParametersTest { @Test @DisplayName("fieldNames generates multiple parameters (SQLite)") fun fieldNamesMultipleSQLite() { - Configuration.dialectValue = Dialect.SQLITE + Configuration.connectionString = ":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") -- 2.47.2 From ee46c0bc5401e74c79a7695fba3c0e800ea1dfdc Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 13 Mar 2025 20:47:21 -0400 Subject: [PATCH 40/88] Add Jackson serializer for tests --- src/java/pom.xml | 12 +++++++++++- .../test/kotlin/JacksonDocumentSerializer.kt | 18 ++++++++++++++++++ src/java/src/test/kotlin/integration/Types.kt | 14 ++++++++++++-- .../test/kotlin/integration/postgresql/PgDB.kt | 1 + .../test/kotlin/integration/sqlite/SQLiteDB.kt | 1 + 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/java/src/test/kotlin/JacksonDocumentSerializer.kt diff --git a/src/java/pom.xml b/src/java/pom.xml index b382510..0ca3107 100644 --- a/src/java/pom.xml +++ b/src/java/pom.xml @@ -25,6 +25,10 @@ https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents + + 2.18.2 + + solutions.bitbadger.documents @@ -34,6 +38,12 @@ ${project.basedir}/../common/target/common-4.0.0-alpha1-SNAPSHOT.jar jar + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + @@ -65,8 +75,8 @@ - ${project.basedir}/src/test/java ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java diff --git a/src/java/src/test/kotlin/JacksonDocumentSerializer.kt b/src/java/src/test/kotlin/JacksonDocumentSerializer.kt new file mode 100644 index 0000000..f491be5 --- /dev/null +++ b/src/java/src/test/kotlin/JacksonDocumentSerializer.kt @@ -0,0 +1,18 @@ +package solutions.bitbadger.documents.java + +import solutions.bitbadger.documents.common.DocumentSerializer +import com.fasterxml.jackson.databind.ObjectMapper + +/** + * A JSON serializer using Jackson's default options + */ +class JacksonDocumentSerializer : DocumentSerializer { + + val mapper = ObjectMapper() + + override fun serialize(document: TDoc) = + mapper.writeValueAsString(document) + + override fun deserialize(json: String, clazz: Class) = + mapper.readValue(json, clazz) +} diff --git a/src/java/src/test/kotlin/integration/Types.kt b/src/java/src/test/kotlin/integration/Types.kt index 730caa0..d239866 100644 --- a/src/java/src/test/kotlin/integration/Types.kt +++ b/src/java/src/test/kotlin/integration/Types.kt @@ -7,13 +7,20 @@ import solutions.bitbadger.documents.java.insert const val TEST_TABLE = "test_table" @Serializable -data class NumIdDocument(val key: Int, val text: String) +data class NumIdDocument(val key: Int, val text: String) { + constructor() : this(0, "") +} @Serializable -data class SubDocument(val foo: String, val bar: String) +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( @@ -26,6 +33,9 @@ data class ArrayDocument(val id: String, val values: List) { @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( diff --git a/src/java/src/test/kotlin/integration/postgresql/PgDB.kt b/src/java/src/test/kotlin/integration/postgresql/PgDB.kt index 902acc1..87413b1 100644 --- a/src/java/src/test/kotlin/integration/postgresql/PgDB.kt +++ b/src/java/src/test/kotlin/integration/postgresql/PgDB.kt @@ -13,6 +13,7 @@ class PgDB : ThrowawayDatabase { private var dbName = "" init { + DocumentConfig.serializer = JacksonDocumentSerializer() dbName = "throwaway_${AutoId.generateRandomString(8)}" Configuration.connectionString = connString("postgres") Configuration.dbConn().use { diff --git a/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt b/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt index 0b98253..5075ae7 100644 --- a/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt +++ b/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt @@ -14,6 +14,7 @@ class SQLiteDB : ThrowawayDatabase { private var dbName = "" init { + DocumentConfig.serializer = JacksonDocumentSerializer() dbName = "test-db-${AutoId.generateRandomString(8)}.db" Configuration.connectionString = "jdbc:sqlite:$dbName" } -- 2.47.2 From be3ae8ffab504251610c6bb0e2e7ec8ddf6cd76e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 14 Mar 2025 20:56:41 -0400 Subject: [PATCH 41/88] Run Java tests --- src/common/pom.xml | 12 ---- src/java/pom.xml | 3 +- .../java/java/integration/common/Count.java | 20 ------ .../integration/common/CountFunctions.java | 62 +++++++++++++++++++ .../java/integration/postgresql/CountIT.java | 53 +++++++++++++++- .../java/java/integration/sqlite/CountIT.java | 56 +++++++++++++++++ src/pom.xml | 2 +- 7 files changed, 170 insertions(+), 38 deletions(-) delete mode 100644 src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/Count.java create mode 100644 src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java create mode 100644 src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java diff --git a/src/common/pom.xml b/src/common/pom.xml index 85865a4..61cfe2f 100644 --- a/src/common/pom.xml +++ b/src/common/pom.xml @@ -60,18 +60,6 @@ - - - kotlinx-serialization - - - - - org.jetbrains.kotlin - kotlin-maven-serialization - ${kotlin.version} - - maven-surefire-plugin diff --git a/src/java/pom.xml b/src/java/pom.xml index 0ca3107..55e441b 100644 --- a/src/java/pom.xml +++ b/src/java/pom.xml @@ -48,7 +48,6 @@ src/main/kotlin - src/test/kotlin org.jetbrains.kotlin @@ -69,7 +68,7 @@ test-compile - test-compile + process-test-sources test-compile diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/Count.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/Count.java deleted file mode 100644 index 4ffc4ce..0000000 --- a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/Count.java +++ /dev/null @@ -1,20 +0,0 @@ -package solutions.bitbadger.documents.java.java.integration.common; - -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase; -import solutions.bitbadger.documents.java.java.testDocs.JsonDocument; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static solutions.bitbadger.documents.java.integration.TypesKt.TEST_TABLE; - -final public class Count { - - public static void all(ThrowawayDatabase db) { - JsonDocument.load(db); - assertEquals(5L, solutions.bitbadger.documents.java.Count.all(TEST_TABLE, db.getConn()), - "There should have been 5 documents in the table"); - } - - - private Count() { - } -} diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java new file mode 100644 index 0000000..4556020 --- /dev/null +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java @@ -0,0 +1,62 @@ +package solutions.bitbadger.documents.java.java.integration.common; + +import solutions.bitbadger.documents.common.Field; +import solutions.bitbadger.documents.java.Count; +import solutions.bitbadger.documents.java.integration.ThrowawayDatabase; +import solutions.bitbadger.documents.java.java.testDocs.JsonDocument; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static solutions.bitbadger.documents.java.integration.TypesKt.TEST_TABLE; + +/** + * Integration tests for the `Count` object + */ +final public class CountFunctions { + + public static void all(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(5L, Count.all(TEST_TABLE, db.getConn()), "There should have been 5 documents in the table"); + } + + public static void byFieldsNumeric(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(3L, Count.byFields(TEST_TABLE, List.of(Field.between("numValue", 10, 20)), db.getConn()), + "There should have been 3 matching documents"); + } + + public static void byFieldsAlpha(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(1L, Count.byFields(TEST_TABLE, List.of(Field.between("value", "aardvark", "apple")), db.getConn()), + "There should have been 1 matching document"); + } + + public static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(2L, Count.byContains(TEST_TABLE, Map.of("value", "purple"), db.getConn()), + "There should have been 2 matching documents"); + } + + public static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(0L, Count.byContains(TEST_TABLE, Map.of("value", "magenta"), db.getConn()), + "There should have been no matching documents"); + } + + public static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(2L, Count.byJsonPath(TEST_TABLE, "$.numValue ? (@ < 5)", db.getConn()), + "There should have been 2 matching documents"); + } + + public static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db); + assertEquals(0L, Count.byJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)", db.getConn()), + "There should have been no matching documents"); + } + + private CountFunctions() { + } +} diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java index 525aee5..54871dc 100644 --- a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java @@ -3,20 +3,67 @@ package solutions.bitbadger.documents.java.java.integration.postgresql; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import solutions.bitbadger.documents.java.integration.postgresql.PgDB; -import solutions.bitbadger.documents.java.java.integration.common.Count; +import solutions.bitbadger.documents.java.java.integration.common.CountFunctions; /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("Java | PostgreSQL: Count") +@DisplayName("Java | Java | PostgreSQL: Count") public class CountIT { @Test @DisplayName("all counts all documents") public void all() { try (PgDB db = new PgDB()) { - Count.all(db); + CountFunctions.all(db); } } + @Test + @DisplayName("byFields counts documents by a numeric value") + public void byFieldsNumeric() { + try (PgDB db = new PgDB()) { + CountFunctions.byFieldsNumeric(db); + } + } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + public void byFieldsAlpha() { + try (PgDB db = new PgDB()) { + CountFunctions.byFieldsAlpha(db); + } + } + + @Test + @DisplayName("byContains counts documents when matches are found") + public void byContainsMatch() { + try (PgDB db = new PgDB()) { + CountFunctions.byContainsMatch(db); + } + } + + @Test + @DisplayName("byContains counts documents when no matches are found") + public void byContainsNoMatch() { + try (PgDB db = new PgDB()) { + CountFunctions.byContainsNoMatch(db); + } + } + + @Test + @DisplayName("byJsonPath counts documents when matches are found") + public void byJsonPathMatch() { + try (PgDB db = new PgDB()) { + CountFunctions.byJsonPathMatch(db); + } + } + + @Test + @DisplayName("byJsonPath counts documents when no matches are found") + public void byJsonPathNoMatch() { + try (PgDB db = new PgDB()) { + CountFunctions.byJsonPathNoMatch(db); + } + } } diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java new file mode 100644 index 0000000..ec59271 --- /dev/null +++ b/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.java.java.integration.sqlite; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.common.DocumentException; +import solutions.bitbadger.documents.java.integration.sqlite.SQLiteDB; +import solutions.bitbadger.documents.java.java.integration.common.CountFunctions; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * SQLite integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("Java | Java | SQLite: Count") +public class CountIT { + + @Test + @DisplayName("all counts all documents") + public void all() { + try (SQLiteDB db = new SQLiteDB()) { + CountFunctions.all(db); + } + } + + @Test + @DisplayName("byFields counts documents by a numeric value") + public void byFieldsNumeric() { + try (SQLiteDB db = new SQLiteDB()) { + CountFunctions.byFieldsNumeric(db); + } + } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + public void byFieldsAlpha() { + 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/pom.xml b/src/pom.xml index 544ca00..fa7686e 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -40,7 +40,7 @@ UTF-8 official ${java.version} - 2.1.0 + 2.1.10 1.8.0 -- 2.47.2 From c2e281334ded62e5f6412b1bb4313384b624ab86 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 14 Mar 2025 20:59:28 -0400 Subject: [PATCH 42/88] Rename java proejct to jvm --- documents.iml | 6 +++--- src/{java => jvm}/pom.xml | 0 src/{java => jvm}/src/main/kotlin/ConnectionExtensions.kt | 0 src/{java => jvm}/src/main/kotlin/Count.kt | 0 src/{java => jvm}/src/main/kotlin/Custom.kt | 0 src/{java => jvm}/src/main/kotlin/Definition.kt | 0 src/{java => jvm}/src/main/kotlin/Delete.kt | 0 src/{java => jvm}/src/main/kotlin/Document.kt | 0 src/{java => jvm}/src/main/kotlin/DocumentConfig.kt | 0 src/{java => jvm}/src/main/kotlin/Exists.kt | 0 src/{java => jvm}/src/main/kotlin/Find.kt | 0 src/{java => jvm}/src/main/kotlin/NullDocumentSerializer.kt | 0 src/{java => jvm}/src/main/kotlin/Parameters.kt | 0 src/{java => jvm}/src/main/kotlin/Patch.kt | 0 src/{java => jvm}/src/main/kotlin/RemoveFields.kt | 0 src/{java => jvm}/src/main/kotlin/Results.kt | 0 .../bitbadger/documents/java/java/ParametersTest.java | 0 .../java/java/integration/common/CountFunctions.java | 0 .../documents/java/java/integration/postgresql/CountIT.java | 0 .../documents/java/java/integration/sqlite/CountIT.java | 0 .../bitbadger/documents/java/java/testDocs/ByteIdClass.java | 0 .../bitbadger/documents/java/java/testDocs/IntIdClass.java | 0 .../documents/java/java/testDocs/JsonDocument.java | 0 .../bitbadger/documents/java/java/testDocs/LongIdClass.java | 0 .../documents/java/java/testDocs/ShortIdClass.java | 0 .../documents/java/java/testDocs/StringIdClass.java | 0 .../bitbadger/documents/java/java/testDocs/SubDocument.java | 0 .../src/test/kotlin/JacksonDocumentSerializer.kt | 0 src/{java => jvm}/src/test/kotlin/ParametersTest.kt | 0 .../src/test/kotlin/integration/ThrowawayDatabase.kt | 0 src/{java => jvm}/src/test/kotlin/integration/Types.kt | 0 .../src/test/kotlin/integration/common/Count.kt | 0 .../src/test/kotlin/integration/common/Custom.kt | 0 .../src/test/kotlin/integration/common/Definition.kt | 0 .../src/test/kotlin/integration/common/Delete.kt | 0 .../src/test/kotlin/integration/common/Document.kt | 0 .../src/test/kotlin/integration/common/Exists.kt | 0 .../src/test/kotlin/integration/common/Find.kt | 0 .../src/test/kotlin/integration/common/Patch.kt | 0 .../src/test/kotlin/integration/common/RemoveFields.kt | 0 .../src/test/kotlin/integration/postgresql/CountIT.kt | 0 .../src/test/kotlin/integration/postgresql/CustomIT.kt | 0 .../src/test/kotlin/integration/postgresql/DefinitionIT.kt | 0 .../src/test/kotlin/integration/postgresql/DeleteIT.kt | 0 .../src/test/kotlin/integration/postgresql/DocumentIT.kt | 0 .../src/test/kotlin/integration/postgresql/ExistsIT.kt | 0 .../src/test/kotlin/integration/postgresql/FindIT.kt | 0 .../src/test/kotlin/integration/postgresql/PatchIT.kt | 0 .../src/test/kotlin/integration/postgresql/PgDB.kt | 0 .../test/kotlin/integration/postgresql/RemoveFieldsIT.kt | 0 .../src/test/kotlin/integration/sqlite/CountIT.kt | 0 .../src/test/kotlin/integration/sqlite/CustomIT.kt | 0 .../src/test/kotlin/integration/sqlite/DefinitionIT.kt | 0 .../src/test/kotlin/integration/sqlite/DeleteIT.kt | 0 .../src/test/kotlin/integration/sqlite/DocumentIT.kt | 0 .../src/test/kotlin/integration/sqlite/ExistsIT.kt | 0 .../src/test/kotlin/integration/sqlite/FindIT.kt | 0 .../src/test/kotlin/integration/sqlite/PatchIT.kt | 0 .../src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt | 0 .../src/test/kotlin/integration/sqlite/SQLiteDB.kt | 0 60 files changed, 3 insertions(+), 3 deletions(-) rename src/{java => jvm}/pom.xml (100%) rename src/{java => jvm}/src/main/kotlin/ConnectionExtensions.kt (100%) rename src/{java => jvm}/src/main/kotlin/Count.kt (100%) rename src/{java => jvm}/src/main/kotlin/Custom.kt (100%) rename src/{java => jvm}/src/main/kotlin/Definition.kt (100%) rename src/{java => jvm}/src/main/kotlin/Delete.kt (100%) rename src/{java => jvm}/src/main/kotlin/Document.kt (100%) rename src/{java => jvm}/src/main/kotlin/DocumentConfig.kt (100%) rename src/{java => jvm}/src/main/kotlin/Exists.kt (100%) rename src/{java => jvm}/src/main/kotlin/Find.kt (100%) rename src/{java => jvm}/src/main/kotlin/NullDocumentSerializer.kt (100%) rename src/{java => jvm}/src/main/kotlin/Parameters.kt (100%) rename src/{java => jvm}/src/main/kotlin/Patch.kt (100%) rename src/{java => jvm}/src/main/kotlin/RemoveFields.kt (100%) rename src/{java => jvm}/src/main/kotlin/Results.kt (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java (100%) rename src/{java => jvm}/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java (100%) rename src/{java => jvm}/src/test/kotlin/JacksonDocumentSerializer.kt (100%) rename src/{java => jvm}/src/test/kotlin/ParametersTest.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/ThrowawayDatabase.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/Types.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/Count.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/Custom.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/Definition.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/Delete.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/Document.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/Exists.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/Find.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/Patch.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/common/RemoveFields.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/CountIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/CustomIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/DefinitionIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/DeleteIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/DocumentIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/ExistsIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/FindIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/PatchIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/PgDB.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/CountIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/CustomIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/DefinitionIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/DeleteIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/DocumentIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/ExistsIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/FindIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/PatchIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt (100%) rename src/{java => jvm}/src/test/kotlin/integration/sqlite/SQLiteDB.kt (100%) diff --git a/documents.iml b/documents.iml index f0b3e9b..115f078 100644 --- a/documents.iml +++ b/documents.iml @@ -5,9 +5,9 @@ - - - + + + diff --git a/src/java/pom.xml b/src/jvm/pom.xml similarity index 100% rename from src/java/pom.xml rename to src/jvm/pom.xml diff --git a/src/java/src/main/kotlin/ConnectionExtensions.kt b/src/jvm/src/main/kotlin/ConnectionExtensions.kt similarity index 100% rename from src/java/src/main/kotlin/ConnectionExtensions.kt rename to src/jvm/src/main/kotlin/ConnectionExtensions.kt diff --git a/src/java/src/main/kotlin/Count.kt b/src/jvm/src/main/kotlin/Count.kt similarity index 100% rename from src/java/src/main/kotlin/Count.kt rename to src/jvm/src/main/kotlin/Count.kt diff --git a/src/java/src/main/kotlin/Custom.kt b/src/jvm/src/main/kotlin/Custom.kt similarity index 100% rename from src/java/src/main/kotlin/Custom.kt rename to src/jvm/src/main/kotlin/Custom.kt diff --git a/src/java/src/main/kotlin/Definition.kt b/src/jvm/src/main/kotlin/Definition.kt similarity index 100% rename from src/java/src/main/kotlin/Definition.kt rename to src/jvm/src/main/kotlin/Definition.kt diff --git a/src/java/src/main/kotlin/Delete.kt b/src/jvm/src/main/kotlin/Delete.kt similarity index 100% rename from src/java/src/main/kotlin/Delete.kt rename to src/jvm/src/main/kotlin/Delete.kt diff --git a/src/java/src/main/kotlin/Document.kt b/src/jvm/src/main/kotlin/Document.kt similarity index 100% rename from src/java/src/main/kotlin/Document.kt rename to src/jvm/src/main/kotlin/Document.kt diff --git a/src/java/src/main/kotlin/DocumentConfig.kt b/src/jvm/src/main/kotlin/DocumentConfig.kt similarity index 100% rename from src/java/src/main/kotlin/DocumentConfig.kt rename to src/jvm/src/main/kotlin/DocumentConfig.kt diff --git a/src/java/src/main/kotlin/Exists.kt b/src/jvm/src/main/kotlin/Exists.kt similarity index 100% rename from src/java/src/main/kotlin/Exists.kt rename to src/jvm/src/main/kotlin/Exists.kt diff --git a/src/java/src/main/kotlin/Find.kt b/src/jvm/src/main/kotlin/Find.kt similarity index 100% rename from src/java/src/main/kotlin/Find.kt rename to src/jvm/src/main/kotlin/Find.kt diff --git a/src/java/src/main/kotlin/NullDocumentSerializer.kt b/src/jvm/src/main/kotlin/NullDocumentSerializer.kt similarity index 100% rename from src/java/src/main/kotlin/NullDocumentSerializer.kt rename to src/jvm/src/main/kotlin/NullDocumentSerializer.kt diff --git a/src/java/src/main/kotlin/Parameters.kt b/src/jvm/src/main/kotlin/Parameters.kt similarity index 100% rename from src/java/src/main/kotlin/Parameters.kt rename to src/jvm/src/main/kotlin/Parameters.kt diff --git a/src/java/src/main/kotlin/Patch.kt b/src/jvm/src/main/kotlin/Patch.kt similarity index 100% rename from src/java/src/main/kotlin/Patch.kt rename to src/jvm/src/main/kotlin/Patch.kt diff --git a/src/java/src/main/kotlin/RemoveFields.kt b/src/jvm/src/main/kotlin/RemoveFields.kt similarity index 100% rename from src/java/src/main/kotlin/RemoveFields.kt rename to src/jvm/src/main/kotlin/RemoveFields.kt diff --git a/src/java/src/main/kotlin/Results.kt b/src/jvm/src/main/kotlin/Results.kt similarity index 100% rename from src/java/src/main/kotlin/Results.kt rename to src/jvm/src/main/kotlin/Results.kt diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java diff --git a/src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java similarity index 100% rename from src/java/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java diff --git a/src/java/src/test/kotlin/JacksonDocumentSerializer.kt b/src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt similarity index 100% rename from src/java/src/test/kotlin/JacksonDocumentSerializer.kt rename to src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt diff --git a/src/java/src/test/kotlin/ParametersTest.kt b/src/jvm/src/test/kotlin/ParametersTest.kt similarity index 100% rename from src/java/src/test/kotlin/ParametersTest.kt rename to src/jvm/src/test/kotlin/ParametersTest.kt diff --git a/src/java/src/test/kotlin/integration/ThrowawayDatabase.kt b/src/jvm/src/test/kotlin/integration/ThrowawayDatabase.kt similarity index 100% rename from src/java/src/test/kotlin/integration/ThrowawayDatabase.kt rename to src/jvm/src/test/kotlin/integration/ThrowawayDatabase.kt diff --git a/src/java/src/test/kotlin/integration/Types.kt b/src/jvm/src/test/kotlin/integration/Types.kt similarity index 100% rename from src/java/src/test/kotlin/integration/Types.kt rename to src/jvm/src/test/kotlin/integration/Types.kt diff --git a/src/java/src/test/kotlin/integration/common/Count.kt b/src/jvm/src/test/kotlin/integration/common/Count.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/Count.kt rename to src/jvm/src/test/kotlin/integration/common/Count.kt diff --git a/src/java/src/test/kotlin/integration/common/Custom.kt b/src/jvm/src/test/kotlin/integration/common/Custom.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/Custom.kt rename to src/jvm/src/test/kotlin/integration/common/Custom.kt diff --git a/src/java/src/test/kotlin/integration/common/Definition.kt b/src/jvm/src/test/kotlin/integration/common/Definition.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/Definition.kt rename to src/jvm/src/test/kotlin/integration/common/Definition.kt diff --git a/src/java/src/test/kotlin/integration/common/Delete.kt b/src/jvm/src/test/kotlin/integration/common/Delete.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/Delete.kt rename to src/jvm/src/test/kotlin/integration/common/Delete.kt diff --git a/src/java/src/test/kotlin/integration/common/Document.kt b/src/jvm/src/test/kotlin/integration/common/Document.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/Document.kt rename to src/jvm/src/test/kotlin/integration/common/Document.kt diff --git a/src/java/src/test/kotlin/integration/common/Exists.kt b/src/jvm/src/test/kotlin/integration/common/Exists.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/Exists.kt rename to src/jvm/src/test/kotlin/integration/common/Exists.kt diff --git a/src/java/src/test/kotlin/integration/common/Find.kt b/src/jvm/src/test/kotlin/integration/common/Find.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/Find.kt rename to src/jvm/src/test/kotlin/integration/common/Find.kt diff --git a/src/java/src/test/kotlin/integration/common/Patch.kt b/src/jvm/src/test/kotlin/integration/common/Patch.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/Patch.kt rename to src/jvm/src/test/kotlin/integration/common/Patch.kt diff --git a/src/java/src/test/kotlin/integration/common/RemoveFields.kt b/src/jvm/src/test/kotlin/integration/common/RemoveFields.kt similarity index 100% rename from src/java/src/test/kotlin/integration/common/RemoveFields.kt rename to src/jvm/src/test/kotlin/integration/common/RemoveFields.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/CountIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/CountIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/CountIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/CountIT.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/CustomIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/CustomIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/CustomIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/CustomIT.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/DefinitionIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/DefinitionIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/DefinitionIT.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/DeleteIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/DeleteIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/DeleteIT.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/DocumentIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/DocumentIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/DocumentIT.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/ExistsIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/ExistsIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/ExistsIT.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/FindIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/FindIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/FindIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/FindIT.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/PatchIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/PatchIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/PatchIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/PatchIT.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/PgDB.kt b/src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/PgDB.kt rename to src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt diff --git a/src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt b/src/jvm/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt rename to src/jvm/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/CountIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/CountIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/CustomIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/CustomIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/CustomIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/CustomIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/DefinitionIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/DeleteIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/DocumentIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/DocumentIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/DocumentIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/ExistsIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/FindIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/FindIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/PatchIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/PatchIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt rename to src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt diff --git a/src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt b/src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt similarity index 100% rename from src/java/src/test/kotlin/integration/sqlite/SQLiteDB.kt rename to src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt -- 2.47.2 From 2697ddaeed435d986fcee4a5f9fd539fc060238d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 14 Mar 2025 21:29:31 -0400 Subject: [PATCH 43/88] Package rearrangement; build succeeds --- .../documents/common/java/AutoIdTest.java | 4 ++-- .../common/java/DocumentIndexTest.java | 2 +- .../documents/common/java/FieldMatchTest.java | 2 +- .../documents/common/java/FieldTest.java | 2 +- .../bitbadger/documents/common/java/OpTest.java | 2 +- .../common/java/ParameterNameTest.java | 2 +- .../documents/common/java/ParameterTest.java | 6 +++--- src/common/src/test/kotlin/AutoIdTest.kt | 2 ++ src/common/src/test/kotlin/ComparisonTest.kt | 1 + src/common/src/test/kotlin/ConfigurationTest.kt | 4 ++++ src/common/src/test/kotlin/DialectTest.kt | 2 ++ src/common/src/test/kotlin/DocumentIndexTest.kt | 1 + src/common/src/test/kotlin/FieldMatchTest.kt | 1 + src/common/src/test/kotlin/FieldTest.kt | 1 + src/common/src/test/kotlin/OpTest.kt | 1 + src/common/src/test/kotlin/ParameterNameTest.kt | 1 + src/common/src/test/kotlin/ParameterTest.kt | 3 +++ src/common/src/test/kotlin/query/CountTest.kt | 9 +++++---- .../src/test/kotlin/query/DefinitionTest.kt | 8 ++++---- src/common/src/test/kotlin/query/DeleteTest.kt | 8 ++++---- .../src/test/kotlin/query/DocumentTest.kt | 9 +++++---- src/common/src/test/kotlin/query/ExistsTest.kt | 8 ++++---- src/common/src/test/kotlin/query/FindTest.kt | 8 ++++---- src/common/src/test/kotlin/query/PatchTest.kt | 8 ++++---- src/common/src/test/kotlin/query/QueryTest.kt | 13 ++++++++----- .../src/test/kotlin/query/RemoveFieldsTest.kt | 1 + src/common/src/test/kotlin/query/WhereTest.kt | 2 ++ src/jvm/pom.xml | 8 -------- src/{common => jvm}/src/main/kotlin/AutoId.kt | 2 +- .../src/main/kotlin/Comparison.kt | 2 +- .../src/main/kotlin/Configuration.kt | 2 +- src/{common => jvm}/src/main/kotlin/Dialect.kt | 2 +- .../src/main/kotlin/DocumentException.kt | 2 +- .../src/main/kotlin/DocumentIndex.kt | 2 +- .../src/main/kotlin/DocumentSerializer.kt | 2 +- src/{common => jvm}/src/main/kotlin/Field.kt | 2 +- .../src/main/kotlin/FieldFormat.kt | 2 +- .../src/main/kotlin/FieldMatch.kt | 2 +- src/{common => jvm}/src/main/kotlin/Op.kt | 2 +- .../src/main/kotlin/Parameter.kt | 2 +- .../src/main/kotlin/ParameterName.kt | 2 +- .../src/main/kotlin/ParameterType.kt | 2 +- .../Connection.kt} | 5 +++-- src/jvm/src/main/kotlin/{ => jvm}/Count.kt | 7 ++++--- src/jvm/src/main/kotlin/{ => jvm}/Custom.kt | 6 +++--- src/jvm/src/main/kotlin/{ => jvm}/Definition.kt | 11 ++++++----- src/jvm/src/main/kotlin/{ => jvm}/Delete.kt | 7 ++++--- src/jvm/src/main/kotlin/{ => jvm}/Document.kt | 17 +++++++++-------- .../src/main/kotlin/{ => jvm}/DocumentConfig.kt | 4 ++-- src/jvm/src/main/kotlin/{ => jvm}/Exists.kt | 7 ++++--- src/jvm/src/main/kotlin/{ => jvm}/Find.kt | 10 ++++++---- .../kotlin/{ => jvm}/NullDocumentSerializer.kt | 4 ++-- src/jvm/src/main/kotlin/{ => jvm}/Parameters.kt | 5 +++-- src/jvm/src/main/kotlin/{ => jvm}/Patch.kt | 7 ++++--- .../src/main/kotlin/{ => jvm}/RemoveFields.kt | 7 ++++--- src/jvm/src/main/kotlin/{ => jvm}/Results.kt | 8 ++++---- .../src/main/kotlin/query/Count.kt | 8 ++++---- .../src/main/kotlin/query/Definition.kt | 4 ++-- .../src/main/kotlin/query/Delete.kt | 10 +++++----- .../src/main/kotlin/query/Document.kt | 8 ++++---- .../src/main/kotlin/query/Exists.kt | 6 +++--- .../src/main/kotlin/query/Find.kt | 10 +++++----- .../src/main/kotlin/query/Patch.kt | 14 +++++++------- .../src/main/kotlin/query/Query.kt | 10 +++++----- .../src/main/kotlin/query/RemoveFields.kt | 8 ++++---- .../src/main/kotlin/query/Where.kt | 12 ++++++------ .../documents/java/java/ParametersTest.java | 4 ++-- .../java/integration/common/CountFunctions.java | 4 ++-- .../java/java/integration/sqlite/CountIT.java | 2 +- .../java/java/testDocs/JsonDocument.java | 2 +- .../test/kotlin/JacksonDocumentSerializer.kt | 2 +- src/jvm/src/test/kotlin/ParametersTest.kt | 2 +- src/jvm/src/test/kotlin/integration/Types.kt | 2 +- .../src/test/kotlin/integration/common/Count.kt | 10 +++++----- .../test/kotlin/integration/common/Custom.kt | 9 ++++++--- .../kotlin/integration/common/Definition.kt | 8 ++++---- .../test/kotlin/integration/common/Delete.kt | 4 ++-- .../test/kotlin/integration/common/Document.kt | 5 ++++- .../test/kotlin/integration/common/Exists.kt | 10 +++++----- .../src/test/kotlin/integration/common/Find.kt | 5 ++++- .../src/test/kotlin/integration/common/Patch.kt | 4 ++-- .../kotlin/integration/common/RemoveFields.kt | 4 ++-- .../test/kotlin/integration/postgresql/PgDB.kt | 9 +++++++++ .../test/kotlin/integration/sqlite/CountIT.kt | 2 +- .../kotlin/integration/sqlite/DefinitionIT.kt | 2 +- .../test/kotlin/integration/sqlite/DeleteIT.kt | 2 +- .../test/kotlin/integration/sqlite/ExistsIT.kt | 2 +- .../test/kotlin/integration/sqlite/FindIT.kt | 2 +- .../test/kotlin/integration/sqlite/PatchIT.kt | 2 +- .../kotlin/integration/sqlite/RemoveFieldsIT.kt | 2 +- .../test/kotlin/integration/sqlite/SQLiteDB.kt | 8 ++++++++ .../src/main/kotlin/ConnectionExtensions.kt | 10 +++++----- src/kotlin/src/main/kotlin/Count.kt | 11 +++++------ src/kotlin/src/main/kotlin/Custom.kt | 5 ++--- src/kotlin/src/main/kotlin/Definition.kt | 2 +- src/kotlin/src/main/kotlin/Delete.kt | 8 ++++---- src/kotlin/src/main/kotlin/Exists.kt | 8 ++++---- src/kotlin/src/main/kotlin/Find.kt | 9 ++++----- src/kotlin/src/main/kotlin/Parameters.kt | 2 ++ src/kotlin/src/main/kotlin/Patch.kt | 8 ++++---- src/kotlin/src/main/kotlin/RemoveFields.kt | 1 + src/kotlin/src/main/kotlin/Results.kt | 7 +++---- src/main/kotlin/Custom.kt | 2 -- src/pom.xml | 2 +- 104 files changed, 288 insertions(+), 237 deletions(-) rename src/{common => jvm}/src/main/kotlin/AutoId.kt (98%) rename src/{common => jvm}/src/main/kotlin/Comparison.kt (97%) rename src/{common => jvm}/src/main/kotlin/Configuration.kt (97%) rename src/{common => jvm}/src/main/kotlin/Dialect.kt (94%) rename src/{common => jvm}/src/main/kotlin/DocumentException.kt (85%) rename src/{common => jvm}/src/main/kotlin/DocumentIndex.kt (87%) rename src/{common => jvm}/src/main/kotlin/DocumentSerializer.kt (93%) rename src/{common => jvm}/src/main/kotlin/Field.kt (99%) rename src/{common => jvm}/src/main/kotlin/FieldFormat.kt (84%) rename src/{common => jvm}/src/main/kotlin/FieldMatch.kt (83%) rename src/{common => jvm}/src/main/kotlin/Op.kt (94%) rename src/{common => jvm}/src/main/kotlin/Parameter.kt (97%) rename src/{common => jvm}/src/main/kotlin/ParameterName.kt (91%) rename src/{common => jvm}/src/main/kotlin/ParameterType.kt (87%) rename src/jvm/src/main/kotlin/{ConnectionExtensions.kt => extensions/Connection.kt} (99%) rename src/jvm/src/main/kotlin/{ => jvm}/Count.kt (96%) rename src/jvm/src/main/kotlin/{ => jvm}/Custom.kt (97%) rename src/jvm/src/main/kotlin/{ => jvm}/Definition.kt (90%) rename src/jvm/src/main/kotlin/{ => jvm}/Delete.kt (95%) rename src/jvm/src/main/kotlin/{ => jvm}/Document.kt (91%) rename src/jvm/src/main/kotlin/{ => jvm}/DocumentConfig.kt (67%) rename src/jvm/src/main/kotlin/{ => jvm}/Exists.kt (96%) rename src/jvm/src/main/kotlin/{ => jvm}/Find.kt (98%) rename src/jvm/src/main/kotlin/{ => jvm}/NullDocumentSerializer.kt (85%) rename src/jvm/src/main/kotlin/{ => jvm}/Parameters.kt (97%) rename src/jvm/src/main/kotlin/{ => jvm}/Patch.kt (96%) rename src/jvm/src/main/kotlin/{ => jvm}/RemoveFields.kt (97%) rename src/jvm/src/main/kotlin/{ => jvm}/Results.kt (93%) rename src/{common => jvm}/src/main/kotlin/query/Count.kt (87%) rename src/{common => jvm}/src/main/kotlin/query/Definition.kt (97%) rename src/{common => jvm}/src/main/kotlin/query/Delete.kt (87%) rename src/{common => jvm}/src/main/kotlin/query/Document.kt (91%) rename src/{common => jvm}/src/main/kotlin/query/Exists.kt (93%) rename src/{common => jvm}/src/main/kotlin/query/Find.kt (87%) rename src/{common => jvm}/src/main/kotlin/query/Patch.kt (84%) rename src/{common => jvm}/src/main/kotlin/query/Query.kt (90%) rename src/{common => jvm}/src/main/kotlin/query/RemoveFields.kt (92%) rename src/{common => jvm}/src/main/kotlin/query/Where.kt (87%) diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java index 9321974..9ff6314 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java +++ b/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java @@ -2,8 +2,8 @@ package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.AutoId; -import solutions.bitbadger.documents.common.DocumentException; +import AutoId; +import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.java.java.testDocs.*; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java index 95716e7..9440d16 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java +++ b/src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.DocumentIndex; +import solutions.bitbadger.documents.DocumentIndex; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java index c8f02da..3335d6e 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java +++ b/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.FieldMatch; +import solutions.bitbadger.documents.FieldMatch; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java index 672ff2a..22192ec 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java +++ b/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java @@ -4,7 +4,7 @@ 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.common.*; +import solutions.bitbadger.documents.*; import java.util.Collection; import java.util.List; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java index 2d70565..d26ffbe 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java +++ b/src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.Op; +import solutions.bitbadger.documents.Op; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java index f8bdff9..910289d 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java +++ b/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.ParameterName; +import solutions.bitbadger.documents.ParameterName; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java b/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java index aba9a4c..2bce5ed 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java +++ b/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java @@ -2,9 +2,9 @@ package solutions.bitbadger.documents.common.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.DocumentException; -import solutions.bitbadger.documents.common.Parameter; -import solutions.bitbadger.documents.common.ParameterType; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Parameter; +import solutions.bitbadger.documents.ParameterType; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/common/src/test/kotlin/AutoIdTest.kt b/src/common/src/test/kotlin/AutoIdTest.kt index 540342e..05f4e59 100644 --- a/src/common/src/test/kotlin/AutoIdTest.kt +++ b/src/common/src/test/kotlin/AutoIdTest.kt @@ -1,8 +1,10 @@ package solutions.bitbadger.documents.common +import AutoId import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.DocumentException import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals diff --git a/src/common/src/test/kotlin/ComparisonTest.kt b/src/common/src/test/kotlin/ComparisonTest.kt index b40a89e..e6c5bf5 100644 --- a/src/common/src/test/kotlin/ComparisonTest.kt +++ b/src/common/src/test/kotlin/ComparisonTest.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.common 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 diff --git a/src/common/src/test/kotlin/ConfigurationTest.kt b/src/common/src/test/kotlin/ConfigurationTest.kt index cef0cb3..338b7c9 100644 --- a/src/common/src/test/kotlin/ConfigurationTest.kt +++ b/src/common/src/test/kotlin/ConfigurationTest.kt @@ -1,8 +1,12 @@ package solutions.bitbadger.documents.common +import AutoId import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/DialectTest.kt b/src/common/src/test/kotlin/DialectTest.kt index b2fe4b2..3eae724 100644 --- a/src/common/src/test/kotlin/DialectTest.kt +++ b/src/common/src/test/kotlin/DialectTest.kt @@ -2,6 +2,8 @@ package solutions.bitbadger.documents.common 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 diff --git a/src/common/src/test/kotlin/DocumentIndexTest.kt b/src/common/src/test/kotlin/DocumentIndexTest.kt index 9840671..fb03e7b 100644 --- a/src/common/src/test/kotlin/DocumentIndexTest.kt +++ b/src/common/src/test/kotlin/DocumentIndexTest.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentIndex import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/FieldMatchTest.kt b/src/common/src/test/kotlin/FieldMatchTest.kt index 2c3fdad..9c4de5a 100644 --- a/src/common/src/test/kotlin/FieldMatchTest.kt +++ b/src/common/src/test/kotlin/FieldMatchTest.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.FieldMatch import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/FieldTest.kt b/src/common/src/test/kotlin/FieldTest.kt index c32a2a9..e3a6d4a 100644 --- a/src/common/src/test/kotlin/FieldTest.kt +++ b/src/common/src/test/kotlin/FieldTest.kt @@ -4,6 +4,7 @@ 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 diff --git a/src/common/src/test/kotlin/OpTest.kt b/src/common/src/test/kotlin/OpTest.kt index 6a272e9..9543275 100644 --- a/src/common/src/test/kotlin/OpTest.kt +++ b/src/common/src/test/kotlin/OpTest.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Op import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/ParameterNameTest.kt b/src/common/src/test/kotlin/ParameterNameTest.kt index 93dd9c8..3c09ce0 100644 --- a/src/common/src/test/kotlin/ParameterNameTest.kt +++ b/src/common/src/test/kotlin/ParameterNameTest.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.common import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.ParameterName import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/ParameterTest.kt b/src/common/src/test/kotlin/ParameterTest.kt index ea8753a..3a312e4 100644 --- a/src/common/src/test/kotlin/ParameterTest.kt +++ b/src/common/src/test/kotlin/ParameterTest.kt @@ -2,6 +2,9 @@ package solutions.bitbadger.documents.common 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 diff --git a/src/common/src/test/kotlin/query/CountTest.kt b/src/common/src/test/kotlin/query/CountTest.kt index a664f21..48f4ddb 100644 --- a/src/common/src/test/kotlin/query/CountTest.kt +++ b/src/common/src/test/kotlin/query/CountTest.kt @@ -4,10 +4,11 @@ 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.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.java.query.Count import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/query/DefinitionTest.kt b/src/common/src/test/kotlin/query/DefinitionTest.kt index 90343c9..7ab6436 100644 --- a/src/common/src/test/kotlin/query/DefinitionTest.kt +++ b/src/common/src/test/kotlin/query/DefinitionTest.kt @@ -4,10 +4,10 @@ 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.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.DocumentIndex +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentIndex import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/query/DeleteTest.kt b/src/common/src/test/kotlin/query/DeleteTest.kt index 54206d8..e9f10ee 100644 --- a/src/common/src/test/kotlin/query/DeleteTest.kt +++ b/src/common/src/test/kotlin/query/DeleteTest.kt @@ -4,10 +4,10 @@ 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.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/query/DocumentTest.kt b/src/common/src/test/kotlin/query/DocumentTest.kt index 1ee9277..cf59b0c 100644 --- a/src/common/src/test/kotlin/query/DocumentTest.kt +++ b/src/common/src/test/kotlin/query/DocumentTest.kt @@ -4,10 +4,11 @@ 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.common.AutoId -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException +import AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.java.query.Document import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/src/common/src/test/kotlin/query/ExistsTest.kt b/src/common/src/test/kotlin/query/ExistsTest.kt index a1ea2d9..7b852c0 100644 --- a/src/common/src/test/kotlin/query/ExistsTest.kt +++ b/src/common/src/test/kotlin/query/ExistsTest.kt @@ -4,10 +4,10 @@ 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.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/query/FindTest.kt b/src/common/src/test/kotlin/query/FindTest.kt index 4b89e82..d0b6583 100644 --- a/src/common/src/test/kotlin/query/FindTest.kt +++ b/src/common/src/test/kotlin/query/FindTest.kt @@ -4,10 +4,10 @@ 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.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/query/PatchTest.kt b/src/common/src/test/kotlin/query/PatchTest.kt index e7511b0..a7f5675 100644 --- a/src/common/src/test/kotlin/query/PatchTest.kt +++ b/src/common/src/test/kotlin/query/PatchTest.kt @@ -4,10 +4,10 @@ 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.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/query/QueryTest.kt b/src/common/src/test/kotlin/query/QueryTest.kt index 82e5285..912f9c4 100644 --- a/src/common/src/test/kotlin/query/QueryTest.kt +++ b/src/common/src/test/kotlin/query/QueryTest.kt @@ -3,10 +3,13 @@ package solutions.bitbadger.documents.common.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.java.query.byFields +import solutions.bitbadger.documents.java.query.byId +import solutions.bitbadger.documents.java.query.orderBy import kotlin.test.assertEquals /** @@ -26,7 +29,7 @@ class QueryTest { @Test @DisplayName("statementWhere generates correctly") fun statementWhere() = - assertEquals("x WHERE y", statementWhere("x", "y"), "Statements not combined correctly") + assertEquals("x WHERE y", solutions.bitbadger.documents.java.query.statementWhere("x", "y"), "Statements not combined correctly") @Test @DisplayName("byId generates a numeric ID query (PostgreSQL)") diff --git a/src/common/src/test/kotlin/query/RemoveFieldsTest.kt b/src/common/src/test/kotlin/query/RemoveFieldsTest.kt index 1710e0b..78199f8 100644 --- a/src/common/src/test/kotlin/query/RemoveFieldsTest.kt +++ b/src/common/src/test/kotlin/query/RemoveFieldsTest.kt @@ -4,6 +4,7 @@ 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.common.* import kotlin.test.assertEquals diff --git a/src/common/src/test/kotlin/query/WhereTest.kt b/src/common/src/test/kotlin/query/WhereTest.kt index aeed546..a5ae71b 100644 --- a/src/common/src/test/kotlin/query/WhereTest.kt +++ b/src/common/src/test/kotlin/query/WhereTest.kt @@ -4,7 +4,9 @@ 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.common.* +import solutions.bitbadger.documents.java.query.Where import kotlin.test.assertEquals /** diff --git a/src/jvm/pom.xml b/src/jvm/pom.xml index 55e441b..2da78a7 100644 --- a/src/jvm/pom.xml +++ b/src/jvm/pom.xml @@ -30,14 +30,6 @@ - - solutions.bitbadger.documents - common - 4.0.0-alpha1-SNAPSHOT - system - ${project.basedir}/../common/target/common-4.0.0-alpha1-SNAPSHOT.jar - jar - com.fasterxml.jackson.core jackson-databind diff --git a/src/common/src/main/kotlin/AutoId.kt b/src/jvm/src/main/kotlin/AutoId.kt similarity index 98% rename from src/common/src/main/kotlin/AutoId.kt rename to src/jvm/src/main/kotlin/AutoId.kt index daa61c1..f94d7da 100644 --- a/src/common/src/main/kotlin/AutoId.kt +++ b/src/jvm/src/main/kotlin/AutoId.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import kotlin.jvm.Throws import kotlin.reflect.full.* diff --git a/src/common/src/main/kotlin/Comparison.kt b/src/jvm/src/main/kotlin/Comparison.kt similarity index 97% rename from src/common/src/main/kotlin/Comparison.kt rename to src/jvm/src/main/kotlin/Comparison.kt index 5946aeb..f187882 100644 --- a/src/common/src/main/kotlin/Comparison.kt +++ b/src/jvm/src/main/kotlin/Comparison.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * Information required to generate a JSON field comparison diff --git a/src/common/src/main/kotlin/Configuration.kt b/src/jvm/src/main/kotlin/Configuration.kt similarity index 97% rename from src/common/src/main/kotlin/Configuration.kt rename to src/jvm/src/main/kotlin/Configuration.kt index c4969a1..e89793a 100644 --- a/src/common/src/main/kotlin/Configuration.kt +++ b/src/jvm/src/main/kotlin/Configuration.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import java.sql.Connection import java.sql.DriverManager diff --git a/src/common/src/main/kotlin/Dialect.kt b/src/jvm/src/main/kotlin/Dialect.kt similarity index 94% rename from src/common/src/main/kotlin/Dialect.kt rename to src/jvm/src/main/kotlin/Dialect.kt index e950ba5..986dddb 100644 --- a/src/common/src/main/kotlin/Dialect.kt +++ b/src/jvm/src/main/kotlin/Dialect.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * The SQL dialect to use when building queries diff --git a/src/common/src/main/kotlin/DocumentException.kt b/src/jvm/src/main/kotlin/DocumentException.kt similarity index 85% rename from src/common/src/main/kotlin/DocumentException.kt rename to src/jvm/src/main/kotlin/DocumentException.kt index c8ce19f..4a292e6 100644 --- a/src/common/src/main/kotlin/DocumentException.kt +++ b/src/jvm/src/main/kotlin/DocumentException.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * An exception caused by invalid operations in the document library diff --git a/src/common/src/main/kotlin/DocumentIndex.kt b/src/jvm/src/main/kotlin/DocumentIndex.kt similarity index 87% rename from src/common/src/main/kotlin/DocumentIndex.kt rename to src/jvm/src/main/kotlin/DocumentIndex.kt index 45f1fac..3ce8743 100644 --- a/src/common/src/main/kotlin/DocumentIndex.kt +++ b/src/jvm/src/main/kotlin/DocumentIndex.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * The type of index to generate for the document diff --git a/src/common/src/main/kotlin/DocumentSerializer.kt b/src/jvm/src/main/kotlin/DocumentSerializer.kt similarity index 93% rename from src/common/src/main/kotlin/DocumentSerializer.kt rename to src/jvm/src/main/kotlin/DocumentSerializer.kt index 09cb1c9..bbdee39 100644 --- a/src/common/src/main/kotlin/DocumentSerializer.kt +++ b/src/jvm/src/main/kotlin/DocumentSerializer.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * The interface for a document serializer/deserializer diff --git a/src/common/src/main/kotlin/Field.kt b/src/jvm/src/main/kotlin/Field.kt similarity index 99% rename from src/common/src/main/kotlin/Field.kt rename to src/jvm/src/main/kotlin/Field.kt index 7b75702..d15a838 100644 --- a/src/common/src/main/kotlin/Field.kt +++ b/src/jvm/src/main/kotlin/Field.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * A field and its comparison diff --git a/src/common/src/main/kotlin/FieldFormat.kt b/src/jvm/src/main/kotlin/FieldFormat.kt similarity index 84% rename from src/common/src/main/kotlin/FieldFormat.kt rename to src/jvm/src/main/kotlin/FieldFormat.kt index 02d4c20..c804a87 100644 --- a/src/common/src/main/kotlin/FieldFormat.kt +++ b/src/jvm/src/main/kotlin/FieldFormat.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * The data format for a document field retrieval diff --git a/src/common/src/main/kotlin/FieldMatch.kt b/src/jvm/src/main/kotlin/FieldMatch.kt similarity index 83% rename from src/common/src/main/kotlin/FieldMatch.kt rename to src/jvm/src/main/kotlin/FieldMatch.kt index e621dba..3b1d3ff 100644 --- a/src/common/src/main/kotlin/FieldMatch.kt +++ b/src/jvm/src/main/kotlin/FieldMatch.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * How fields should be matched in by-field queries diff --git a/src/common/src/main/kotlin/Op.kt b/src/jvm/src/main/kotlin/Op.kt similarity index 94% rename from src/common/src/main/kotlin/Op.kt rename to src/jvm/src/main/kotlin/Op.kt index dc93dfc..61723c9 100644 --- a/src/common/src/main/kotlin/Op.kt +++ b/src/jvm/src/main/kotlin/Op.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * A comparison operator used for fields diff --git a/src/common/src/main/kotlin/Parameter.kt b/src/jvm/src/main/kotlin/Parameter.kt similarity index 97% rename from src/common/src/main/kotlin/Parameter.kt rename to src/jvm/src/main/kotlin/Parameter.kt index 9e63336..1bd707b 100644 --- a/src/common/src/main/kotlin/Parameter.kt +++ b/src/jvm/src/main/kotlin/Parameter.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import java.sql.PreparedStatement import java.sql.Types diff --git a/src/common/src/main/kotlin/ParameterName.kt b/src/jvm/src/main/kotlin/ParameterName.kt similarity index 91% rename from src/common/src/main/kotlin/ParameterName.kt rename to src/jvm/src/main/kotlin/ParameterName.kt index 566dca0..a090db0 100644 --- a/src/common/src/main/kotlin/ParameterName.kt +++ b/src/jvm/src/main/kotlin/ParameterName.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * Derive parameter names; each instance wraps a counter to provide names for anonymous fields diff --git a/src/common/src/main/kotlin/ParameterType.kt b/src/jvm/src/main/kotlin/ParameterType.kt similarity index 87% rename from src/common/src/main/kotlin/ParameterType.kt rename to src/jvm/src/main/kotlin/ParameterType.kt index 159bc14..edf44b7 100644 --- a/src/common/src/main/kotlin/ParameterType.kt +++ b/src/jvm/src/main/kotlin/ParameterType.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents /** * The types of parameters supported by the document library diff --git a/src/jvm/src/main/kotlin/ConnectionExtensions.kt b/src/jvm/src/main/kotlin/extensions/Connection.kt similarity index 99% rename from src/jvm/src/main/kotlin/ConnectionExtensions.kt rename to src/jvm/src/main/kotlin/extensions/Connection.kt index f25e658..71f7ecc 100644 --- a/src/jvm/src/main/kotlin/ConnectionExtensions.kt +++ b/src/jvm/src/main/kotlin/extensions/Connection.kt @@ -1,8 +1,9 @@ @file:JvmName("ConnExt") -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.extensions -import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.jvm.* import java.sql.Connection import java.sql.ResultSet diff --git a/src/jvm/src/main/kotlin/Count.kt b/src/jvm/src/main/kotlin/jvm/Count.kt similarity index 96% rename from src/jvm/src/main/kotlin/Count.kt rename to src/jvm/src/main/kotlin/jvm/Count.kt index df9bc16..94ced3a 100644 --- a/src/jvm/src/main/kotlin/Count.kt +++ b/src/jvm/src/main/kotlin/jvm/Count.kt @@ -1,7 +1,8 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.Count +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.Count +import solutions.bitbadger.documents.extensions.customScalar import java.sql.Connection /** diff --git a/src/jvm/src/main/kotlin/Custom.kt b/src/jvm/src/main/kotlin/jvm/Custom.kt similarity index 97% rename from src/jvm/src/main/kotlin/Custom.kt rename to src/jvm/src/main/kotlin/jvm/Custom.kt index 5503022..b4f5d54 100644 --- a/src/jvm/src/main/kotlin/Custom.kt +++ b/src/jvm/src/main/kotlin/jvm/Custom.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Parameter +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Parameter import java.sql.Connection import java.sql.ResultSet diff --git a/src/jvm/src/main/kotlin/Definition.kt b/src/jvm/src/main/kotlin/jvm/Definition.kt similarity index 90% rename from src/jvm/src/main/kotlin/Definition.kt rename to src/jvm/src/main/kotlin/jvm/Definition.kt index 531415d..7946838 100644 --- a/src/jvm/src/main/kotlin/Definition.kt +++ b/src/jvm/src/main/kotlin/jvm/Definition.kt @@ -1,9 +1,10 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.DocumentIndex -import solutions.bitbadger.documents.common.query.Definition +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.extensions.customNonQuery +import solutions.bitbadger.documents.query.Definition import java.sql.Connection /** diff --git a/src/jvm/src/main/kotlin/Delete.kt b/src/jvm/src/main/kotlin/jvm/Delete.kt similarity index 95% rename from src/jvm/src/main/kotlin/Delete.kt rename to src/jvm/src/main/kotlin/jvm/Delete.kt index 0587fb7..9715698 100644 --- a/src/jvm/src/main/kotlin/Delete.kt +++ b/src/jvm/src/main/kotlin/jvm/Delete.kt @@ -1,7 +1,8 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.Delete +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.extensions.customNonQuery +import solutions.bitbadger.documents.query.Delete import java.sql.Connection /** diff --git a/src/jvm/src/main/kotlin/Document.kt b/src/jvm/src/main/kotlin/jvm/Document.kt similarity index 91% rename from src/jvm/src/main/kotlin/Document.kt rename to src/jvm/src/main/kotlin/jvm/Document.kt index 6a29a7c..ba7faac 100644 --- a/src/jvm/src/main/kotlin/Document.kt +++ b/src/jvm/src/main/kotlin/jvm/Document.kt @@ -1,13 +1,14 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.AutoId -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.Field +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.extensions.customNonQuery +import solutions.bitbadger.documents.query.Document +import solutions.bitbadger.documents.query.Where +import solutions.bitbadger.documents.query.statementWhere import java.sql.Connection -import solutions.bitbadger.documents.common.query.Document -import solutions.bitbadger.documents.common.query.Where -import solutions.bitbadger.documents.common.query.statementWhere /** * Functions for manipulating documents diff --git a/src/jvm/src/main/kotlin/DocumentConfig.kt b/src/jvm/src/main/kotlin/jvm/DocumentConfig.kt similarity index 67% rename from src/jvm/src/main/kotlin/DocumentConfig.kt rename to src/jvm/src/main/kotlin/jvm/DocumentConfig.kt index 81431d7..d8b39fd 100644 --- a/src/jvm/src/main/kotlin/DocumentConfig.kt +++ b/src/jvm/src/main/kotlin/jvm/DocumentConfig.kt @@ -1,6 +1,6 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.DocumentSerializer +import solutions.bitbadger.documents.DocumentSerializer /** * Configuration for document serialization diff --git a/src/jvm/src/main/kotlin/Exists.kt b/src/jvm/src/main/kotlin/jvm/Exists.kt similarity index 96% rename from src/jvm/src/main/kotlin/Exists.kt rename to src/jvm/src/main/kotlin/jvm/Exists.kt index 7e55f89..d69fa88 100644 --- a/src/jvm/src/main/kotlin/Exists.kt +++ b/src/jvm/src/main/kotlin/jvm/Exists.kt @@ -1,7 +1,8 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.Exists +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.extensions.customScalar +import solutions.bitbadger.documents.query.Exists import java.sql.Connection /** diff --git a/src/jvm/src/main/kotlin/Find.kt b/src/jvm/src/main/kotlin/jvm/Find.kt similarity index 98% rename from src/jvm/src/main/kotlin/Find.kt rename to src/jvm/src/main/kotlin/jvm/Find.kt index 70cef9c..ab22a91 100644 --- a/src/jvm/src/main/kotlin/Find.kt +++ b/src/jvm/src/main/kotlin/jvm/Find.kt @@ -1,8 +1,10 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.Find -import solutions.bitbadger.documents.common.query.orderBy +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.extensions.customList +import solutions.bitbadger.documents.extensions.customSingle +import solutions.bitbadger.documents.query.Find +import solutions.bitbadger.documents.query.orderBy import java.sql.Connection /** diff --git a/src/jvm/src/main/kotlin/NullDocumentSerializer.kt b/src/jvm/src/main/kotlin/jvm/NullDocumentSerializer.kt similarity index 85% rename from src/jvm/src/main/kotlin/NullDocumentSerializer.kt rename to src/jvm/src/main/kotlin/jvm/NullDocumentSerializer.kt index 0fd5ea4..41a7b63 100644 --- a/src/jvm/src/main/kotlin/NullDocumentSerializer.kt +++ b/src/jvm/src/main/kotlin/jvm/NullDocumentSerializer.kt @@ -1,6 +1,6 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.DocumentSerializer +import solutions.bitbadger.documents.DocumentSerializer /** * A serializer that tells the user to implement another one diff --git a/src/jvm/src/main/kotlin/Parameters.kt b/src/jvm/src/main/kotlin/jvm/Parameters.kt similarity index 97% rename from src/jvm/src/main/kotlin/Parameters.kt rename to src/jvm/src/main/kotlin/jvm/Parameters.kt index e8b94b6..a614480 100644 --- a/src/jvm/src/main/kotlin/Parameters.kt +++ b/src/jvm/src/main/kotlin/jvm/Parameters.kt @@ -1,6 +1,7 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.ParameterName import java.sql.Connection import java.sql.PreparedStatement import java.sql.SQLException diff --git a/src/jvm/src/main/kotlin/Patch.kt b/src/jvm/src/main/kotlin/jvm/Patch.kt similarity index 96% rename from src/jvm/src/main/kotlin/Patch.kt rename to src/jvm/src/main/kotlin/jvm/Patch.kt index a59fc2f..80d265a 100644 --- a/src/jvm/src/main/kotlin/Patch.kt +++ b/src/jvm/src/main/kotlin/jvm/Patch.kt @@ -1,7 +1,8 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.Patch +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.extensions.customNonQuery +import solutions.bitbadger.documents.query.Patch import java.sql.Connection /** diff --git a/src/jvm/src/main/kotlin/RemoveFields.kt b/src/jvm/src/main/kotlin/jvm/RemoveFields.kt similarity index 97% rename from src/jvm/src/main/kotlin/RemoveFields.kt rename to src/jvm/src/main/kotlin/jvm/RemoveFields.kt index 3a52eba..22bc308 100644 --- a/src/jvm/src/main/kotlin/RemoveFields.kt +++ b/src/jvm/src/main/kotlin/jvm/RemoveFields.kt @@ -1,7 +1,8 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.RemoveFields +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.extensions.customNonQuery +import solutions.bitbadger.documents.query.RemoveFields import java.sql.Connection /** diff --git a/src/jvm/src/main/kotlin/Results.kt b/src/jvm/src/main/kotlin/jvm/Results.kt similarity index 93% rename from src/jvm/src/main/kotlin/Results.kt rename to src/jvm/src/main/kotlin/jvm/Results.kt index 028007c..f670a67 100644 --- a/src/jvm/src/main/kotlin/Results.kt +++ b/src/jvm/src/main/kotlin/jvm/Results.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.SQLException diff --git a/src/common/src/main/kotlin/query/Count.kt b/src/jvm/src/main/kotlin/query/Count.kt similarity index 87% rename from src/common/src/main/kotlin/query/Count.kt rename to src/jvm/src/main/kotlin/query/Count.kt index 28b726d..e3db117 100644 --- a/src/common/src/main/kotlin/query/Count.kt +++ b/src/jvm/src/main/kotlin/query/Count.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.query.byFields as byFieldsBase; +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.query.byFields as byFieldsBase; /** * Functions to count documents diff --git a/src/common/src/main/kotlin/query/Definition.kt b/src/jvm/src/main/kotlin/query/Definition.kt similarity index 97% rename from src/common/src/main/kotlin/query/Definition.kt rename to src/jvm/src/main/kotlin/query/Definition.kt index c43ea3b..6187216 100644 --- a/src/common/src/main/kotlin/query/Definition.kt +++ b/src/jvm/src/main/kotlin/query/Definition.kt @@ -1,6 +1,6 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.* /** * Functions to create queries to define tables and indexes diff --git a/src/common/src/main/kotlin/query/Delete.kt b/src/jvm/src/main/kotlin/query/Delete.kt similarity index 87% rename from src/common/src/main/kotlin/query/Delete.kt rename to src/jvm/src/main/kotlin/query/Delete.kt index e5fc511..0899c8b 100644 --- a/src/common/src/main/kotlin/query/Delete.kt +++ b/src/jvm/src/main/kotlin/query/Delete.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.query.byFields as byFieldsBase -import solutions.bitbadger.documents.common.query.byId as byIdBase +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.query.byFields as byFieldsBase +import solutions.bitbadger.documents.query.byId as byIdBase /** * Functions to delete documents diff --git a/src/common/src/main/kotlin/query/Document.kt b/src/jvm/src/main/kotlin/query/Document.kt similarity index 91% rename from src/common/src/main/kotlin/query/Document.kt rename to src/jvm/src/main/kotlin/query/Document.kt index 6ee19b9..c60c986 100644 --- a/src/common/src/main/kotlin/query/Document.kt +++ b/src/jvm/src/main/kotlin/query/Document.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.AutoId -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect /** * Functions for document-level operations diff --git a/src/common/src/main/kotlin/query/Exists.kt b/src/jvm/src/main/kotlin/query/Exists.kt similarity index 93% rename from src/common/src/main/kotlin/query/Exists.kt rename to src/jvm/src/main/kotlin/query/Exists.kt index 7e21c8e..54689d0 100644 --- a/src/common/src/main/kotlin/query/Exists.kt +++ b/src/jvm/src/main/kotlin/query/Exists.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch /** * Functions to check for document existence diff --git a/src/common/src/main/kotlin/query/Find.kt b/src/jvm/src/main/kotlin/query/Find.kt similarity index 87% rename from src/common/src/main/kotlin/query/Find.kt rename to src/jvm/src/main/kotlin/query/Find.kt index c622485..6d57232 100644 --- a/src/common/src/main/kotlin/query/Find.kt +++ b/src/jvm/src/main/kotlin/query/Find.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.query.byId as byIdBase -import solutions.bitbadger.documents.common.query.byFields as byFieldsBase +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.query.byId as byIdBase +import solutions.bitbadger.documents.query.byFields as byFieldsBase /** * Functions to retrieve documents diff --git a/src/common/src/main/kotlin/query/Patch.kt b/src/jvm/src/main/kotlin/query/Patch.kt similarity index 84% rename from src/common/src/main/kotlin/query/Patch.kt rename to src/jvm/src/main/kotlin/query/Patch.kt index 3589154..dc1e699 100644 --- a/src/common/src/main/kotlin/query/Patch.kt +++ b/src/jvm/src/main/kotlin/query/Patch.kt @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.query.byFields as byFieldsBase -import solutions.bitbadger.documents.common.query.byId as byIdBase +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +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 diff --git a/src/common/src/main/kotlin/query/Query.kt b/src/jvm/src/main/kotlin/query/Query.kt similarity index 90% rename from src/common/src/main/kotlin/query/Query.kt rename to src/jvm/src/main/kotlin/query/Query.kt index 5d6cafd..39784bc 100644 --- a/src/common/src/main/kotlin/query/Query.kt +++ b/src/jvm/src/main/kotlin/query/Query.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch // ~~~ TOP-LEVEL FUNCTIONS FOR THE QUERY PACKAGE ~~~ diff --git a/src/common/src/main/kotlin/query/RemoveFields.kt b/src/jvm/src/main/kotlin/query/RemoveFields.kt similarity index 92% rename from src/common/src/main/kotlin/query/RemoveFields.kt rename to src/jvm/src/main/kotlin/query/RemoveFields.kt index 0190269..b3842c4 100644 --- a/src/common/src/main/kotlin/query/RemoveFields.kt +++ b/src/jvm/src/main/kotlin/query/RemoveFields.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.byFields as byFieldsBase -import solutions.bitbadger.documents.common.query.byId as byIdBase +import solutions.bitbadger.documents.* +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 diff --git a/src/common/src/main/kotlin/query/Where.kt b/src/jvm/src/main/kotlin/query/Where.kt similarity index 87% rename from src/common/src/main/kotlin/query/Where.kt rename to src/jvm/src/main/kotlin/query/Where.kt index 7e49aa2..dce4a32 100644 --- a/src/common/src/main/kotlin/query/Where.kt +++ b/src/jvm/src/main/kotlin/query/Where.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch +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 /** * Functions to create `WHERE` clause fragments diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java index d2f2ee4..a0f48d0 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java @@ -3,8 +3,8 @@ package solutions.bitbadger.documents.java.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.*; -import solutions.bitbadger.documents.java.Parameters; +import solutions.bitbadger.documents.*; +import solutions.bitbadger.documents.java.jvm.Parameters; import java.util.List; diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java index 4556020..912dc4e 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java @@ -1,7 +1,7 @@ package solutions.bitbadger.documents.java.java.integration.common; -import solutions.bitbadger.documents.common.Field; -import solutions.bitbadger.documents.java.Count; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.java.jvm.Count; import solutions.bitbadger.documents.java.integration.ThrowawayDatabase; import solutions.bitbadger.documents.java.java.testDocs.JsonDocument; diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java index ec59271..3c7338e 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.java.java.integration.sqlite; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.DocumentException; +import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.java.integration.sqlite.SQLiteDB; import solutions.bitbadger.documents.java.java.integration.common.CountFunctions; diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java index d1d02c6..97cd733 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java @@ -1,7 +1,7 @@ package solutions.bitbadger.documents.java.java.testDocs; import kotlinx.serialization.Serializable; -import solutions.bitbadger.documents.java.Document; +import solutions.bitbadger.documents.java.jvm.Document; import solutions.bitbadger.documents.java.integration.ThrowawayDatabase; import java.util.List; diff --git a/src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt b/src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt index f491be5..a0b493f 100644 --- a/src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt +++ b/src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt @@ -1,6 +1,6 @@ package solutions.bitbadger.documents.java -import solutions.bitbadger.documents.common.DocumentSerializer +import solutions.bitbadger.documents.DocumentSerializer import com.fasterxml.jackson.databind.ObjectMapper /** diff --git a/src/jvm/src/test/kotlin/ParametersTest.kt b/src/jvm/src/test/kotlin/ParametersTest.kt index 6e31030..f2469ed 100644 --- a/src/jvm/src/test/kotlin/ParametersTest.kt +++ b/src/jvm/src/test/kotlin/ParametersTest.kt @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.java import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.* import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotSame diff --git a/src/jvm/src/test/kotlin/integration/Types.kt b/src/jvm/src/test/kotlin/integration/Types.kt index d239866..9cf9462 100644 --- a/src/jvm/src/test/kotlin/integration/Types.kt +++ b/src/jvm/src/test/kotlin/integration/Types.kt @@ -1,7 +1,7 @@ package solutions.bitbadger.documents.java.integration import kotlinx.serialization.Serializable -import solutions.bitbadger.documents.java.insert +import solutions.bitbadger.documents.java.extensions.insert /** The test table name to use for integration tests */ const val TEST_TABLE = "test_table" diff --git a/src/jvm/src/test/kotlin/integration/common/Count.kt b/src/jvm/src/test/kotlin/integration/common/Count.kt index e8b3258..b452aab 100644 --- a/src/jvm/src/test/kotlin/integration/common/Count.kt +++ b/src/jvm/src/test/kotlin/integration/common/Count.kt @@ -1,10 +1,10 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.java.countAll -import solutions.bitbadger.documents.java.countByContains -import solutions.bitbadger.documents.java.countByFields -import solutions.bitbadger.documents.java.countByJsonPath +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.java.extensions.countAll +import solutions.bitbadger.documents.java.extensions.countByContains +import solutions.bitbadger.documents.java.extensions.countByFields +import solutions.bitbadger.documents.java.extensions.countByJsonPath import solutions.bitbadger.documents.java.integration.JsonDocument import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase diff --git a/src/jvm/src/test/kotlin/integration/common/Custom.kt b/src/jvm/src/test/kotlin/integration/common/Custom.kt index a66e4e8..5e8d106 100644 --- a/src/jvm/src/test/kotlin/integration/common/Custom.kt +++ b/src/jvm/src/test/kotlin/integration/common/Custom.kt @@ -1,13 +1,16 @@ package solutions.bitbadger.documents.java.integration.common +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.common.query.Count -import solutions.bitbadger.documents.common.query.Delete -import solutions.bitbadger.documents.common.query.Find import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.extensions.* import solutions.bitbadger.documents.java.integration.JsonDocument import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.jvm.Results import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull diff --git a/src/jvm/src/test/kotlin/integration/common/Definition.kt b/src/jvm/src/test/kotlin/integration/common/Definition.kt index 6f36226..7c7dc31 100644 --- a/src/jvm/src/test/kotlin/integration/common/Definition.kt +++ b/src/jvm/src/test/kotlin/integration/common/Definition.kt @@ -1,9 +1,9 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.common.DocumentIndex -import solutions.bitbadger.documents.java.ensureDocumentIndex -import solutions.bitbadger.documents.java.ensureFieldIndex -import solutions.bitbadger.documents.java.ensureTable +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.java.extensions.ensureDocumentIndex +import solutions.bitbadger.documents.java.extensions.ensureFieldIndex +import solutions.bitbadger.documents.java.extensions.ensureTable import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase import kotlin.test.assertFalse diff --git a/src/jvm/src/test/kotlin/integration/common/Delete.kt b/src/jvm/src/test/kotlin/integration/common/Delete.kt index 2ea4338..cead888 100644 --- a/src/jvm/src/test/kotlin/integration/common/Delete.kt +++ b/src/jvm/src/test/kotlin/integration/common/Delete.kt @@ -1,7 +1,7 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.java.extensions.* import solutions.bitbadger.documents.java.integration.JsonDocument import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase diff --git a/src/jvm/src/test/kotlin/integration/common/Document.kt b/src/jvm/src/test/kotlin/integration/common/Document.kt index 2a1eb98..43fa8fa 100644 --- a/src/jvm/src/test/kotlin/integration/common/Document.kt +++ b/src/jvm/src/test/kotlin/integration/common/Document.kt @@ -1,7 +1,10 @@ package solutions.bitbadger.documents.java.integration.common +import AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.extensions.* import solutions.bitbadger.documents.java.integration.* import kotlin.test.* diff --git a/src/jvm/src/test/kotlin/integration/common/Exists.kt b/src/jvm/src/test/kotlin/integration/common/Exists.kt index 3d5513e..750eb07 100644 --- a/src/jvm/src/test/kotlin/integration/common/Exists.kt +++ b/src/jvm/src/test/kotlin/integration/common/Exists.kt @@ -1,10 +1,10 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.java.existsByContains -import solutions.bitbadger.documents.java.existsByFields -import solutions.bitbadger.documents.java.existsById -import solutions.bitbadger.documents.java.existsByJsonPath +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.java.extensions.existsByContains +import solutions.bitbadger.documents.java.extensions.existsByFields +import solutions.bitbadger.documents.java.extensions.existsById +import solutions.bitbadger.documents.java.extensions.existsByJsonPath import solutions.bitbadger.documents.java.integration.JsonDocument import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase diff --git a/src/jvm/src/test/kotlin/integration/common/Find.kt b/src/jvm/src/test/kotlin/integration/common/Find.kt index 3be673c..d6fb4e5 100644 --- a/src/jvm/src/test/kotlin/integration/common/Find.kt +++ b/src/jvm/src/test/kotlin/integration/common/Find.kt @@ -1,7 +1,10 @@ package solutions.bitbadger.documents.java.integration.common +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.extensions.* import solutions.bitbadger.documents.java.integration.* import kotlin.test.assertEquals import kotlin.test.assertNotNull diff --git a/src/jvm/src/test/kotlin/integration/common/Patch.kt b/src/jvm/src/test/kotlin/integration/common/Patch.kt index 05982ed..a74e670 100644 --- a/src/jvm/src/test/kotlin/integration/common/Patch.kt +++ b/src/jvm/src/test/kotlin/integration/common/Patch.kt @@ -1,7 +1,7 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.java.extensions.* import solutions.bitbadger.documents.java.integration.JsonDocument import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase diff --git a/src/jvm/src/test/kotlin/integration/common/RemoveFields.kt b/src/jvm/src/test/kotlin/integration/common/RemoveFields.kt index d6f2387..f70251e 100644 --- a/src/jvm/src/test/kotlin/integration/common/RemoveFields.kt +++ b/src/jvm/src/test/kotlin/integration/common/RemoveFields.kt @@ -1,7 +1,7 @@ package solutions.bitbadger.documents.java.integration.common -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.java.extensions.* import solutions.bitbadger.documents.java.integration.JsonDocument import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase diff --git a/src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt b/src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt index 87413b1..5c90daa 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt +++ b/src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt @@ -1,9 +1,18 @@ package solutions.bitbadger.documents.java.integration.postgresql +import AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType import solutions.bitbadger.documents.common.* import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.extensions.customNonQuery +import solutions.bitbadger.documents.java.extensions.customScalar +import solutions.bitbadger.documents.java.extensions.ensureTable import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.jvm.DocumentConfig +import solutions.bitbadger.documents.java.jvm.Results /** * A wrapper for a throwaway PostgreSQL database diff --git a/src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt index 4b3581e..3b4aeda 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt +++ b/src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.java.integration.common.Count import kotlin.test.Test diff --git a/src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt index ca943f4..b43f661 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt +++ b/src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.java.integration.common.Definition import kotlin.test.Test diff --git a/src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt index 112711f..9947de5 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt +++ b/src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.java.integration.common.Delete import kotlin.test.Test diff --git a/src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt index c1502f8..da43f75 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt +++ b/src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.java.integration.common.Exists import kotlin.test.Test diff --git a/src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt index dc893b6..b40d364 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt +++ b/src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.java.integration.common.Find import kotlin.test.Test diff --git a/src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt index 1a51dae..7c2a15a 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt +++ b/src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.java.integration.common.Patch import kotlin.test.Test diff --git a/src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt b/src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt index 3cfeab8..4dc23dd 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt +++ b/src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.java.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.common.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.java.integration.common.RemoveFields import kotlin.test.Test diff --git a/src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt b/src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt index 5075ae7..cdcbd32 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt +++ b/src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt @@ -1,9 +1,17 @@ package solutions.bitbadger.documents.java.integration.sqlite +import AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType import solutions.bitbadger.documents.common.* import solutions.bitbadger.documents.java.* +import solutions.bitbadger.documents.java.extensions.customScalar +import solutions.bitbadger.documents.java.extensions.ensureTable import solutions.bitbadger.documents.java.integration.TEST_TABLE import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.jvm.DocumentConfig +import solutions.bitbadger.documents.java.jvm.Results import java.io.File /** diff --git a/src/kotlin/src/main/kotlin/ConnectionExtensions.kt b/src/kotlin/src/main/kotlin/ConnectionExtensions.kt index 28e951b..599cb22 100644 --- a/src/kotlin/src/main/kotlin/ConnectionExtensions.kt +++ b/src/kotlin/src/main/kotlin/ConnectionExtensions.kt @@ -1,10 +1,10 @@ package solutions.bitbadger.documents.kotlin -import solutions.bitbadger.documents.common.DocumentIndex -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.java.Document +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.java.jvm.Document import java.sql.Connection import java.sql.ResultSet diff --git a/src/kotlin/src/main/kotlin/Count.kt b/src/kotlin/src/main/kotlin/Count.kt index e2ac2d5..02836a3 100644 --- a/src/kotlin/src/main/kotlin/Count.kt +++ b/src/kotlin/src/main/kotlin/Count.kt @@ -1,11 +1,10 @@ package solutions.bitbadger.documents.kotlin -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType -import solutions.bitbadger.documents.common.query.Count +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType import java.sql.Connection diff --git a/src/kotlin/src/main/kotlin/Custom.kt b/src/kotlin/src/main/kotlin/Custom.kt index b3f75fd..d84df88 100644 --- a/src/kotlin/src/main/kotlin/Custom.kt +++ b/src/kotlin/src/main/kotlin/Custom.kt @@ -1,8 +1,7 @@ package solutions.bitbadger.documents.kotlin -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.java.Custom +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Parameter import java.sql.Connection import java.sql.ResultSet diff --git a/src/kotlin/src/main/kotlin/Definition.kt b/src/kotlin/src/main/kotlin/Definition.kt index 07af62a..8c2b2d9 100644 --- a/src/kotlin/src/main/kotlin/Definition.kt +++ b/src/kotlin/src/main/kotlin/Definition.kt @@ -1,4 +1,4 @@ -import solutions.bitbadger.documents.common.DocumentIndex +import solutions.bitbadger.documents.DocumentIndex import java.sql.Connection /** diff --git a/src/kotlin/src/main/kotlin/Delete.kt b/src/kotlin/src/main/kotlin/Delete.kt index 07288a2..e092b05 100644 --- a/src/kotlin/src/main/kotlin/Delete.kt +++ b/src/kotlin/src/main/kotlin/Delete.kt @@ -1,7 +1,7 @@ -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType import java.sql.Connection /** diff --git a/src/kotlin/src/main/kotlin/Exists.kt b/src/kotlin/src/main/kotlin/Exists.kt index dfb1eb2..5146ba7 100644 --- a/src/kotlin/src/main/kotlin/Exists.kt +++ b/src/kotlin/src/main/kotlin/Exists.kt @@ -1,7 +1,7 @@ -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType import java.sql.Connection /** diff --git a/src/kotlin/src/main/kotlin/Find.kt b/src/kotlin/src/main/kotlin/Find.kt index 3e58e71..d698790 100644 --- a/src/kotlin/src/main/kotlin/Find.kt +++ b/src/kotlin/src/main/kotlin/Find.kt @@ -1,9 +1,8 @@ -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType import java.sql.Connection -import solutions.bitbadger.documents.common.query.orderBy /** * Functions to find and retrieve documents diff --git a/src/kotlin/src/main/kotlin/Parameters.kt b/src/kotlin/src/main/kotlin/Parameters.kt index 46cd843..7dae9e9 100644 --- a/src/kotlin/src/main/kotlin/Parameters.kt +++ b/src/kotlin/src/main/kotlin/Parameters.kt @@ -1,3 +1,5 @@ +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.ParameterName import solutions.bitbadger.documents.common.* import java.sql.Connection import java.sql.PreparedStatement diff --git a/src/kotlin/src/main/kotlin/Patch.kt b/src/kotlin/src/main/kotlin/Patch.kt index 5217b4a..ea8ebc3 100644 --- a/src/kotlin/src/main/kotlin/Patch.kt +++ b/src/kotlin/src/main/kotlin/Patch.kt @@ -1,7 +1,7 @@ -import solutions.bitbadger.documents.common.Field -import solutions.bitbadger.documents.common.FieldMatch -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.common.ParameterType +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType import java.sql.Connection /** diff --git a/src/kotlin/src/main/kotlin/RemoveFields.kt b/src/kotlin/src/main/kotlin/RemoveFields.kt index d75c63b..9d40864 100644 --- a/src/kotlin/src/main/kotlin/RemoveFields.kt +++ b/src/kotlin/src/main/kotlin/RemoveFields.kt @@ -1,3 +1,4 @@ +import solutions.bitbadger.documents.* import solutions.bitbadger.documents.common.* import java.sql.Connection diff --git a/src/kotlin/src/main/kotlin/Results.kt b/src/kotlin/src/main/kotlin/Results.kt index f961b00..131dd66 100644 --- a/src/kotlin/src/main/kotlin/Results.kt +++ b/src/kotlin/src/main/kotlin/Results.kt @@ -1,9 +1,8 @@ package solutions.bitbadger.documents.kotlin -import solutions.bitbadger.documents.common.Configuration -import solutions.bitbadger.documents.common.Dialect -import solutions.bitbadger.documents.common.DocumentException -import solutions.bitbadger.documents.java.Results +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.SQLException diff --git a/src/main/kotlin/Custom.kt b/src/main/kotlin/Custom.kt index e3e8ca7..fd52866 100644 --- a/src/main/kotlin/Custom.kt +++ b/src/main/kotlin/Custom.kt @@ -1,7 +1,5 @@ package solutions.bitbadger.documents -import solutions.bitbadger.documents.common.Parameter -import solutions.bitbadger.documents.java.Custom import java.sql.Connection import java.sql.ResultSet diff --git a/src/pom.xml b/src/pom.xml index fa7686e..1c520aa 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -46,7 +46,7 @@ common - java + jvm -- 2.47.2 From 11e3200ff7063ae197acbcfdbf9018154181c25e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 14 Mar 2025 22:38:23 -0400 Subject: [PATCH 44/88] Reorg complete, jvm tests pass --- src/common/pom.xml | 90 ------------- .../src/main/kotlin/extensions/Connection.kt | 11 +- .../bitbadger/documents}/java/AutoIdTest.java | 6 +- .../documents}/java/DocumentIndexTest.java | 2 +- .../documents}/java/FieldMatchTest.java | 2 +- .../bitbadger/documents}/java/FieldTest.java | 2 +- .../bitbadger/documents}/java/OpTest.java | 2 +- .../documents}/java/ParameterNameTest.java | 2 +- .../documents}/java/ParameterTest.java | 2 +- .../java/{java => jvm}/ParametersTest.java | 4 +- .../integration/common/CountFunctions.java | 10 +- .../integration/postgresql/CountIT.java | 6 +- .../integration/sqlite/CountIT.java | 6 +- .../testDocs => support}/ByteIdClass.java | 2 +- .../testDocs => support}/IntIdClass.java | 2 +- .../testDocs => support}/JsonDocument.java | 8 +- .../testDocs => support}/LongIdClass.java | 2 +- .../testDocs => support}/ShortIdClass.java | 2 +- .../testDocs => support}/StringIdClass.java | 2 +- .../testDocs => support}/SubDocument.java | 2 +- .../src/test/kotlin/AutoIdTest.kt | 4 +- .../src/test/kotlin/ComparisonTest.kt | 3 +- .../src/test/kotlin/ConfigurationTest.kt | 6 +- .../src/test/kotlin/DialectTest.kt | 4 +- .../src/test/kotlin/DocumentIndexTest.kt | 3 +- .../src/test/kotlin/FieldMatchTest.kt | 3 +- .../src/test/kotlin/FieldTest.kt | 3 +- src/{common => jvm}/src/test/kotlin/OpTest.kt | 3 +- .../src/test/kotlin/ParameterNameTest.kt | 3 +- .../src/test/kotlin/ParameterTest.kt | 5 +- .../test/kotlin/{ => jvm}/ParametersTest.kt | 5 +- .../{ => jvm}/integration/common/Count.kt | 11 +- .../{ => jvm}/integration/common/Custom.kt | 20 ++- .../integration/common/Definition.kt | 12 +- .../{ => jvm}/integration/common/Delete.kt | 10 +- .../{ => jvm}/integration/common/Document.kt | 9 +- .../{ => jvm}/integration/common/Exists.kt | 11 +- .../{ => jvm}/integration/common/Find.kt | 7 +- .../{ => jvm}/integration/common/Patch.kt | 8 +- .../integration/common/RemoveFields.kt | 8 +- .../integration/postgresql/CountIT.kt | 4 +- .../integration/postgresql/CustomIT.kt | 4 +- .../integration/postgresql/DefinitionIT.kt | 4 +- .../integration/postgresql/DeleteIT.kt | 4 +- .../integration/postgresql/DocumentIT.kt | 4 +- .../integration/postgresql/ExistsIT.kt | 4 +- .../integration/postgresql/FindIT.kt | 4 +- .../integration/postgresql/PatchIT.kt | 4 +- .../{ => jvm}/integration/postgresql/PgDB.kt | 21 ++-- .../integration/postgresql/RemoveFieldsIT.kt | 4 +- .../{ => jvm}/integration/sqlite/CountIT.kt | 4 +- .../{ => jvm}/integration/sqlite/CustomIT.kt | 4 +- .../integration/sqlite/DefinitionIT.kt | 4 +- .../{ => jvm}/integration/sqlite/DeleteIT.kt | 4 +- .../integration/sqlite/DocumentIT.kt | 4 +- .../{ => jvm}/integration/sqlite/ExistsIT.kt | 4 +- .../{ => jvm}/integration/sqlite/FindIT.kt | 4 +- .../{ => jvm}/integration/sqlite/PatchIT.kt | 4 +- .../integration/sqlite/RemoveFieldsIT.kt | 4 +- .../{ => jvm}/integration/sqlite/SQLiteDB.kt | 19 +-- .../src/test/kotlin/query/CountTest.kt | 3 +- .../src/test/kotlin/query/DefinitionTest.kt | 2 +- .../src/test/kotlin/query/DeleteTest.kt | 2 +- .../src/test/kotlin/query/DocumentTest.kt | 5 +- .../src/test/kotlin/query/ExistsTest.kt | 2 +- .../src/test/kotlin/query/FindTest.kt | 2 +- .../src/test/kotlin/query/PatchTest.kt | 2 +- .../src/test/kotlin/query/QueryTest.kt | 7 +- .../src/test/kotlin/query/RemoveFieldsTest.kt | 3 +- .../src/test/kotlin/query/WhereTest.kt | 4 +- .../JacksonDocumentSerializer.kt | 2 +- .../ThrowawayDatabase.kt | 2 +- .../kotlin/{integration => support}/Types.kt | 4 +- src/kotlin/src/main/kotlin/Configuration.kt | 24 ---- src/kotlin/src/main/kotlin/Count.kt | 1 + src/kotlin/src/main/kotlin/Custom.kt | 38 +++--- src/kotlin/src/main/kotlin/DocumentConfig.kt | 33 +++++ .../src/test/kotlin/DocumentConfigTest.kt | 21 ++++ src/main/kotlin/Custom.kt | 118 ------------------ src/pom.xml | 1 - .../documents/java/ConfigurationTest.java | 57 --------- src/test/kotlin/ConfigurationTest.kt | 21 ---- 82 files changed, 232 insertions(+), 537 deletions(-) delete mode 100644 src/common/pom.xml rename src/{common/src/test/java/solutions/bitbadger/documents/common => jvm/src/test/java/solutions/bitbadger/documents}/java/AutoIdTest.java (98%) rename src/{common/src/test/java/solutions/bitbadger/documents/common => jvm/src/test/java/solutions/bitbadger/documents}/java/DocumentIndexTest.java (93%) rename src/{common/src/test/java/solutions/bitbadger/documents/common => jvm/src/test/java/solutions/bitbadger/documents}/java/FieldMatchTest.java (92%) rename src/{common/src/test/java/solutions/bitbadger/documents/common => jvm/src/test/java/solutions/bitbadger/documents}/java/FieldTest.java (99%) rename src/{common/src/test/java/solutions/bitbadger/documents/common => jvm/src/test/java/solutions/bitbadger/documents}/java/OpTest.java (97%) rename src/{common/src/test/java/solutions/bitbadger/documents/common => jvm/src/test/java/solutions/bitbadger/documents}/java/ParameterNameTest.java (96%) rename src/{common/src/test/java/solutions/bitbadger/documents/common => jvm/src/test/java/solutions/bitbadger/documents}/java/ParameterTest.java (96%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java => jvm}/ParametersTest.java (98%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java => jvm}/integration/common/CountFunctions.java (85%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java => jvm}/integration/postgresql/CountIT.java (88%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java => jvm}/integration/sqlite/CountIT.java (86%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java/testDocs => support}/ByteIdClass.java (79%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java/testDocs => support}/IntIdClass.java (79%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java/testDocs => support}/JsonDocument.java (87%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java/testDocs => support}/LongIdClass.java (79%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java/testDocs => support}/ShortIdClass.java (80%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java/testDocs => support}/StringIdClass.java (80%) rename src/jvm/src/test/java/solutions/bitbadger/documents/java/{java/testDocs => support}/SubDocument.java (89%) rename src/{common => jvm}/src/test/kotlin/AutoIdTest.kt (98%) rename src/{common => jvm}/src/test/kotlin/ComparisonTest.kt (98%) rename src/{common => jvm}/src/test/kotlin/ConfigurationTest.kt (85%) rename src/{common => jvm}/src/test/kotlin/DialectTest.kt (90%) rename src/{common => jvm}/src/test/kotlin/DocumentIndexTest.kt (86%) rename src/{common => jvm}/src/test/kotlin/FieldMatchTest.kt (84%) rename src/{common => jvm}/src/test/kotlin/FieldTest.kt (99%) rename src/{common => jvm}/src/test/kotlin/OpTest.kt (96%) rename src/{common => jvm}/src/test/kotlin/ParameterNameTest.kt (92%) rename src/{common => jvm}/src/test/kotlin/ParameterTest.kt (86%) rename src/jvm/src/test/kotlin/{ => jvm}/ParametersTest.kt (98%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/Count.kt (78%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/Custom.kt (81%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/Definition.kt (84%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/Delete.kt (90%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/Document.kt (96%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/Exists.kt (79%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/Find.kt (98%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/Patch.kt (91%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/common/RemoveFields.kt (93%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/CountIT.kt (90%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/CustomIT.kt (89%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/DefinitionIT.kt (86%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/DeleteIT.kt (91%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/DocumentIT.kt (91%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/ExistsIT.kt (91%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/FindIT.kt (97%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/PatchIT.kt (91%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/PgDB.kt (66%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/postgresql/RemoveFieldsIT.kt (94%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/CountIT.kt (89%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/CustomIT.kt (89%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/DefinitionIT.kt (88%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/DeleteIT.kt (90%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/DocumentIT.kt (92%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/ExistsIT.kt (90%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/FindIT.kt (96%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/PatchIT.kt (90%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/RemoveFieldsIT.kt (92%) rename src/jvm/src/test/kotlin/{ => jvm}/integration/sqlite/SQLiteDB.kt (53%) rename src/{common => jvm}/src/test/kotlin/query/CountTest.kt (96%) rename src/{common => jvm}/src/test/kotlin/query/DefinitionTest.kt (98%) rename src/{common => jvm}/src/test/kotlin/query/DeleteTest.kt (98%) rename src/{common => jvm}/src/test/kotlin/query/DocumentTest.kt (97%) rename src/{common => jvm}/src/test/kotlin/query/ExistsTest.kt (98%) rename src/{common => jvm}/src/test/kotlin/query/FindTest.kt (98%) rename src/{common => jvm}/src/test/kotlin/query/PatchTest.kt (98%) rename src/{common => jvm}/src/test/kotlin/query/QueryTest.kt (94%) rename src/{common => jvm}/src/test/kotlin/query/RemoveFieldsTest.kt (97%) rename src/{common => jvm}/src/test/kotlin/query/WhereTest.kt (97%) rename src/jvm/src/test/kotlin/{ => support}/JacksonDocumentSerializer.kt (91%) rename src/jvm/src/test/kotlin/{integration => support}/ThrowawayDatabase.kt (89%) rename src/jvm/src/test/kotlin/{integration => support}/Types.kt (93%) delete mode 100644 src/kotlin/src/main/kotlin/Configuration.kt create mode 100644 src/kotlin/src/main/kotlin/DocumentConfig.kt create mode 100644 src/kotlin/src/test/kotlin/DocumentConfigTest.kt delete mode 100644 src/main/kotlin/Custom.kt delete mode 100644 src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java delete mode 100644 src/test/kotlin/ConfigurationTest.kt diff --git a/src/common/pom.xml b/src/common/pom.xml deleted file mode 100644 index 61cfe2f..0000000 --- a/src/common/pom.xml +++ /dev/null @@ -1,90 +0,0 @@ - - - 4.0.0 - - solutions.bitbadger.documents - common - 4.0.0-alpha1-SNAPSHOT - jar - - - solutions.bitbadger - documents - 4.0.0-alpha1-SNAPSHOT - - - ${project.groupId}:${project.artifactId} - Expose a document store interface for PostgreSQL and SQLite (Common Library) - https://bitbadger.solutions/open-source/relational-documents/jvm/ - - - 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 - - - - src/main/kotlin - src/test/kotlin - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - process-sources - - compile - - - - ${project.basedir}/src/main/kotlin - - - - - test-compile - test-compile - - test-compile - - - - ${project.basedir}/src/test/java - ${project.basedir}/src/test/kotlin - - - - - - - maven-surefire-plugin - 2.22.2 - - - maven-failsafe-plugin - 2.22.2 - - - - integration-test - verify - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java.version} - ${java.version} - - - - - diff --git a/src/jvm/src/main/kotlin/extensions/Connection.kt b/src/jvm/src/main/kotlin/extensions/Connection.kt index 71f7ecc..97c44ab 100644 --- a/src/jvm/src/main/kotlin/extensions/Connection.kt +++ b/src/jvm/src/main/kotlin/extensions/Connection.kt @@ -23,7 +23,8 @@ fun Connection.customList( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> TDoc -) = Custom.list(query, parameters, clazz, this, mapFunc) +) = + solutions.bitbadger.documents.jvm.Custom.list(query, parameters, clazz, this, mapFunc) /** * Execute a query that returns one or no results @@ -39,7 +40,8 @@ fun Connection.customSingle( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> TDoc -) = Custom.single(query, parameters, clazz, this, mapFunc) +) = + solutions.bitbadger.documents.jvm.Custom.single(query, parameters, clazz, this, mapFunc) /** * Execute a query that returns no results @@ -48,7 +50,7 @@ fun Connection.customSingle( * @param parameters Parameters to use for the query */ fun Connection.customNonQuery(query: String, parameters: Collection> = listOf()) = - Custom.nonQuery(query, parameters, this) + solutions.bitbadger.documents.jvm.Custom.nonQuery(query, parameters, this) /** * Execute a query that returns a scalar result @@ -64,7 +66,8 @@ fun Connection.customScalar( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> T -) = Custom.scalar(query, parameters, clazz, this, mapFunc) +) = + solutions.bitbadger.documents.jvm.Custom.scalar(query, parameters, clazz, this, mapFunc) // ~~~ DEFINITION QUERIES ~~~ diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java similarity index 98% rename from src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java index 9ff6314..511d621 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/AutoIdTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.common.java; +package solutions.bitbadger.documents.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import AutoId; +import solutions.bitbadger.documents.AutoId; import solutions.bitbadger.documents.DocumentException; -import solutions.bitbadger.documents.java.java.testDocs.*; +import solutions.bitbadger.documents.java.support.*; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java similarity index 93% rename from src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java index 9440d16..47b9543 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/DocumentIndexTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.java; +package solutions.bitbadger.documents.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java similarity index 92% rename from src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java index 3335d6e..6854747 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldMatchTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.java; +package solutions.bitbadger.documents.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java similarity index 99% rename from src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java index 22192ec..9af8b6d 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/FieldTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.java; +package solutions.bitbadger.documents.java; import kotlin.Pair; import org.junit.jupiter.api.AfterEach; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java similarity index 97% rename from src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java index d26ffbe..3f800c8 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/OpTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.java; +package solutions.bitbadger.documents.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java similarity index 96% rename from src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java index 910289d..4a38121 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterNameTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.java; +package solutions.bitbadger.documents.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java similarity index 96% rename from src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java index 2bce5ed..acbf0cf 100644 --- a/src/common/src/test/java/solutions/bitbadger/documents/common/java/ParameterTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.java; +package solutions.bitbadger.documents.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java similarity index 98% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java index a0f48d0..59902e7 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/ParametersTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.java.java; +package solutions.bitbadger.documents.java.jvm; 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.jvm.Parameters; +import solutions.bitbadger.documents.jvm.Parameters; import java.util.List; diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java similarity index 85% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java index 912dc4e..f3dfd47 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/common/CountFunctions.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java @@ -1,15 +1,15 @@ -package solutions.bitbadger.documents.java.java.integration.common; +package solutions.bitbadger.documents.java.jvm.integration.common; import solutions.bitbadger.documents.Field; -import solutions.bitbadger.documents.java.jvm.Count; -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase; -import solutions.bitbadger.documents.java.java.testDocs.JsonDocument; +import solutions.bitbadger.documents.jvm.Count; +import solutions.bitbadger.documents.support.ThrowawayDatabase; +import solutions.bitbadger.documents.java.support.JsonDocument; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; -import static solutions.bitbadger.documents.java.integration.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; /** * Integration tests for the `Count` object diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java similarity index 88% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java index 54871dc..eb07bec 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/postgresql/CountIT.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.java.integration.postgresql; +package solutions.bitbadger.documents.java.jvm.integration.postgresql; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.java.integration.postgresql.PgDB; -import solutions.bitbadger.documents.java.java.integration.common.CountFunctions; +import solutions.bitbadger.documents.java.jvm.integration.common.CountFunctions; +import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB; /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java similarity index 86% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java index 3c7338e..18a4d70 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/integration/sqlite/CountIT.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.java.java.integration.sqlite; +package solutions.bitbadger.documents.java.jvm.integration.sqlite; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import solutions.bitbadger.documents.DocumentException; -import solutions.bitbadger.documents.java.integration.sqlite.SQLiteDB; -import solutions.bitbadger.documents.java.java.integration.common.CountFunctions; +import solutions.bitbadger.documents.java.jvm.integration.common.CountFunctions; +import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ByteIdClass.java similarity index 79% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ByteIdClass.java index 4ffdf15..bf2be5d 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ByteIdClass.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ByteIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.java.testDocs; +package solutions.bitbadger.documents.java.support; public class ByteIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/IntIdClass.java similarity index 79% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/support/IntIdClass.java index 25a5613..2db8732 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/IntIdClass.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/IntIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.java.testDocs; +package solutions.bitbadger.documents.java.support; public class IntIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java similarity index 87% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java index 97cd733..d18acaa 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/JsonDocument.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java @@ -1,12 +1,12 @@ -package solutions.bitbadger.documents.java.java.testDocs; +package solutions.bitbadger.documents.java.support; import kotlinx.serialization.Serializable; -import solutions.bitbadger.documents.java.jvm.Document; -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase; +import solutions.bitbadger.documents.jvm.Document; +import solutions.bitbadger.documents.support.ThrowawayDatabase; import java.util.List; -import static solutions.bitbadger.documents.java.integration.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; @Serializable public class JsonDocument { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/LongIdClass.java similarity index 79% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/support/LongIdClass.java index 9a56846..665e2c1 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/LongIdClass.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/LongIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.java.testDocs; +package solutions.bitbadger.documents.java.support; public class LongIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ShortIdClass.java similarity index 80% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ShortIdClass.java index c5913cc..6608387 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/ShortIdClass.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ShortIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.java.testDocs; +package solutions.bitbadger.documents.java.support; public class ShortIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/StringIdClass.java similarity index 80% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/support/StringIdClass.java index 35e4945..8264d2b 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/StringIdClass.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/StringIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.java.testDocs; +package solutions.bitbadger.documents.java.support; public class StringIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/SubDocument.java similarity index 89% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java rename to src/jvm/src/test/java/solutions/bitbadger/documents/java/support/SubDocument.java index fa4c730..6d5bdf9 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/java/testDocs/SubDocument.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/SubDocument.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.java.testDocs; +package solutions.bitbadger.documents.java.support; public class SubDocument { diff --git a/src/common/src/test/kotlin/AutoIdTest.kt b/src/jvm/src/test/kotlin/AutoIdTest.kt similarity index 98% rename from src/common/src/test/kotlin/AutoIdTest.kt rename to src/jvm/src/test/kotlin/AutoIdTest.kt index 05f4e59..4a1aeff 100644 --- a/src/common/src/test/kotlin/AutoIdTest.kt +++ b/src/jvm/src/test/kotlin/AutoIdTest.kt @@ -1,10 +1,8 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents -import AutoId import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.DocumentException import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals diff --git a/src/common/src/test/kotlin/ComparisonTest.kt b/src/jvm/src/test/kotlin/ComparisonTest.kt similarity index 98% rename from src/common/src/test/kotlin/ComparisonTest.kt rename to src/jvm/src/test/kotlin/ComparisonTest.kt index e6c5bf5..912000b 100644 --- a/src/common/src/test/kotlin/ComparisonTest.kt +++ b/src/jvm/src/test/kotlin/ComparisonTest.kt @@ -1,8 +1,7 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents 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 diff --git a/src/common/src/test/kotlin/ConfigurationTest.kt b/src/jvm/src/test/kotlin/ConfigurationTest.kt similarity index 85% rename from src/common/src/test/kotlin/ConfigurationTest.kt rename to src/jvm/src/test/kotlin/ConfigurationTest.kt index 338b7c9..a9255a5 100644 --- a/src/common/src/test/kotlin/ConfigurationTest.kt +++ b/src/jvm/src/test/kotlin/ConfigurationTest.kt @@ -1,12 +1,8 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents -import AutoId import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.DocumentException import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/DialectTest.kt b/src/jvm/src/test/kotlin/DialectTest.kt similarity index 90% rename from src/common/src/test/kotlin/DialectTest.kt rename to src/jvm/src/test/kotlin/DialectTest.kt index 3eae724..7dbdce0 100644 --- a/src/common/src/test/kotlin/DialectTest.kt +++ b/src/jvm/src/test/kotlin/DialectTest.kt @@ -1,9 +1,7 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents 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 diff --git a/src/common/src/test/kotlin/DocumentIndexTest.kt b/src/jvm/src/test/kotlin/DocumentIndexTest.kt similarity index 86% rename from src/common/src/test/kotlin/DocumentIndexTest.kt rename to src/jvm/src/test/kotlin/DocumentIndexTest.kt index fb03e7b..f23d6a0 100644 --- a/src/common/src/test/kotlin/DocumentIndexTest.kt +++ b/src/jvm/src/test/kotlin/DocumentIndexTest.kt @@ -1,8 +1,7 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.DocumentIndex import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/FieldMatchTest.kt b/src/jvm/src/test/kotlin/FieldMatchTest.kt similarity index 84% rename from src/common/src/test/kotlin/FieldMatchTest.kt rename to src/jvm/src/test/kotlin/FieldMatchTest.kt index 9c4de5a..2ee51d7 100644 --- a/src/common/src/test/kotlin/FieldMatchTest.kt +++ b/src/jvm/src/test/kotlin/FieldMatchTest.kt @@ -1,8 +1,7 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.FieldMatch import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/FieldTest.kt b/src/jvm/src/test/kotlin/FieldTest.kt similarity index 99% rename from src/common/src/test/kotlin/FieldTest.kt rename to src/jvm/src/test/kotlin/FieldTest.kt index e3a6d4a..a48dbf1 100644 --- a/src/common/src/test/kotlin/FieldTest.kt +++ b/src/jvm/src/test/kotlin/FieldTest.kt @@ -1,10 +1,9 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents 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 diff --git a/src/common/src/test/kotlin/OpTest.kt b/src/jvm/src/test/kotlin/OpTest.kt similarity index 96% rename from src/common/src/test/kotlin/OpTest.kt rename to src/jvm/src/test/kotlin/OpTest.kt index 9543275..095e9a2 100644 --- a/src/common/src/test/kotlin/OpTest.kt +++ b/src/jvm/src/test/kotlin/OpTest.kt @@ -1,8 +1,7 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.Op import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/ParameterNameTest.kt b/src/jvm/src/test/kotlin/ParameterNameTest.kt similarity index 92% rename from src/common/src/test/kotlin/ParameterNameTest.kt rename to src/jvm/src/test/kotlin/ParameterNameTest.kt index 3c09ce0..d8f1d6d 100644 --- a/src/common/src/test/kotlin/ParameterNameTest.kt +++ b/src/jvm/src/test/kotlin/ParameterNameTest.kt @@ -1,8 +1,7 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.ParameterName import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/ParameterTest.kt b/src/jvm/src/test/kotlin/ParameterTest.kt similarity index 86% rename from src/common/src/test/kotlin/ParameterTest.kt rename to src/jvm/src/test/kotlin/ParameterTest.kt index 3a312e4..0dadc04 100644 --- a/src/common/src/test/kotlin/ParameterTest.kt +++ b/src/jvm/src/test/kotlin/ParameterTest.kt @@ -1,10 +1,7 @@ -package solutions.bitbadger.documents.common +package solutions.bitbadger.documents 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 diff --git a/src/jvm/src/test/kotlin/ParametersTest.kt b/src/jvm/src/test/kotlin/jvm/ParametersTest.kt similarity index 98% rename from src/jvm/src/test/kotlin/ParametersTest.kt rename to src/jvm/src/test/kotlin/jvm/ParametersTest.kt index f2469ed..30ef568 100644 --- a/src/jvm/src/test/kotlin/ParametersTest.kt +++ b/src/jvm/src/test/kotlin/jvm/ParametersTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.jvm import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -38,7 +38,8 @@ class ParametersTest { @DisplayName("nameFields works when changing fields") fun nameFieldsChange() { val fields = listOf( - Field.equal("a", ""), Field.equal("e", "", ":hi"), Field.equal("b", ""), Field.notExists("z")) + 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") diff --git a/src/jvm/src/test/kotlin/integration/common/Count.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Count.kt similarity index 78% rename from src/jvm/src/test/kotlin/integration/common/Count.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/Count.kt index b452aab..5ed8ecc 100644 --- a/src/jvm/src/test/kotlin/integration/common/Count.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Count.kt @@ -1,13 +1,8 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.java.extensions.countAll -import solutions.bitbadger.documents.java.extensions.countByContains -import solutions.bitbadger.documents.java.extensions.countByFields -import solutions.bitbadger.documents.java.extensions.countByJsonPath -import solutions.bitbadger.documents.java.integration.JsonDocument -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.extensions.* +import solutions.bitbadger.documents.support.* import kotlin.test.assertEquals /** diff --git a/src/jvm/src/test/kotlin/integration/common/Custom.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt similarity index 81% rename from src/jvm/src/test/kotlin/integration/common/Custom.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt index 5e8d106..34a8160 100644 --- a/src/jvm/src/test/kotlin/integration/common/Custom.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt @@ -1,16 +1,12 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.java.* -import solutions.bitbadger.documents.java.extensions.* -import solutions.bitbadger.documents.java.integration.JsonDocument -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase -import solutions.bitbadger.documents.java.jvm.Results +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.extensions.* +import solutions.bitbadger.documents.jvm.Results +import solutions.bitbadger.documents.query.Count +import solutions.bitbadger.documents.query.Delete +import solutions.bitbadger.documents.query.Find +import solutions.bitbadger.documents.support.* import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull diff --git a/src/jvm/src/test/kotlin/integration/common/Definition.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Definition.kt similarity index 84% rename from src/jvm/src/test/kotlin/integration/common/Definition.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/Definition.kt index 7c7dc31..0d7aa26 100644 --- a/src/jvm/src/test/kotlin/integration/common/Definition.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Definition.kt @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common import solutions.bitbadger.documents.DocumentIndex -import solutions.bitbadger.documents.java.extensions.ensureDocumentIndex -import solutions.bitbadger.documents.java.extensions.ensureFieldIndex -import solutions.bitbadger.documents.java.extensions.ensureTable -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.extensions.ensureDocumentIndex +import solutions.bitbadger.documents.extensions.ensureFieldIndex +import solutions.bitbadger.documents.extensions.ensureTable +import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.support.ThrowawayDatabase import kotlin.test.assertFalse import kotlin.test.assertTrue diff --git a/src/jvm/src/test/kotlin/integration/common/Delete.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Delete.kt similarity index 90% rename from src/jvm/src/test/kotlin/integration/common/Delete.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/Delete.kt index cead888..e0ceada 100644 --- a/src/jvm/src/test/kotlin/integration/common/Delete.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Delete.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.java.extensions.* -import solutions.bitbadger.documents.java.integration.JsonDocument -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.extensions.* +import solutions.bitbadger.documents.support.JsonDocument +import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.support.ThrowawayDatabase import kotlin.test.assertEquals /** diff --git a/src/jvm/src/test/kotlin/integration/common/Document.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Document.kt similarity index 96% rename from src/jvm/src/test/kotlin/integration/common/Document.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/Document.kt index 43fa8fa..5b8e7ec 100644 --- a/src/jvm/src/test/kotlin/integration/common/Document.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Document.kt @@ -1,11 +1,10 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common -import AutoId +import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.java.extensions.* -import solutions.bitbadger.documents.java.integration.* +import solutions.bitbadger.documents.extensions.* +import solutions.bitbadger.documents.support.* import kotlin.test.* /** diff --git a/src/jvm/src/test/kotlin/integration/common/Exists.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Exists.kt similarity index 79% rename from src/jvm/src/test/kotlin/integration/common/Exists.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/Exists.kt index 750eb07..a981f81 100644 --- a/src/jvm/src/test/kotlin/integration/common/Exists.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Exists.kt @@ -1,13 +1,8 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.java.extensions.existsByContains -import solutions.bitbadger.documents.java.extensions.existsByFields -import solutions.bitbadger.documents.java.extensions.existsById -import solutions.bitbadger.documents.java.extensions.existsByJsonPath -import solutions.bitbadger.documents.java.integration.JsonDocument -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.extensions.* +import solutions.bitbadger.documents.support.* import kotlin.test.assertFalse import kotlin.test.assertTrue diff --git a/src/jvm/src/test/kotlin/integration/common/Find.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Find.kt similarity index 98% rename from src/jvm/src/test/kotlin/integration/common/Find.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/Find.kt index d6fb4e5..03aaa94 100644 --- a/src/jvm/src/test/kotlin/integration/common/Find.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Find.kt @@ -1,11 +1,10 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.java.extensions.* -import solutions.bitbadger.documents.java.integration.* +import solutions.bitbadger.documents.extensions.* +import solutions.bitbadger.documents.support.* import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull diff --git a/src/jvm/src/test/kotlin/integration/common/Patch.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Patch.kt similarity index 91% rename from src/jvm/src/test/kotlin/integration/common/Patch.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/Patch.kt index a74e670..30c0731 100644 --- a/src/jvm/src/test/kotlin/integration/common/Patch.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Patch.kt @@ -1,10 +1,8 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.java.extensions.* -import solutions.bitbadger.documents.java.integration.JsonDocument -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.extensions.* +import solutions.bitbadger.documents.support.* import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull diff --git a/src/jvm/src/test/kotlin/integration/common/RemoveFields.kt b/src/jvm/src/test/kotlin/jvm/integration/common/RemoveFields.kt similarity index 93% rename from src/jvm/src/test/kotlin/integration/common/RemoveFields.kt rename to src/jvm/src/test/kotlin/jvm/integration/common/RemoveFields.kt index f70251e..2c90f1b 100644 --- a/src/jvm/src/test/kotlin/integration/common/RemoveFields.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/RemoveFields.kt @@ -1,10 +1,8 @@ -package solutions.bitbadger.documents.java.integration.common +package solutions.bitbadger.documents.jvm.integration.common import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.java.extensions.* -import solutions.bitbadger.documents.java.integration.JsonDocument -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase +import solutions.bitbadger.documents.extensions.* +import solutions.bitbadger.documents.support.* import kotlin.test.* diff --git a/src/jvm/src/test/kotlin/integration/postgresql/CountIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt similarity index 90% rename from src/jvm/src/test/kotlin/integration/postgresql/CountIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt index 04afcc4..21a50c1 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/CountIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Count +import solutions.bitbadger.documents.jvm.integration.common.Count import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/postgresql/CustomIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt similarity index 89% rename from src/jvm/src/test/kotlin/integration/postgresql/CustomIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt index 673d78d..a8ea10e 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/CustomIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Custom +import solutions.bitbadger.documents.jvm.integration.common.Custom import kotlin.test.Test diff --git a/src/jvm/src/test/kotlin/integration/postgresql/DefinitionIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt similarity index 86% rename from src/jvm/src/test/kotlin/integration/postgresql/DefinitionIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt index ddf47a6..298fb93 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/DefinitionIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Definition +import solutions.bitbadger.documents.jvm.integration.common.Definition import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/postgresql/DeleteIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt similarity index 91% rename from src/jvm/src/test/kotlin/integration/postgresql/DeleteIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt index f5332c7..be56697 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/DeleteIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Delete +import solutions.bitbadger.documents.jvm.integration.common.Delete import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/postgresql/DocumentIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt similarity index 91% rename from src/jvm/src/test/kotlin/integration/postgresql/DocumentIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt index bb070d5..478e9e1 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/DocumentIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Document +import solutions.bitbadger.documents.jvm.integration.common.Document import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/postgresql/ExistsIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt similarity index 91% rename from src/jvm/src/test/kotlin/integration/postgresql/ExistsIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt index 4c358e3..848962a 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/ExistsIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Exists +import solutions.bitbadger.documents.jvm.integration.common.Exists import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/postgresql/FindIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt similarity index 97% rename from src/jvm/src/test/kotlin/integration/postgresql/FindIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt index 9907a6c..b6d8e98 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/FindIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Find +import solutions.bitbadger.documents.jvm.integration.common.Find import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/postgresql/PatchIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt similarity index 91% rename from src/jvm/src/test/kotlin/integration/postgresql/PatchIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt index 2bf5e63..7cedf46 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/PatchIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Patch +import solutions.bitbadger.documents.jvm.integration.common.Patch import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/PgDB.kt similarity index 66% rename from src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/PgDB.kt index 5c90daa..6f60f71 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/PgDB.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/PgDB.kt @@ -1,18 +1,11 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql -import AutoId -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.java.* -import solutions.bitbadger.documents.java.extensions.customNonQuery -import solutions.bitbadger.documents.java.extensions.customScalar -import solutions.bitbadger.documents.java.extensions.ensureTable -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase -import solutions.bitbadger.documents.java.jvm.DocumentConfig -import solutions.bitbadger.documents.java.jvm.Results +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.extensions.customNonQuery +import solutions.bitbadger.documents.extensions.customScalar +import solutions.bitbadger.documents.extensions.ensureTable +import solutions.bitbadger.documents.jvm.* +import solutions.bitbadger.documents.support.* /** * A wrapper for a throwaway PostgreSQL database diff --git a/src/jvm/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt similarity index 94% rename from src/jvm/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt index 4fa0fac..4919ce8 100644 --- a/src/jvm/src/test/kotlin/integration/postgresql/RemoveFieldsIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.postgresql +package solutions.bitbadger.documents.jvm.integration.postgresql import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.RemoveFields +import solutions.bitbadger.documents.jvm.integration.common.RemoveFields import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt similarity index 89% rename from src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt index 3b4aeda..5d2d269 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/CountIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.java.integration.common.Count +import solutions.bitbadger.documents.jvm.integration.common.Count import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/CustomIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt similarity index 89% rename from src/jvm/src/test/kotlin/integration/sqlite/CustomIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt index 047371f..5d9b3dc 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/CustomIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Custom +import solutions.bitbadger.documents.jvm.integration.common.Custom import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt similarity index 88% rename from src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt index b43f661..313eab5 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/DefinitionIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.java.integration.common.Definition +import solutions.bitbadger.documents.jvm.integration.common.Definition import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt similarity index 90% rename from src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt index 9947de5..7975e1a 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/DeleteIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.java.integration.common.Delete +import solutions.bitbadger.documents.jvm.integration.common.Delete import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/DocumentIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt similarity index 92% rename from src/jvm/src/test/kotlin/integration/sqlite/DocumentIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt index c5317e7..414dbcd 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/DocumentIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.java.integration.common.Document +import solutions.bitbadger.documents.jvm.integration.common.Document import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt similarity index 90% rename from src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt index da43f75..36b0bd4 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/ExistsIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.java.integration.common.Exists +import solutions.bitbadger.documents.jvm.integration.common.Exists import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt similarity index 96% rename from src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt index b40d364..abe0aaa 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/FindIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.java.integration.common.Find +import solutions.bitbadger.documents.jvm.integration.common.Find import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt similarity index 90% rename from src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt index 7c2a15a..ac1d2c4 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/PatchIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.java.integration.common.Patch +import solutions.bitbadger.documents.jvm.integration.common.Patch import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt similarity index 92% rename from src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt index 4dc23dd..7518ab4 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/RemoveFieldsIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt @@ -1,9 +1,9 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.assertThrows import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.java.integration.common.RemoveFields +import solutions.bitbadger.documents.jvm.integration.common.RemoveFields import kotlin.test.Test /** diff --git a/src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/SQLiteDB.kt similarity index 53% rename from src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt rename to src/jvm/src/test/kotlin/jvm/integration/sqlite/SQLiteDB.kt index cdcbd32..609124e 100644 --- a/src/jvm/src/test/kotlin/integration/sqlite/SQLiteDB.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/SQLiteDB.kt @@ -1,17 +1,10 @@ -package solutions.bitbadger.documents.java.integration.sqlite +package solutions.bitbadger.documents.jvm.integration.sqlite -import AutoId -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType -import solutions.bitbadger.documents.common.* -import solutions.bitbadger.documents.java.* -import solutions.bitbadger.documents.java.extensions.customScalar -import solutions.bitbadger.documents.java.extensions.ensureTable -import solutions.bitbadger.documents.java.integration.TEST_TABLE -import solutions.bitbadger.documents.java.integration.ThrowawayDatabase -import solutions.bitbadger.documents.java.jvm.DocumentConfig -import solutions.bitbadger.documents.java.jvm.Results +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.extensions.customScalar +import solutions.bitbadger.documents.extensions.ensureTable +import solutions.bitbadger.documents.jvm.* +import solutions.bitbadger.documents.support.* import java.io.File /** diff --git a/src/common/src/test/kotlin/query/CountTest.kt b/src/jvm/src/test/kotlin/query/CountTest.kt similarity index 96% rename from src/common/src/test/kotlin/query/CountTest.kt rename to src/jvm/src/test/kotlin/query/CountTest.kt index 48f4ddb..7943110 100644 --- a/src/common/src/test/kotlin/query/CountTest.kt +++ b/src/jvm/src/test/kotlin/query/CountTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -8,7 +8,6 @@ import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.java.query.Count import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/query/DefinitionTest.kt b/src/jvm/src/test/kotlin/query/DefinitionTest.kt similarity index 98% rename from src/common/src/test/kotlin/query/DefinitionTest.kt rename to src/jvm/src/test/kotlin/query/DefinitionTest.kt index 7ab6436..4a37200 100644 --- a/src/common/src/test/kotlin/query/DefinitionTest.kt +++ b/src/jvm/src/test/kotlin/query/DefinitionTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName diff --git a/src/common/src/test/kotlin/query/DeleteTest.kt b/src/jvm/src/test/kotlin/query/DeleteTest.kt similarity index 98% rename from src/common/src/test/kotlin/query/DeleteTest.kt rename to src/jvm/src/test/kotlin/query/DeleteTest.kt index e9f10ee..dfdd80a 100644 --- a/src/common/src/test/kotlin/query/DeleteTest.kt +++ b/src/jvm/src/test/kotlin/query/DeleteTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName diff --git a/src/common/src/test/kotlin/query/DocumentTest.kt b/src/jvm/src/test/kotlin/query/DocumentTest.kt similarity index 97% rename from src/common/src/test/kotlin/query/DocumentTest.kt rename to src/jvm/src/test/kotlin/query/DocumentTest.kt index cf59b0c..6566c7f 100644 --- a/src/common/src/test/kotlin/query/DocumentTest.kt +++ b/src/jvm/src/test/kotlin/query/DocumentTest.kt @@ -1,14 +1,13 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query 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 AutoId +import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException -import solutions.bitbadger.documents.java.query.Document import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/src/common/src/test/kotlin/query/ExistsTest.kt b/src/jvm/src/test/kotlin/query/ExistsTest.kt similarity index 98% rename from src/common/src/test/kotlin/query/ExistsTest.kt rename to src/jvm/src/test/kotlin/query/ExistsTest.kt index 7b852c0..fb22189 100644 --- a/src/common/src/test/kotlin/query/ExistsTest.kt +++ b/src/jvm/src/test/kotlin/query/ExistsTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName diff --git a/src/common/src/test/kotlin/query/FindTest.kt b/src/jvm/src/test/kotlin/query/FindTest.kt similarity index 98% rename from src/common/src/test/kotlin/query/FindTest.kt rename to src/jvm/src/test/kotlin/query/FindTest.kt index d0b6583..5ad3e5a 100644 --- a/src/common/src/test/kotlin/query/FindTest.kt +++ b/src/jvm/src/test/kotlin/query/FindTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName diff --git a/src/common/src/test/kotlin/query/PatchTest.kt b/src/jvm/src/test/kotlin/query/PatchTest.kt similarity index 98% rename from src/common/src/test/kotlin/query/PatchTest.kt rename to src/jvm/src/test/kotlin/query/PatchTest.kt index a7f5675..fd0cfad 100644 --- a/src/common/src/test/kotlin/query/PatchTest.kt +++ b/src/jvm/src/test/kotlin/query/PatchTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName diff --git a/src/common/src/test/kotlin/query/QueryTest.kt b/src/jvm/src/test/kotlin/query/QueryTest.kt similarity index 94% rename from src/common/src/test/kotlin/query/QueryTest.kt rename to src/jvm/src/test/kotlin/query/QueryTest.kt index 912f9c4..6788ac4 100644 --- a/src/common/src/test/kotlin/query/QueryTest.kt +++ b/src/jvm/src/test/kotlin/query/QueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -7,9 +7,6 @@ import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.java.query.byFields -import solutions.bitbadger.documents.java.query.byId -import solutions.bitbadger.documents.java.query.orderBy import kotlin.test.assertEquals /** @@ -29,7 +26,7 @@ class QueryTest { @Test @DisplayName("statementWhere generates correctly") fun statementWhere() = - assertEquals("x WHERE y", solutions.bitbadger.documents.java.query.statementWhere("x", "y"), "Statements not combined correctly") + assertEquals("x WHERE y", statementWhere("x", "y"), "Statements not combined correctly") @Test @DisplayName("byId generates a numeric ID query (PostgreSQL)") diff --git a/src/common/src/test/kotlin/query/RemoveFieldsTest.kt b/src/jvm/src/test/kotlin/query/RemoveFieldsTest.kt similarity index 97% rename from src/common/src/test/kotlin/query/RemoveFieldsTest.kt rename to src/jvm/src/test/kotlin/query/RemoveFieldsTest.kt index 78199f8..34fce2d 100644 --- a/src/common/src/test/kotlin/query/RemoveFieldsTest.kt +++ b/src/jvm/src/test/kotlin/query/RemoveFieldsTest.kt @@ -1,11 +1,10 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query 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.common.* import kotlin.test.assertEquals /** diff --git a/src/common/src/test/kotlin/query/WhereTest.kt b/src/jvm/src/test/kotlin/query/WhereTest.kt similarity index 97% rename from src/common/src/test/kotlin/query/WhereTest.kt rename to src/jvm/src/test/kotlin/query/WhereTest.kt index a5ae71b..cdac60d 100644 --- a/src/common/src/test/kotlin/query/WhereTest.kt +++ b/src/jvm/src/test/kotlin/query/WhereTest.kt @@ -1,12 +1,10 @@ -package solutions.bitbadger.documents.common.query +package solutions.bitbadger.documents.query 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.common.* -import solutions.bitbadger.documents.java.query.Where import kotlin.test.assertEquals /** diff --git a/src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt b/src/jvm/src/test/kotlin/support/JacksonDocumentSerializer.kt similarity index 91% rename from src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt rename to src/jvm/src/test/kotlin/support/JacksonDocumentSerializer.kt index a0b493f..fb211c2 100644 --- a/src/jvm/src/test/kotlin/JacksonDocumentSerializer.kt +++ b/src/jvm/src/test/kotlin/support/JacksonDocumentSerializer.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java +package solutions.bitbadger.documents.support import solutions.bitbadger.documents.DocumentSerializer import com.fasterxml.jackson.databind.ObjectMapper diff --git a/src/jvm/src/test/kotlin/integration/ThrowawayDatabase.kt b/src/jvm/src/test/kotlin/support/ThrowawayDatabase.kt similarity index 89% rename from src/jvm/src/test/kotlin/integration/ThrowawayDatabase.kt rename to src/jvm/src/test/kotlin/support/ThrowawayDatabase.kt index 1e46251..9c63c30 100644 --- a/src/jvm/src/test/kotlin/integration/ThrowawayDatabase.kt +++ b/src/jvm/src/test/kotlin/support/ThrowawayDatabase.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.integration +package solutions.bitbadger.documents.support import java.sql.Connection diff --git a/src/jvm/src/test/kotlin/integration/Types.kt b/src/jvm/src/test/kotlin/support/Types.kt similarity index 93% rename from src/jvm/src/test/kotlin/integration/Types.kt rename to src/jvm/src/test/kotlin/support/Types.kt index 9cf9462..97385e2 100644 --- a/src/jvm/src/test/kotlin/integration/Types.kt +++ b/src/jvm/src/test/kotlin/support/Types.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.java.integration +package solutions.bitbadger.documents.support import kotlinx.serialization.Serializable -import solutions.bitbadger.documents.java.extensions.insert +import solutions.bitbadger.documents.extensions.insert /** The test table name to use for integration tests */ const val TEST_TABLE = "test_table" diff --git a/src/kotlin/src/main/kotlin/Configuration.kt b/src/kotlin/src/main/kotlin/Configuration.kt deleted file mode 100644 index f8becb9..0000000 --- a/src/kotlin/src/main/kotlin/Configuration.kt +++ /dev/null @@ -1,24 +0,0 @@ -package solutions.bitbadger.documents.kotlin - -import kotlinx.serialization.json.Json - -object Configuration { - - /** - * JSON serializer; replace to configure with non-default options - * - * The default sets `encodeDefaults` to `true` and `explicitNulls` to `false`; see - * https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md for all configuration options - */ - @JvmField - var json = Json { - encodeDefaults = true - explicitNulls = false - coerceInputValues = true - } - - /** The JSON serializer to use for documents */ - @JvmStatic - var serializer: DocumentSerializer = DocumentSerializerKotlin() - -} diff --git a/src/kotlin/src/main/kotlin/Count.kt b/src/kotlin/src/main/kotlin/Count.kt index 02836a3..4b2f3f5 100644 --- a/src/kotlin/src/main/kotlin/Count.kt +++ b/src/kotlin/src/main/kotlin/Count.kt @@ -5,6 +5,7 @@ import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch import solutions.bitbadger.documents.Parameter import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.query.Count import java.sql.Connection diff --git a/src/kotlin/src/main/kotlin/Custom.kt b/src/kotlin/src/main/kotlin/Custom.kt index d84df88..265abd6 100644 --- a/src/kotlin/src/main/kotlin/Custom.kt +++ b/src/kotlin/src/main/kotlin/Custom.kt @@ -1,7 +1,9 @@ package solutions.bitbadger.documents.kotlin +import solutions.bitbadger.documents.* import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.jvm.Parameters +import solutions.bitbadger.documents.jvm.Custom as JvmCustom import java.sql.Connection import java.sql.ResultSet @@ -15,17 +17,16 @@ object Custom { * * @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 */ - inline fun list( + inline fun list( query: String, parameters: Collection> = listOf(), conn: Connection, - noinline mapFunc: (ResultSet, Class) -> TDoc - ) = Custom.list(query, parameters, TDoc::class.java, conn, mapFunc) + mapFunc: (ResultSet) -> TDoc + ) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, mapFunc) } /** * Execute a query that returns a list of results (creates connection) @@ -35,10 +36,10 @@ object Custom { * @param mapFunc The mapping function between the document and the domain item * @return A list of results for the given query */ - inline fun list( + inline fun list( query: String, parameters: Collection> = listOf(), - noinline mapFunc: (ResultSet, Class) -> TDoc + mapFunc: (ResultSet) -> TDoc ) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) } /** @@ -50,12 +51,12 @@ object Custom { * @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( + inline fun single( query: String, parameters: Collection> = listOf(), conn: Connection, - noinline mapFunc: (ResultSet, Class) -> TDoc - ) = Custom.single(query, parameters, TDoc::class.java, conn, mapFunc) + mapFunc: (ResultSet) -> TDoc + ) = list("$query LIMIT 1", parameters, conn, mapFunc).singleOrNull() /** * Execute a query that returns one or no results @@ -65,10 +66,10 @@ object Custom { * @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( + inline fun single( query: String, parameters: Collection> = listOf(), - noinline mapFunc: (ResultSet, Class) -> TDoc + noinline mapFunc: (ResultSet) -> TDoc ) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) } /** @@ -79,7 +80,7 @@ object Custom { * @param parameters Parameters to use for the query */ fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) = - Custom.nonQuery(query, parameters, conn) + JvmCustom.nonQuery(query, parameters, conn) /** * Execute a query that returns no results @@ -103,8 +104,13 @@ object Custom { query: String, parameters: Collection> = listOf(), conn: Connection, - noinline mapFunc: (ResultSet, Class) -> T - ) = Custom.scalar(query, parameters, T::class.java, conn, mapFunc) + 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 @@ -115,6 +121,6 @@ object Custom { * @return The scalar value from the query */ inline fun scalar( - query: String, parameters: Collection> = listOf(), noinline mapFunc: (ResultSet, Class) -> T + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> T ) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) } } diff --git a/src/kotlin/src/main/kotlin/DocumentConfig.kt b/src/kotlin/src/main/kotlin/DocumentConfig.kt new file mode 100644 index 0000000..cb42341 --- /dev/null +++ b/src/kotlin/src/main/kotlin/DocumentConfig.kt @@ -0,0 +1,33 @@ +package solutions.bitbadger.documents.kotlin + +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/kotlin/src/test/kotlin/DocumentConfigTest.kt b/src/kotlin/src/test/kotlin/DocumentConfigTest.kt new file mode 100644 index 0000000..9b90f19 --- /dev/null +++ b/src/kotlin/src/test/kotlin/DocumentConfigTest.kt @@ -0,0 +1,21 @@ +package solutions.bitbadger.documents.kotlin + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Unit tests for the `Configuration` object + */ +@DisplayName("Kotlin | 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/main/kotlin/Custom.kt b/src/main/kotlin/Custom.kt deleted file mode 100644 index fd52866..0000000 --- a/src/main/kotlin/Custom.kt +++ /dev/null @@ -1,118 +0,0 @@ -package solutions.bitbadger.documents - -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 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 - */ - inline fun list( - query: String, - parameters: Collection> = listOf(), - conn: Connection, - noinline mapFunc: (ResultSet, Class) -> TDoc - ) = Custom.list(query, parameters, TDoc::class.java, 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 - */ - inline fun list( - query: String, - parameters: Collection> = listOf(), - noinline mapFunc: (ResultSet, Class) -> TDoc - ) = Configuration.dbConn().use { list(query, parameters, 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 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, - noinline mapFunc: (ResultSet, Class) -> TDoc - ) = Custom.single(query, parameters, TDoc::class.java, conn, 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 single( - query: String, - parameters: Collection> = listOf(), - noinline mapFunc: (ResultSet, Class) -> TDoc - ) = Configuration.dbConn().use { single(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 - */ - fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) = - Custom.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, - noinline mapFunc: (ResultSet, Class) -> T - ) = Custom.scalar(query, parameters, T::class.java, conn, mapFunc) - - /** - * 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(), noinline mapFunc: (ResultSet, Class) -> T - ) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) } -} diff --git a/src/pom.xml b/src/pom.xml index 1c520aa..96aa6fc 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -45,7 +45,6 @@ - common jvm diff --git a/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java b/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java deleted file mode 100644 index fe67b44..0000000 --- a/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package solutions.bitbadger.documents.java; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import solutions.bitbadger.documents.common.Dialect; -import solutions.bitbadger.documents.common.DocumentException; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for the `Configuration` object - */ -@DisplayName("Java | Configuration") -final public class ConfigurationTest { - - @Test - @DisplayName("Default JSON options are as expected") - public void defaultJsonOptions() { - assertTrue(Configuration.json.getConfiguration().getEncodeDefaults(), "Encode Defaults should have been set"); - assertFalse(Configuration.json.getConfiguration().getExplicitNulls(), - "Explicit Nulls should not have been set"); - assertTrue(Configuration.json.getConfiguration().getCoerceInputValues(), - "Coerce Input Values should have been set"); - } - - @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() { - try { - assertThrows(DocumentException.class, Configuration::dialect); - Configuration.setConnectionString("jdbc:postgresql:db"); - assertEquals(Dialect.POSTGRESQL, Configuration.dialect()); - } catch (DocumentException ex) { - fail(ex); - } finally { - Configuration.setConnectionString(null); - } - } -} diff --git a/src/test/kotlin/ConfigurationTest.kt b/src/test/kotlin/ConfigurationTest.kt deleted file mode 100644 index d3689f7..0000000 --- a/src/test/kotlin/ConfigurationTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package solutions.bitbadger.documents - -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -/** - * Unit tests for the `Configuration` object - */ -@DisplayName("Kotlin | Configuration") -class ConfigurationTest { - - @Test - @DisplayName("Default JSON options are as expected") - fun defaultJsonOptions() { - assertTrue(Configuration.json.configuration.encodeDefaults, "Encode Defaults should have been set") - assertFalse(Configuration.json.configuration.explicitNulls, "Explicit Nulls should not have been set") - assertTrue(Configuration.json.configuration.coerceInputValues, "Coerce Input Values should have been set") - } -} -- 2.47.2 From 5497238fb19e9e162515410ced6e158d34fc178a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 14 Mar 2025 23:28:30 -0400 Subject: [PATCH 45/88] Kotlin project builds --- src/jvm/pom.xml | 4 +- src/kotlin/pom.xml | 23 +--- src/kotlin/src/main/kotlin/Count.kt | 17 +-- src/kotlin/src/main/kotlin/Custom.kt | 1 - src/kotlin/src/main/kotlin/Definition.kt | 19 +-- src/kotlin/src/main/kotlin/Delete.kt | 29 ++--- src/kotlin/src/main/kotlin/Document.kt | 114 ++++++++++++++++++ src/kotlin/src/main/kotlin/Exists.kt | 43 ++----- src/kotlin/src/main/kotlin/Find.kt | 76 +++++++----- src/kotlin/src/main/kotlin/Parameters.kt | 81 ++----------- src/kotlin/src/main/kotlin/Patch.kt | 9 +- src/kotlin/src/main/kotlin/RemoveFields.kt | 57 +++------ src/kotlin/src/main/kotlin/Results.kt | 17 ++- .../Connection.kt} | 35 +++--- src/pom.xml | 1 + 15 files changed, 260 insertions(+), 266 deletions(-) create mode 100644 src/kotlin/src/main/kotlin/Document.kt rename src/kotlin/src/main/kotlin/{ConnectionExtensions.kt => extensions/Connection.kt} (94%) diff --git a/src/jvm/pom.xml b/src/jvm/pom.xml index 2da78a7..33c4d65 100644 --- a/src/jvm/pom.xml +++ b/src/jvm/pom.xml @@ -5,7 +5,7 @@ 4.0.0 solutions.bitbadger.documents - java + jvm 4.0.0-alpha1-SNAPSHOT jar @@ -16,7 +16,7 @@ ${project.groupId}:${project.artifactId} - Expose a document store interface for PostgreSQL and SQLite (Java Library) + Expose a document store interface for PostgreSQL and SQLite (Standard JVM Library) https://bitbadger.solutions/open-source/relational-documents/jvm/ diff --git a/src/kotlin/pom.xml b/src/kotlin/pom.xml index 88bafc0..9889629 100644 --- a/src/kotlin/pom.xml +++ b/src/kotlin/pom.xml @@ -28,16 +28,10 @@ solutions.bitbadger.documents - common + jvm + 4.0.0-alpha1-SNAPSHOT system - ${project.basedir}/../common/target/common-4.0.0-alpha1-SNAPSHOT.jar - jar - - - solutions.bitbadger.documents - java - system - ${project.basedir}/../java/target/java-4.0.0-alpha1-SNAPSHOT.jar + ${project.basedir}/../jvm/target/jvm-4.0.0-alpha1-SNAPSHOT.jar jar @@ -57,11 +51,6 @@ compile - - - ${project.basedir}/src/main/kotlin - - test-compile @@ -69,12 +58,6 @@ test-compile - - - ${project.basedir}/src/test/java - ${project.basedir}/src/test/kotlin - - diff --git a/src/kotlin/src/main/kotlin/Count.kt b/src/kotlin/src/main/kotlin/Count.kt index 4b2f3f5..d8fbf7b 100644 --- a/src/kotlin/src/main/kotlin/Count.kt +++ b/src/kotlin/src/main/kotlin/Count.kt @@ -1,10 +1,7 @@ package solutions.bitbadger.documents.kotlin -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlin.extensions.* import solutions.bitbadger.documents.query.Count import java.sql.Connection @@ -21,7 +18,6 @@ object Count { * @param conn The connection over which documents should be counted * @return A count of the documents in the table */ - @JvmStatic fun all(tableName: String, conn: Connection) = conn.customScalar(Count.all(tableName), mapFunc = Results::toCount) @@ -31,7 +27,6 @@ object Count { * @param tableName The name of the table in which documents should be counted * @return A count of the documents in the table */ - @JvmStatic fun all(tableName: String) = Configuration.dbConn().use { all(tableName, it) } @@ -44,8 +39,6 @@ object Count { * @param conn The connection on which the deletion should be executed * @return A count of the matching documents in the table */ - @JvmStatic - @JvmOverloads fun byFields( tableName: String, fields: Collection>, @@ -68,8 +61,6 @@ object Count { * @param howMatched How the fields should be matched * @return A count of the matching documents in the table */ - @JvmStatic - @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } @@ -82,7 +73,6 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ - @JvmStatic inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customScalar(Count.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Results::toCount) @@ -94,7 +84,6 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ - @JvmStatic inline fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } @@ -107,7 +96,6 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ - @JvmStatic fun byJsonPath(tableName: String, path: String, conn: Connection) = conn.customScalar( Count.byJsonPath(tableName), @@ -123,7 +111,6 @@ object Count { * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ - @JvmStatic fun byJsonPath(tableName: String, path: String) = Configuration.dbConn().use { byJsonPath(tableName, path, it) } } diff --git a/src/kotlin/src/main/kotlin/Custom.kt b/src/kotlin/src/main/kotlin/Custom.kt index 265abd6..4d77ee5 100644 --- a/src/kotlin/src/main/kotlin/Custom.kt +++ b/src/kotlin/src/main/kotlin/Custom.kt @@ -2,7 +2,6 @@ package solutions.bitbadger.documents.kotlin import solutions.bitbadger.documents.* import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.jvm.Parameters import solutions.bitbadger.documents.jvm.Custom as JvmCustom import java.sql.Connection import java.sql.ResultSet diff --git a/src/kotlin/src/main/kotlin/Definition.kt b/src/kotlin/src/main/kotlin/Definition.kt index 8c2b2d9..23fa87e 100644 --- a/src/kotlin/src/main/kotlin/Definition.kt +++ b/src/kotlin/src/main/kotlin/Definition.kt @@ -1,4 +1,8 @@ +package solutions.bitbadger.documents.kotlin + +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.jvm.Definition as JvmDefinition import java.sql.Connection /** @@ -13,10 +17,7 @@ object Definition { * @param conn The connection on which the query should be executed */ fun ensureTable(tableName: String, conn: Connection) = - Configuration.dialect("ensure $tableName exists").let { - conn.customNonQuery(Definition.ensureTable(tableName, it)) - conn.customNonQuery(Definition.ensureKey(tableName, it)) - } + JvmDefinition.ensureTable(tableName, conn) /** * Create a document table if necessary @@ -24,7 +25,7 @@ object Definition { * @param tableName The table whose existence should be ensured (may include schema) */ fun ensureTable(tableName: String) = - Configuration.dbConn().use { ensureTable(tableName, it) } + JvmDefinition.ensureTable(tableName) /** * Create an index on field(s) within documents in the specified table if necessary @@ -35,7 +36,7 @@ object Definition { * @param conn The connection on which the query should be executed */ fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection, conn: Connection) = - conn.customNonQuery(Definition.ensureIndexOn(tableName, indexName, fields)) + JvmDefinition.ensureFieldIndex(tableName, indexName, fields, conn) /** * Create an index on field(s) within documents in the specified table if necessary @@ -45,7 +46,7 @@ object Definition { * @param fields One or more fields to be indexed< */ fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection) = - Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) } + JvmDefinition.ensureFieldIndex(tableName, indexName, fields) /** * Create a document index on a table (PostgreSQL only) @@ -56,7 +57,7 @@ object Definition { * @throws DocumentException If called on a SQLite connection */ fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) = - conn.customNonQuery(Definition.ensureDocumentIndexOn(tableName, indexType)) + JvmDefinition.ensureDocumentIndex(tableName, indexType, conn) /** * Create a document index on a table (PostgreSQL only) @@ -66,5 +67,5 @@ object Definition { * @throws DocumentException If called on a SQLite connection */ fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex) = - Configuration.dbConn().use { ensureDocumentIndex(tableName, indexType, it) } + JvmDefinition.ensureDocumentIndex(tableName, indexType) } diff --git a/src/kotlin/src/main/kotlin/Delete.kt b/src/kotlin/src/main/kotlin/Delete.kt index e092b05..e670d48 100644 --- a/src/kotlin/src/main/kotlin/Delete.kt +++ b/src/kotlin/src/main/kotlin/Delete.kt @@ -1,7 +1,9 @@ -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType +package solutions.bitbadger.documents.kotlin + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.jvm.Delete as JvmDelete +import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.query.Delete import java.sql.Connection /** @@ -17,10 +19,7 @@ object Delete { * @param conn The connection on which the deletion should be executed */ fun byId(tableName: String, docId: TKey, conn: Connection) = - conn.customNonQuery( - Delete.byId(tableName, docId), - Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))) - ) + JvmDelete.byId(tableName, docId, conn) /** * Delete a document by its ID @@ -29,7 +28,7 @@ object Delete { * @param docId The ID of the document to be deleted */ fun byId(tableName: String, docId: TKey) = - Configuration.dbConn().use { byId(tableName, docId, it) } + JvmDelete.byId(tableName, docId) /** * Delete documents using a field comparison @@ -39,10 +38,8 @@ object Delete { * @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) { - val named = Parameters.nameFields(fields) - conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named)) - } + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) = + JvmDelete.byFields(tableName, fields, howMatched, conn) /** * Delete documents using a field comparison @@ -52,7 +49,7 @@ object Delete { * @param howMatched How the fields should be matched */ fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = - Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } + JvmDelete.byFields(tableName, fields, howMatched) /** * Delete documents using a JSON containment query (PostgreSQL only) @@ -84,7 +81,7 @@ object Delete { * @throws DocumentException If called on a SQLite connection */ fun byJsonPath(tableName: String, path: String, conn: Connection) = - conn.customNonQuery(Delete.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) + JvmDelete.byJsonPath(tableName, path, conn) /** * Delete documents using a JSON Path match query (PostgreSQL only) @@ -94,5 +91,5 @@ object Delete { * @throws DocumentException If called on a SQLite connection */ fun byJsonPath(tableName: String, path: String) = - Configuration.dbConn().use { byJsonPath(tableName, path, it) } + JvmDelete.byJsonPath(tableName, path) } diff --git a/src/kotlin/src/main/kotlin/Document.kt b/src/kotlin/src/main/kotlin/Document.kt new file mode 100644 index 0000000..2c2547e --- /dev/null +++ b/src/kotlin/src/main/kotlin/Document.kt @@ -0,0 +1,114 @@ +package solutions.bitbadger.documents.kotlin + +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.extensions.customNonQuery +import solutions.bitbadger.documents.query.Document +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) { + Document.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" + } + + Document.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(Document.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(Document.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/kotlin/src/main/kotlin/Exists.kt b/src/kotlin/src/main/kotlin/Exists.kt index 5146ba7..e253bbb 100644 --- a/src/kotlin/src/main/kotlin/Exists.kt +++ b/src/kotlin/src/main/kotlin/Exists.kt @@ -1,7 +1,9 @@ -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType +package solutions.bitbadger.documents.kotlin + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.jvm.Exists as JvmExists +import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.query.Exists import java.sql.Connection /** @@ -18,11 +20,7 @@ object Exists { * @return True if the document exists, false if not */ fun byId(tableName: String, docId: TKey, conn: Connection) = - conn.customScalar( - Exists.byId(tableName, docId), - Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), - Results::toExists - ) + JvmExists.byId(tableName, docId, conn) /** * Determine a document's existence by its ID @@ -32,7 +30,7 @@ object Exists { * @return True if the document exists, false if not */ fun byId(tableName: String, docId: TKey) = - Configuration.dbConn().use { byId(tableName, docId, it) } + JvmExists.byId(tableName, docId) /** * Determine document existence using a field comparison @@ -43,19 +41,8 @@ object Exists { * @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 - ): Boolean { - val named = Parameters.nameFields(fields) - return conn.customScalar( - Exists.byFields(tableName, named, howMatched), - Parameters.addFields(named), - Results::toExists - ) - } + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) = + JvmExists.byFields(tableName, fields, howMatched, conn) /** * Determine document existence using a field comparison @@ -66,7 +53,7 @@ object Exists { * @return True if any matching documents exist, false if not */ fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = - Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) } + JvmExists.byFields(tableName, fields, howMatched) /** * Determine document existence using a JSON containment query (PostgreSQL only) @@ -105,11 +92,7 @@ object Exists { * @throws DocumentException If called on a SQLite connection */ fun byJsonPath(tableName: String, path: String, conn: Connection) = - conn.customScalar( - Exists.byJsonPath(tableName), - listOf(Parameter(":path", ParameterType.STRING, path)), - Results::toExists - ) + JvmExists.byJsonPath(tableName, path, conn) /** * Determine document existence using a JSON Path match query (PostgreSQL only) @@ -120,5 +103,5 @@ object Exists { * @throws DocumentException If called on a SQLite connection */ fun byJsonPath(tableName: String, path: String) = - Configuration.dbConn().use { byJsonPath(tableName, path, it) } + JvmExists.byJsonPath(tableName, path) } diff --git a/src/kotlin/src/main/kotlin/Find.kt b/src/kotlin/src/main/kotlin/Find.kt index d698790..a9ecf18 100644 --- a/src/kotlin/src/main/kotlin/Find.kt +++ b/src/kotlin/src/main/kotlin/Find.kt @@ -1,7 +1,9 @@ -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType +package solutions.bitbadger.documents.kotlin + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.query.Find +import solutions.bitbadger.documents.query.orderBy import java.sql.Connection /** @@ -17,7 +19,7 @@ object Find { * @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) = + inline fun all(tableName: String, orderBy: Collection>? = null, conn: Connection) = conn.customList(Find.all(tableName) + (orderBy?.let(::orderBy) ?: ""), mapFunc = Results::fromData) /** @@ -27,7 +29,7 @@ object Find { * @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) = + inline fun all(tableName: String, orderBy: Collection>? = null) = Configuration.dbConn().use { all(tableName, orderBy, it) } /** @@ -37,7 +39,7 @@ object Find { * @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) = + inline fun all(tableName: String, conn: Connection) = all(tableName, null, conn) /** @@ -48,7 +50,7 @@ object Find { * @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) = + inline fun byId(tableName: String, docId: TKey, conn: Connection) = conn.customSingle( Find.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), @@ -62,7 +64,7 @@ object Find { * @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) = + inline fun byId(tableName: String, docId: TKey) = Configuration.dbConn().use { byId(tableName, docId, it) } /** @@ -75,7 +77,7 @@ object Find { * @param conn The connection over which documents should be retrieved * @return A list of documents matching the field comparison */ - inline fun byFields( + inline fun byFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null, @@ -99,7 +101,7 @@ object Find { * @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( + inline fun byFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null, @@ -116,7 +118,7 @@ object Find { * @param conn The connection over which documents should be retrieved * @return A list of documents matching the field comparison */ - inline fun byFields( + inline fun byFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null, @@ -132,7 +134,7 @@ object Find { * @param howMatched How the fields should be matched * @return A list of documents matching the field comparison */ - inline fun byFields( + inline fun byFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null @@ -149,7 +151,7 @@ object Find { * @return A list of documents matching the JSON containment query * @throws DocumentException If called on a SQLite connection */ - inline fun byContains( + inline fun byContains( tableName: String, criteria: TContains, orderBy: Collection>? = null, @@ -170,7 +172,7 @@ object Find { * @return A list of documents matching the JSON containment query * @throws DocumentException If called on a SQLite connection */ - inline fun byContains( + inline fun byContains( tableName: String, criteria: TContains, orderBy: Collection>? = null @@ -186,7 +188,11 @@ object Find { * @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) = + inline fun byContains( + tableName: String, + criteria: TContains, + conn: Connection + ) = byContains(tableName, criteria, null, conn) /** @@ -197,7 +203,7 @@ object Find { * @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) = + inline fun byContains(tableName: String, criteria: TContains) = Configuration.dbConn().use { byContains(tableName, criteria, it) } /** @@ -210,7 +216,7 @@ object Find { * @return A list of documents matching the JSON Path match query * @throws DocumentException If called on a SQLite connection */ - inline fun byJsonPath( + inline fun byJsonPath( tableName: String, path: String, orderBy: Collection>? = null, @@ -231,7 +237,7 @@ object Find { * @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) = + inline fun byJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = Configuration.dbConn().use { byJsonPath(tableName, path, orderBy, it) } /** @@ -243,7 +249,7 @@ object Find { * @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) = + inline fun byJsonPath(tableName: String, path: String, conn: Connection) = byJsonPath(tableName, path, null, conn) /** @@ -256,7 +262,7 @@ object Find { * @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( + inline fun firstByFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null, @@ -280,7 +286,7 @@ object Find { * @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( + inline fun firstByFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null, @@ -297,7 +303,7 @@ object Find { * @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( + inline fun firstByFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null, @@ -315,7 +321,7 @@ object Find { * @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( + inline fun firstByContains( tableName: String, criteria: TContains, orderBy: Collection>? = null, @@ -336,7 +342,11 @@ object Find { * @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) = + inline fun firstByContains( + tableName: String, + criteria: TContains, + conn: Connection + ) = firstByContains(tableName, criteria, null, conn) /** @@ -348,7 +358,11 @@ object Find { * @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) = + inline fun firstByContains( + tableName: String, + criteria: TContains, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { firstByContains(tableName, criteria, orderBy, it) } /** @@ -361,7 +375,7 @@ object Find { * @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( + inline fun firstByJsonPath( tableName: String, path: String, orderBy: Collection>? = null, @@ -382,7 +396,7 @@ object Find { * @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) = + inline fun firstByJsonPath(tableName: String, path: String, conn: Connection) = firstByJsonPath(tableName, path, null, conn) /** @@ -394,6 +408,10 @@ object Find { * @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) = + inline fun firstByJsonPath( + tableName: String, + path: String, + orderBy: Collection>? = null + ) = Configuration.dbConn().use { firstByJsonPath(tableName, path, orderBy, it) } } diff --git a/src/kotlin/src/main/kotlin/Parameters.kt b/src/kotlin/src/main/kotlin/Parameters.kt index 7dae9e9..74bf62c 100644 --- a/src/kotlin/src/main/kotlin/Parameters.kt +++ b/src/kotlin/src/main/kotlin/Parameters.kt @@ -1,10 +1,8 @@ +package solutions.bitbadger.documents.kotlin + import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.ParameterName -import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.jvm.Parameters as JvmParameters 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 @@ -19,17 +17,8 @@ object Parameters { * @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 - } - } - } + fun nameFields(fields: Collection>): Collection> = + JvmParameters.nameFields(fields) /** * Create a parameter by encoding a JSON object @@ -38,9 +27,8 @@ object Parameters { * @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, Configuration.serializer.serialize(value)) + inline fun json(name: String, value: T) = + Parameter(name, ParameterType.JSON, DocumentConfig.serialize(value)) /** * Add field parameters to the given set of parameters @@ -49,9 +37,8 @@ object Parameters { * @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) } + JvmParameters.addFields(fields, existing) /** * Replace the parameter names in the query with question marks @@ -60,9 +47,8 @@ object Parameters { * @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, "?") } + JvmParameters.replaceNamesInQuery(query, parameters) /** * Apply the given parameters to the given query, returning a prepared statement @@ -73,38 +59,8 @@ object Parameters { * @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) - } - } + fun apply(conn: Connection, query: String, parameters: Collection>) = + JvmParameters.apply(conn, query, parameters) /** * Create parameters for field names to be removed from a document @@ -114,17 +70,6 @@ object Parameters { * @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() - } + fun fieldNames(names: Collection, parameterName: String = ":name") = + JvmParameters.fieldNames(names, parameterName) } diff --git a/src/kotlin/src/main/kotlin/Patch.kt b/src/kotlin/src/main/kotlin/Patch.kt index ea8ebc3..097eb2d 100644 --- a/src/kotlin/src/main/kotlin/Patch.kt +++ b/src/kotlin/src/main/kotlin/Patch.kt @@ -1,7 +1,8 @@ -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType +package solutions.bitbadger.documents.kotlin + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.query.Patch import java.sql.Connection /** diff --git a/src/kotlin/src/main/kotlin/RemoveFields.kt b/src/kotlin/src/main/kotlin/RemoveFields.kt index 9d40864..f1c7675 100644 --- a/src/kotlin/src/main/kotlin/RemoveFields.kt +++ b/src/kotlin/src/main/kotlin/RemoveFields.kt @@ -1,5 +1,9 @@ +package solutions.bitbadger.documents.kotlin + import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.common.* +import solutions.bitbadger.documents.jvm.RemoveFields as JvmRemoveFields +import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.query.RemoveFields import java.sql.Connection /** @@ -7,20 +11,6 @@ import java.sql.Connection */ 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 * @@ -29,16 +19,8 @@ object RemoveFields { * @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) { - val nameParams = Parameters.fieldNames(toRemove) - conn.customNonQuery( - RemoveFields.byId(tableName, nameParams, docId), - Parameters.addFields( - listOf(Field.equal(Configuration.idField, docId, ":id")), - translatePath(nameParams) - ) - ) - } + fun byId(tableName: String, docId: TKey, toRemove: Collection, conn: Connection) = + JvmRemoveFields.byId(tableName, docId, toRemove, conn) /** * Remove fields from a document by its ID @@ -48,7 +30,7 @@ object RemoveFields { * @param toRemove The names of the fields to be removed */ fun byId(tableName: String, docId: TKey, toRemove: Collection) = - Configuration.dbConn().use { byId(tableName, docId, toRemove, it) } + JvmRemoveFields.byId(tableName, docId, toRemove) /** * Remove fields from documents using a field comparison @@ -65,14 +47,8 @@ object RemoveFields { toRemove: Collection, howMatched: FieldMatch? = null, conn: Connection - ) { - val named = Parameters.nameFields(fields) - val nameParams = Parameters.fieldNames(toRemove) - conn.customNonQuery( - RemoveFields.byFields(tableName, nameParams, named, howMatched), - Parameters.addFields(named, translatePath(nameParams)) - ) - } + ) = + JvmRemoveFields.byFields(tableName, fields, toRemove, howMatched, conn) /** * Remove fields from documents using a field comparison @@ -88,7 +64,7 @@ object RemoveFields { toRemove: Collection, howMatched: FieldMatch? = null ) = - Configuration.dbConn().use { byFields(tableName, fields, toRemove, howMatched, it) } + JvmRemoveFields.byFields(tableName, fields, toRemove, howMatched) /** * Remove fields from documents using a JSON containment query (PostgreSQL only) @@ -132,13 +108,8 @@ object RemoveFields { * @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) { - val nameParams = Parameters.fieldNames(toRemove) - conn.customNonQuery( - RemoveFields.byJsonPath(tableName, nameParams), - listOf(Parameter(":path", ParameterType.STRING, path), *nameParams.toTypedArray()) - ) - } + 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) @@ -149,5 +120,5 @@ object RemoveFields { * @throws DocumentException If called on a SQLite connection */ fun byJsonPath(tableName: String, path: String, toRemove: Collection) = - Configuration.dbConn().use { byJsonPath(tableName, path, toRemove, it) } + JvmRemoveFields.byJsonPath(tableName, path, toRemove) } diff --git a/src/kotlin/src/main/kotlin/Results.kt b/src/kotlin/src/main/kotlin/Results.kt index 131dd66..791fb60 100644 --- a/src/kotlin/src/main/kotlin/Results.kt +++ b/src/kotlin/src/main/kotlin/Results.kt @@ -16,21 +16,18 @@ 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 - * @return The constructed domain item + * @return A function to create the constructed domain item */ - inline fun fromDocument(field: String): (ResultSet, Class) -> TDoc = - { rs, _ -> Results.fromDocument(field, rs, TDoc::class.java) } + inline fun fromDocument(field: String): (ResultSet) -> TDoc = + { rs -> DocumentConfig.deserialize(rs.getString(field)) } /** * 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 */ - inline fun fromData(rs: ResultSet, clazz: Class = TDoc::class.java) = - Results.fromDocument("data", rs, TDoc::class.java) + 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 @@ -59,7 +56,7 @@ object Results { * @param rs A `ResultSet` set to the row with the count to retrieve * @return The count from the row */ - fun toCount(rs: ResultSet, clazz: Class = Long::class.java) = + fun toCount(rs: ResultSet) = when (Configuration.dialect()) { Dialect.POSTGRESQL -> rs.getInt("it").toLong() Dialect.SQLITE -> rs.getLong("it") @@ -71,7 +68,7 @@ object Results { * @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, clazz: Class = Boolean::class.java) = + fun toExists(rs: ResultSet) = when (Configuration.dialect()) { Dialect.POSTGRESQL -> rs.getBoolean("it") Dialect.SQLITE -> toCount(rs) > 0L diff --git a/src/kotlin/src/main/kotlin/ConnectionExtensions.kt b/src/kotlin/src/main/kotlin/extensions/Connection.kt similarity index 94% rename from src/kotlin/src/main/kotlin/ConnectionExtensions.kt rename to src/kotlin/src/main/kotlin/extensions/Connection.kt index 599cb22..d02e85b 100644 --- a/src/kotlin/src/main/kotlin/ConnectionExtensions.kt +++ b/src/kotlin/src/main/kotlin/extensions/Connection.kt @@ -1,10 +1,7 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlin.extensions -import solutions.bitbadger.documents.DocumentIndex -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.java.jvm.Document +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.kotlin.* import java.sql.Connection import java.sql.ResultSet @@ -18,8 +15,8 @@ import java.sql.ResultSet * @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(), noinline mapFunc: (ResultSet, Class) -> TDoc +inline fun Connection.customList( + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Custom.list(query, parameters, this, mapFunc) /** @@ -30,8 +27,8 @@ inline fun Connection.customList( * @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(), noinline mapFunc: (ResultSet, Class) -> TDoc +inline fun Connection.customSingle( + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Custom.single(query, parameters, this, mapFunc) /** @@ -54,7 +51,7 @@ fun Connection.customNonQuery(query: String, parameters: Collection inline fun Connection.customScalar( query: String, parameters: Collection> = listOf(), - noinline mapFunc: (ResultSet, Class) -> T + mapFunc: (ResultSet) -> T ) = Custom.scalar(query, parameters, this, mapFunc) // ~~~ DEFINITION QUERIES ~~~ @@ -215,7 +212,7 @@ fun Connection.existsByJsonPath(tableName: String, path: String) = * @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) = +inline fun Connection.findAll(tableName: String, orderBy: Collection>? = null) = Find.all(tableName, orderBy, this) /** @@ -225,7 +222,7 @@ inline fun Connection.findAll(tableName: String, orderBy: Collect * @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) = +inline fun Connection.findById(tableName: String, docId: TKey) = Find.byId(tableName, docId, this) /** @@ -237,7 +234,7 @@ inline fun Connection.findById(tableName: String, docId: TK * @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( +inline fun Connection.findByFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null, @@ -254,7 +251,7 @@ inline fun Connection.findByFields( * @return A list of documents matching the JSON containment query * @throws DocumentException If called on a SQLite connection */ -inline fun Connection.findByContains( +inline fun Connection.findByContains( tableName: String, criteria: TContains, orderBy: Collection>? = null @@ -270,7 +267,7 @@ inline fun Connection.findByContains( * @return A list of documents matching the JSON Path match query * @throws DocumentException If called on a SQLite connection */ -inline fun Connection.findByJsonPath( +inline fun Connection.findByJsonPath( tableName: String, path: String, orderBy: Collection>? = null @@ -286,7 +283,7 @@ inline fun Connection.findByJsonPath( * @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( +inline fun Connection.findFirstByFields( tableName: String, fields: Collection>, howMatched: FieldMatch? = null, @@ -303,7 +300,7 @@ inline fun Connection.findFirstByFields( * @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( +inline fun Connection.findFirstByContains( tableName: String, criteria: TContains, orderBy: Collection>? = null @@ -319,7 +316,7 @@ inline fun Connection.findFirstByContains( * @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( +inline fun Connection.findFirstByJsonPath( tableName: String, path: String, orderBy: Collection>? = null diff --git a/src/pom.xml b/src/pom.xml index 96aa6fc..27748e6 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -46,6 +46,7 @@ jvm + kotlin -- 2.47.2 From c4ef2b1d9a798b3ec79960babbd6ef4098256d81 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 15 Mar 2025 11:53:26 -0400 Subject: [PATCH 46/88] Add Query suffix to query classes --- .idea/compiler.xml | 4 +- .idea/encodings.xml | 6 + .idea/kotlinc.xml | 2 +- .idea/modules.xml | 8 -- pom.xml | 123 +----------------- .../src/main/kotlin/extensions/Connection.kt | 13 +- src/jvm/src/main/kotlin/jvm/Count.kt | 10 +- src/jvm/src/main/kotlin/jvm/Definition.kt | 10 +- src/jvm/src/main/kotlin/jvm/Delete.kt | 10 +- src/jvm/src/main/kotlin/jvm/Document.kt | 38 +----- src/jvm/src/main/kotlin/jvm/Exists.kt | 10 +- src/jvm/src/main/kotlin/jvm/Find.kt | 18 +-- src/jvm/src/main/kotlin/jvm/Patch.kt | 10 +- src/jvm/src/main/kotlin/jvm/RemoveFields.kt | 10 +- .../kotlin/query/{Count.kt => CountQuery.kt} | 15 ++- .../{Definition.kt => DefinitionQuery.kt} | 18 ++- .../query/{Delete.kt => DeleteQuery.kt} | 14 +- .../query/{Document.kt => DocumentQuery.kt} | 14 +- .../query/{Exists.kt => ExistsQuery.kt} | 14 +- .../kotlin/query/{Find.kt => FindQuery.kt} | 15 ++- .../kotlin/query/{Patch.kt => PatchQuery.kt} | 22 +++- src/jvm/src/main/kotlin/query/Query.kt | 3 + .../{RemoveFields.kt => RemoveFieldsQuery.kt} | 17 ++- src/jvm/src/main/kotlin/query/Where.kt | 11 ++ .../bitbadger/documents/java/AutoIdTest.java | 2 +- .../documents/java/DocumentIndexTest.java | 2 +- .../documents/java/FieldMatchTest.java | 2 +- .../bitbadger/documents/java/FieldTest.java | 2 +- .../bitbadger/documents/java/OpTest.java | 2 +- .../documents/java/ParameterNameTest.java | 2 +- .../documents/java/ParameterTest.java | 2 +- .../documents/java/jvm/ParametersTest.java | 2 +- .../integration/common/CountFunctions.java | 27 ++-- .../jvm/integration/postgresql/CountIT.java | 11 +- .../java/jvm/integration/sqlite/CountIT.java | 2 +- .../documents/java/support/JsonDocument.java | 2 - src/jvm/src/test/kotlin/AutoIdTest.kt | 9 +- src/jvm/src/test/kotlin/ComparisonTest.kt | 8 +- src/jvm/src/test/kotlin/ConfigurationTest.kt | 2 +- src/jvm/src/test/kotlin/DialectTest.kt | 2 +- src/jvm/src/test/kotlin/DocumentIndexTest.kt | 2 +- src/jvm/src/test/kotlin/FieldMatchTest.kt | 2 +- src/jvm/src/test/kotlin/FieldTest.kt | 2 +- src/jvm/src/test/kotlin/OpTest.kt | 2 +- src/jvm/src/test/kotlin/ParameterNameTest.kt | 2 +- src/jvm/src/test/kotlin/ParameterTest.kt | 2 +- src/jvm/src/test/kotlin/jvm/ParametersTest.kt | 2 +- .../kotlin/jvm/integration/common/Custom.kt | 24 ++-- .../integration/postgresql/DefinitionIT.kt | 2 +- .../jvm/integration/postgresql/DeleteIT.kt | 2 +- .../jvm/integration/postgresql/DocumentIT.kt | 2 +- .../jvm/integration/postgresql/ExistsIT.kt | 2 +- .../jvm/integration/postgresql/FindIT.kt | 2 +- .../jvm/integration/postgresql/PatchIT.kt | 2 +- .../integration/postgresql/RemoveFieldsIT.kt | 2 +- .../jvm/integration/sqlite/DefinitionIT.kt | 2 +- .../kotlin/jvm/integration/sqlite/DeleteIT.kt | 2 +- .../jvm/integration/sqlite/DocumentIT.kt | 2 +- .../kotlin/jvm/integration/sqlite/ExistsIT.kt | 2 +- .../kotlin/jvm/integration/sqlite/FindIT.kt | 2 +- .../kotlin/jvm/integration/sqlite/PatchIT.kt | 2 +- .../jvm/integration/sqlite/RemoveFieldsIT.kt | 2 +- .../query/{CountTest.kt => CountQueryTest.kt} | 18 +-- ...finitionTest.kt => DefinitionQueryTest.kt} | 28 ++-- .../{DeleteTest.kt => DeleteQueryTest.kt} | 20 +-- .../{DocumentTest.kt => DocumentQueryTest.kt} | 26 ++-- .../{ExistsTest.kt => ExistsQueryTest.kt} | 18 +-- .../query/{FindTest.kt => FindQueryTest.kt} | 35 +++-- .../query/{PatchTest.kt => PatchQueryTest.kt} | 20 +-- src/jvm/src/test/kotlin/query/QueryTest.kt | 4 +- ...FieldsTest.kt => RemoveFieldsQueryTest.kt} | 20 +-- src/jvm/src/test/kotlin/query/WhereTest.kt | 2 +- src/jvm/src/test/kotlin/support/Types.kt | 13 +- src/kotlin/pom.xml | 2 - src/kotlin/src/main/kotlin/Count.kt | 10 +- src/kotlin/src/main/kotlin/Delete.kt | 4 +- src/kotlin/src/main/kotlin/Document.kt | 10 +- src/kotlin/src/main/kotlin/Exists.kt | 4 +- src/kotlin/src/main/kotlin/Find.kt | 18 +-- src/kotlin/src/main/kotlin/Patch.kt | 10 +- src/kotlin/src/main/kotlin/RemoveFields.kt | 4 +- src/kotlin/src/test/kotlin/Types.kt | 52 ++++++++ 82 files changed, 450 insertions(+), 428 deletions(-) delete mode 100644 .idea/modules.xml rename src/jvm/src/main/kotlin/query/{Count.kt => CountQuery.kt} (80%) rename src/jvm/src/main/kotlin/query/{Definition.kt => DefinitionQuery.kt} (87%) rename src/jvm/src/main/kotlin/query/{Delete.kt => DeleteQuery.kt} (84%) rename src/jvm/src/main/kotlin/query/{Document.kt => DocumentQuery.kt} (85%) rename src/jvm/src/main/kotlin/query/{Exists.kt => ExistsQuery.kt} (84%) rename src/jvm/src/main/kotlin/query/{Find.kt => FindQuery.kt} (84%) rename src/jvm/src/main/kotlin/query/{Patch.kt => PatchQuery.kt} (79%) rename src/jvm/src/main/kotlin/query/{RemoveFields.kt => RemoveFieldsQuery.kt} (84%) rename src/jvm/src/test/kotlin/query/{CountTest.kt => CountQueryTest.kt} (79%) rename src/jvm/src/test/kotlin/query/{DefinitionTest.kt => DefinitionQueryTest.kt} (80%) rename src/jvm/src/test/kotlin/query/{DeleteTest.kt => DeleteQueryTest.kt} (75%) rename src/jvm/src/test/kotlin/query/{DocumentTest.kt => DocumentQueryTest.kt} (85%) rename src/jvm/src/test/kotlin/query/{ExistsTest.kt => ExistsQueryTest.kt} (82%) rename src/jvm/src/test/kotlin/query/{FindTest.kt => FindQueryTest.kt} (63%) rename src/jvm/src/test/kotlin/query/{PatchTest.kt => PatchQueryTest.kt} (80%) rename src/jvm/src/test/kotlin/query/{RemoveFieldsTest.kt => RemoveFieldsQueryTest.kt} (82%) create mode 100644 src/kotlin/src/test/kotlin/Types.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 1106a32..afb5348 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -6,11 +6,13 @@ - + + + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index e48c513..2afc1f9 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,9 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index bb44937..c22b6fa 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 724dd10..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3f86db7..6871788 100644 --- a/pom.xml +++ b/pom.xml @@ -43,125 +43,8 @@ 1.8.0 - - - - src/main/kotlin - src/test/kotlin - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - process-sources - - compile - - - - test-compile - test-compile - - test-compile - - - - - - kotlinx-serialization - - - - - org.jetbrains.kotlin - kotlin-maven-serialization - ${kotlin.version} - - - - - maven-surefire-plugin - 2.22.2 - - - maven-failsafe-plugin - 2.22.2 - - - - integration-test - verify - - - - - - org.codehaus.mojo - exec-maven-plugin - 1.6.0 - - MainKt - - - - org.apache.maven.plugins - maven-compiler-plugin - - 9 - 9 - - - - - - - - org.junit.jupiter - junit-jupiter - 5.11.1 - test - - - org.jetbrains.kotlin - kotlin-test-junit5 - ${kotlin.version} - test - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - org.jetbrains.kotlin - kotlin-reflect - ${kotlin.version} - - - org.jetbrains.kotlinx - kotlinx-serialization-json - ${serialization.version} - - - org.xerial - sqlite-jdbc - 3.46.1.2 - test - - - org.postgresql - postgresql - 42.7.5 - test - - + + src + \ No newline at end of file diff --git a/src/jvm/src/main/kotlin/extensions/Connection.kt b/src/jvm/src/main/kotlin/extensions/Connection.kt index 97c44ab..8c4267a 100644 --- a/src/jvm/src/main/kotlin/extensions/Connection.kt +++ b/src/jvm/src/main/kotlin/extensions/Connection.kt @@ -24,7 +24,7 @@ fun Connection.customList( clazz: Class, mapFunc: (ResultSet, Class) -> TDoc ) = - solutions.bitbadger.documents.jvm.Custom.list(query, parameters, clazz, this, mapFunc) + Custom.list(query, parameters, clazz, this, mapFunc) /** * Execute a query that returns one or no results @@ -41,7 +41,7 @@ fun Connection.customSingle( clazz: Class, mapFunc: (ResultSet, Class) -> TDoc ) = - solutions.bitbadger.documents.jvm.Custom.single(query, parameters, clazz, this, mapFunc) + Custom.single(query, parameters, clazz, this, mapFunc) /** * Execute a query that returns no results @@ -50,7 +50,7 @@ fun Connection.customSingle( * @param parameters Parameters to use for the query */ fun Connection.customNonQuery(query: String, parameters: Collection> = listOf()) = - solutions.bitbadger.documents.jvm.Custom.nonQuery(query, parameters, this) + Custom.nonQuery(query, parameters, this) /** * Execute a query that returns a scalar result @@ -67,7 +67,7 @@ fun Connection.customScalar( clazz: Class, mapFunc: (ResultSet, Class) -> T ) = - solutions.bitbadger.documents.jvm.Custom.scalar(query, parameters, clazz, this, mapFunc) + Custom.scalar(query, parameters, clazz, this, mapFunc) // ~~~ DEFINITION QUERIES ~~~ @@ -109,7 +109,7 @@ fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) * @param document The document to be inserted */ fun Connection.insert(tableName: String, document: TDoc) = - Document.insert(tableName, document, this) + Document.insert(tableName, document, this) /** * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") @@ -149,6 +149,7 @@ fun Connection.countAll(tableName: String) = * @param howMatched How the fields should be matched * @return A count of the matching documents in the table */ +@JvmOverloads fun Connection.countByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = Count.byFields(tableName, fields, howMatched, this) @@ -196,6 +197,7 @@ fun Connection.existsById(tableName: String, docId: TKey) = * @param howMatched How the fields should be matched * @return True if any matching documents exist, false if not */ +@JvmOverloads fun Connection.existsByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = Exists.byFields(tableName, fields, howMatched, this) @@ -499,6 +501,7 @@ fun Connection.deleteById(tableName: String, docId: TKey) = * @param fields The fields which should be compared * @param howMatched How the fields should be matched */ +@JvmOverloads fun Connection.deleteByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = Delete.byFields(tableName, fields, howMatched, this) diff --git a/src/jvm/src/main/kotlin/jvm/Count.kt b/src/jvm/src/main/kotlin/jvm/Count.kt index 94ced3a..1127d80 100644 --- a/src/jvm/src/main/kotlin/jvm/Count.kt +++ b/src/jvm/src/main/kotlin/jvm/Count.kt @@ -1,7 +1,7 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.query.Count +import solutions.bitbadger.documents.query.CountQuery import solutions.bitbadger.documents.extensions.customScalar import java.sql.Connection @@ -19,7 +19,7 @@ object Count { */ @JvmStatic fun all(tableName: String, conn: Connection) = - conn.customScalar(Count.all(tableName), listOf(), Long::class.java, Results::toCount) + conn.customScalar(CountQuery.all(tableName), listOf(), Long::class.java, Results::toCount) /** * Count all documents in the table @@ -50,7 +50,7 @@ object Count { ): Long { val named = Parameters.nameFields(fields) return conn.customScalar( - Count.byFields(tableName, named, howMatched), + CountQuery.byFields(tableName, named, howMatched), Parameters.addFields(named), Long::class.java, Results::toCount @@ -82,7 +82,7 @@ object Count { @JvmStatic fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customScalar( - Count.byContains(tableName), + CountQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Long::class.java, Results::toCount @@ -112,7 +112,7 @@ object Count { @JvmStatic fun byJsonPath(tableName: String, path: String, conn: Connection) = conn.customScalar( - Count.byJsonPath(tableName), + CountQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), Long::class.java, Results::toCount diff --git a/src/jvm/src/main/kotlin/jvm/Definition.kt b/src/jvm/src/main/kotlin/jvm/Definition.kt index 7946838..8e18818 100644 --- a/src/jvm/src/main/kotlin/jvm/Definition.kt +++ b/src/jvm/src/main/kotlin/jvm/Definition.kt @@ -4,7 +4,7 @@ import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.DocumentIndex import solutions.bitbadger.documents.extensions.customNonQuery -import solutions.bitbadger.documents.query.Definition +import solutions.bitbadger.documents.query.DefinitionQuery import java.sql.Connection /** @@ -21,8 +21,8 @@ object Definition { @JvmStatic fun ensureTable(tableName: String, conn: Connection) = Configuration.dialect("ensure $tableName exists").let { - conn.customNonQuery(Definition.ensureTable(tableName, it)) - conn.customNonQuery(Definition.ensureKey(tableName, it)) + conn.customNonQuery(DefinitionQuery.ensureTable(tableName, it)) + conn.customNonQuery(DefinitionQuery.ensureKey(tableName, it)) } /** @@ -44,7 +44,7 @@ object Definition { */ @JvmStatic fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection, conn: Connection) = - conn.customNonQuery(Definition.ensureIndexOn(tableName, indexName, fields)) + conn.customNonQuery(DefinitionQuery.ensureIndexOn(tableName, indexName, fields)) /** * Create an index on field(s) within documents in the specified table if necessary @@ -68,7 +68,7 @@ object Definition { @Throws(DocumentException::class) @JvmStatic fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) = - conn.customNonQuery(Definition.ensureDocumentIndexOn(tableName, indexType)) + conn.customNonQuery(DefinitionQuery.ensureDocumentIndexOn(tableName, indexType)) /** * Create a document index on a table (PostgreSQL only) diff --git a/src/jvm/src/main/kotlin/jvm/Delete.kt b/src/jvm/src/main/kotlin/jvm/Delete.kt index 9715698..d442d7f 100644 --- a/src/jvm/src/main/kotlin/jvm/Delete.kt +++ b/src/jvm/src/main/kotlin/jvm/Delete.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customNonQuery -import solutions.bitbadger.documents.query.Delete +import solutions.bitbadger.documents.query.DeleteQuery import java.sql.Connection /** @@ -20,7 +20,7 @@ object Delete { @JvmStatic fun byId(tableName: String, docId: TKey, conn: Connection) = conn.customNonQuery( - Delete.byId(tableName, docId), + DeleteQuery.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))) ) @@ -46,7 +46,7 @@ object Delete { @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { val named = Parameters.nameFields(fields) - conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named)) + conn.customNonQuery(DeleteQuery.byFields(tableName, named, howMatched), Parameters.addFields(named)) } /** @@ -72,7 +72,7 @@ object Delete { @Throws(DocumentException::class) @JvmStatic fun byContains(tableName: String, criteria: TContains, conn: Connection) = - conn.customNonQuery(Delete.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) + conn.customNonQuery(DeleteQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) /** * Delete documents using a JSON containment query (PostgreSQL only) @@ -97,7 +97,7 @@ object Delete { @Throws(DocumentException::class) @JvmStatic fun byJsonPath(tableName: String, path: String, conn: Connection) = - conn.customNonQuery(Delete.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) + conn.customNonQuery(DeleteQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) /** * Delete documents using a JSON Path match query (PostgreSQL only) diff --git a/src/jvm/src/main/kotlin/jvm/Document.kt b/src/jvm/src/main/kotlin/jvm/Document.kt index ba7faac..7b2e0eb 100644 --- a/src/jvm/src/main/kotlin/jvm/Document.kt +++ b/src/jvm/src/main/kotlin/jvm/Document.kt @@ -2,10 +2,9 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.extensions.customNonQuery -import solutions.bitbadger.documents.query.Document +import solutions.bitbadger.documents.query.DocumentQuery import solutions.bitbadger.documents.query.Where import solutions.bitbadger.documents.query.statementWhere import java.sql.Connection @@ -25,35 +24,10 @@ object Document { @JvmStatic fun insert(tableName: String, document: TDoc, conn: Connection) { val strategy = Configuration.autoIdStrategy - val query = if (strategy == AutoId.DISABLED) { - Document.insert(tableName) + val query = if (strategy == AutoId.DISABLED || !AutoId.needsAutoId(strategy, document, Configuration.idField)) { + 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" - } - - Document.insert(tableName).replace(":data", dataParam) + DocumentQuery.insert(tableName, strategy) } conn.customNonQuery(query, listOf(Parameters.json(":data", document))) } @@ -77,7 +51,7 @@ object Document { */ @JvmStatic fun save(tableName: String, document: TDoc, conn: Connection) = - conn.customNonQuery(Document.save(tableName), listOf(Parameters.json(":data", document))) + 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") @@ -100,7 +74,7 @@ object Document { @JvmStatic fun update(tableName: String, docId: TKey, document: TDoc, conn: Connection) = conn.customNonQuery( - statementWhere(Document.update(tableName), Where.byId(":id", docId)), + statementWhere(DocumentQuery.update(tableName), Where.byId(":id", docId)), Parameters.addFields( listOf(Field.equal(Configuration.idField, docId, ":id")), mutableListOf(Parameters.json(":data", document)) diff --git a/src/jvm/src/main/kotlin/jvm/Exists.kt b/src/jvm/src/main/kotlin/jvm/Exists.kt index d69fa88..96d3cfb 100644 --- a/src/jvm/src/main/kotlin/jvm/Exists.kt +++ b/src/jvm/src/main/kotlin/jvm/Exists.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customScalar -import solutions.bitbadger.documents.query.Exists +import solutions.bitbadger.documents.query.ExistsQuery import java.sql.Connection /** @@ -21,7 +21,7 @@ object Exists { @JvmStatic fun byId(tableName: String, docId: TKey, conn: Connection) = conn.customScalar( - Exists.byId(tableName, docId), + ExistsQuery.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), Boolean::class.java, Results::toExists @@ -57,7 +57,7 @@ object Exists { ): Boolean { val named = Parameters.nameFields(fields) return conn.customScalar( - Exists.byFields(tableName, named, howMatched), + ExistsQuery.byFields(tableName, named, howMatched), Parameters.addFields(named), Boolean::class.java, Results::toExists @@ -90,7 +90,7 @@ object Exists { @JvmStatic fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customScalar( - Exists.byContains(tableName), + ExistsQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Boolean::class.java, Results::toExists @@ -122,7 +122,7 @@ object Exists { @JvmStatic fun byJsonPath(tableName: String, path: String, conn: Connection) = conn.customScalar( - Exists.byJsonPath(tableName), + ExistsQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), Boolean::class.java, Results::toExists diff --git a/src/jvm/src/main/kotlin/jvm/Find.kt b/src/jvm/src/main/kotlin/jvm/Find.kt index ab22a91..59948cc 100644 --- a/src/jvm/src/main/kotlin/jvm/Find.kt +++ b/src/jvm/src/main/kotlin/jvm/Find.kt @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customList import solutions.bitbadger.documents.extensions.customSingle -import solutions.bitbadger.documents.query.Find +import solutions.bitbadger.documents.query.FindQuery import solutions.bitbadger.documents.query.orderBy import java.sql.Connection @@ -23,7 +23,7 @@ object Find { */ @JvmStatic fun all(tableName: String, clazz: Class, orderBy: Collection>? = null, conn: Connection) = - conn.customList(Find.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), clazz, Results::fromData) + conn.customList(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), clazz, Results::fromData) /** * Retrieve all documents in the given table @@ -62,7 +62,7 @@ object Find { @JvmStatic fun byId(tableName: String, docId: TKey, clazz: Class, conn: Connection) = conn.customSingle( - Find.byId(tableName, docId), + FindQuery.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), clazz, Results::fromData @@ -102,7 +102,7 @@ object Find { ): List { val named = Parameters.nameFields(fields) return conn.customList( - Find.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), Parameters.addFields(named), clazz, Results::fromData @@ -171,7 +171,7 @@ object Find { conn: Connection ) = conn.customList( - Find.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameters.json(":criteria", criteria)), clazz, Results::fromData @@ -234,7 +234,7 @@ object Find { conn: Connection ) = conn.customList( - Find.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameter(":path", ParameterType.STRING, path)), clazz, Results::fromData @@ -293,7 +293,7 @@ object Find { ): TDoc? { val named = Parameters.nameFields(fields) return conn.customSingle( - Find.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), Parameters.addFields(named), clazz, Results::fromData @@ -361,7 +361,7 @@ object Find { conn: Connection ) = conn.customSingle( - Find.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameters.json(":criteria", criteria)), clazz, Results::fromData @@ -429,7 +429,7 @@ object Find { conn: Connection ) = conn.customSingle( - Find.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameter(":path", ParameterType.STRING, path)), clazz, Results::fromData diff --git a/src/jvm/src/main/kotlin/jvm/Patch.kt b/src/jvm/src/main/kotlin/jvm/Patch.kt index 80d265a..1b09991 100644 --- a/src/jvm/src/main/kotlin/jvm/Patch.kt +++ b/src/jvm/src/main/kotlin/jvm/Patch.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customNonQuery -import solutions.bitbadger.documents.query.Patch +import solutions.bitbadger.documents.query.PatchQuery import java.sql.Connection /** @@ -21,7 +21,7 @@ object Patch { @JvmStatic fun byId(tableName: String, docId: TKey, patch: TPatch, conn: Connection) = conn.customNonQuery( - Patch.byId(tableName, docId), + PatchQuery.byId(tableName, docId), Parameters.addFields( listOf(Field.equal(Configuration.idField, docId, ":id")), mutableListOf(Parameters.json(":data", patch)) @@ -58,7 +58,7 @@ object Patch { ) { val named = Parameters.nameFields(fields) conn.customNonQuery( - Patch.byFields(tableName, named, howMatched), Parameters.addFields( + PatchQuery.byFields(tableName, named, howMatched), Parameters.addFields( named, mutableListOf(Parameters.json(":data", patch)) ) @@ -96,7 +96,7 @@ object Patch { @JvmStatic fun byContains(tableName: String, criteria: TContains, patch: TPatch, conn: Connection) = conn.customNonQuery( - Patch.byContains(tableName), + PatchQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch)) ) @@ -126,7 +126,7 @@ object Patch { @JvmStatic fun byJsonPath(tableName: String, path: String, patch: TPatch, conn: Connection) = conn.customNonQuery( - Patch.byJsonPath(tableName), + PatchQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch)) ) diff --git a/src/jvm/src/main/kotlin/jvm/RemoveFields.kt b/src/jvm/src/main/kotlin/jvm/RemoveFields.kt index 22bc308..0484b1e 100644 --- a/src/jvm/src/main/kotlin/jvm/RemoveFields.kt +++ b/src/jvm/src/main/kotlin/jvm/RemoveFields.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customNonQuery -import solutions.bitbadger.documents.query.RemoveFields +import solutions.bitbadger.documents.query.RemoveFieldsQuery import java.sql.Connection /** @@ -36,7 +36,7 @@ object RemoveFields { fun byId(tableName: String, docId: TKey, toRemove: Collection, conn: Connection) { val nameParams = Parameters.fieldNames(toRemove) conn.customNonQuery( - RemoveFields.byId(tableName, nameParams, docId), + RemoveFieldsQuery.byId(tableName, nameParams, docId), Parameters.addFields( listOf(Field.equal(Configuration.idField, docId, ":id")), translatePath(nameParams) @@ -75,7 +75,7 @@ object RemoveFields { val named = Parameters.nameFields(fields) val nameParams = Parameters.fieldNames(toRemove) conn.customNonQuery( - RemoveFields.byFields(tableName, nameParams, named, howMatched), + RemoveFieldsQuery.byFields(tableName, nameParams, named, howMatched), Parameters.addFields(named, translatePath(nameParams)) ) } @@ -117,7 +117,7 @@ object RemoveFields { ) { val nameParams = Parameters.fieldNames(toRemove) conn.customNonQuery( - RemoveFields.byContains(tableName, nameParams), + RemoveFieldsQuery.byContains(tableName, nameParams), listOf(Parameters.json(":criteria", criteria), *nameParams.toTypedArray()) ) } @@ -149,7 +149,7 @@ object RemoveFields { fun byJsonPath(tableName: String, path: String, toRemove: Collection, conn: Connection) { val nameParams = Parameters.fieldNames(toRemove) conn.customNonQuery( - RemoveFields.byJsonPath(tableName, nameParams), + RemoveFieldsQuery.byJsonPath(tableName, nameParams), listOf(Parameter(":path", ParameterType.STRING, path), *nameParams.toTypedArray()) ) } diff --git a/src/jvm/src/main/kotlin/query/Count.kt b/src/jvm/src/main/kotlin/query/CountQuery.kt similarity index 80% rename from src/jvm/src/main/kotlin/query/Count.kt rename to src/jvm/src/main/kotlin/query/CountQuery.kt index e3db117..82b97b4 100644 --- a/src/jvm/src/main/kotlin/query/Count.kt +++ b/src/jvm/src/main/kotlin/query/CountQuery.kt @@ -1,13 +1,15 @@ package solutions.bitbadger.documents.query +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch -import solutions.bitbadger.documents.query.byFields as byFieldsBase; +import kotlin.jvm.Throws +import solutions.bitbadger.documents.query.byFields as byFieldsBase /** * Functions to count documents */ -object Count { +object CountQuery { /** * Query to count all documents in a table @@ -15,6 +17,7 @@ object Count { * @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" @@ -26,6 +29,8 @@ object Count { * @param howMatched How fields should be compared (optional, defaults to ALL) * @return A query to count documents matching the given fields */ + @JvmStatic + @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = byFieldsBase(all(tableName), fields, howMatched) @@ -34,7 +39,10 @@ object Count { * * @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()) @@ -43,7 +51,10 @@ object Count { * * @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/jvm/src/main/kotlin/query/Definition.kt b/src/jvm/src/main/kotlin/query/DefinitionQuery.kt similarity index 87% rename from src/jvm/src/main/kotlin/query/Definition.kt rename to src/jvm/src/main/kotlin/query/DefinitionQuery.kt index 6187216..24876bb 100644 --- a/src/jvm/src/main/kotlin/query/Definition.kt +++ b/src/jvm/src/main/kotlin/query/DefinitionQuery.kt @@ -1,11 +1,12 @@ package solutions.bitbadger.documents.query import solutions.bitbadger.documents.* +import kotlin.jvm.Throws /** * Functions to create queries to define tables and indexes */ -object Definition { +object DefinitionQuery { /** * SQL statement to create a document table @@ -14,6 +15,7 @@ object Definition { * @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)" @@ -23,7 +25,11 @@ object Definition { * @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") @@ -47,7 +53,11 @@ object Definition { * @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, @@ -70,7 +80,11 @@ object Definition { * @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") @@ -82,6 +96,8 @@ object Definition { * @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") diff --git a/src/jvm/src/main/kotlin/query/Delete.kt b/src/jvm/src/main/kotlin/query/DeleteQuery.kt similarity index 84% rename from src/jvm/src/main/kotlin/query/Delete.kt rename to src/jvm/src/main/kotlin/query/DeleteQuery.kt index 0899c8b..8241a6e 100644 --- a/src/jvm/src/main/kotlin/query/Delete.kt +++ b/src/jvm/src/main/kotlin/query/DeleteQuery.kt @@ -1,14 +1,16 @@ 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 Delete { +object DeleteQuery { /** * Query to delete documents from a table @@ -26,6 +28,8 @@ object Delete { * @param docId The ID of the document (optional, used for type checking) * @return A query to delete a document by its ID */ + @JvmStatic + @JvmOverloads fun byId(tableName: String, docId: TKey? = null) = byIdBase(delete(tableName), docId) @@ -37,6 +41,8 @@ object Delete { * @param howMatched How fields should be compared (optional, defaults to ALL) * @return A query to delete documents matching for the given fields */ + @JvmStatic + @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = byFieldsBase(delete(tableName), fields, howMatched) @@ -45,7 +51,10 @@ object Delete { * * @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()) @@ -54,7 +63,10 @@ object Delete { * * @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/jvm/src/main/kotlin/query/Document.kt b/src/jvm/src/main/kotlin/query/DocumentQuery.kt similarity index 85% rename from src/jvm/src/main/kotlin/query/Document.kt rename to src/jvm/src/main/kotlin/query/DocumentQuery.kt index c60c986..34834c6 100644 --- a/src/jvm/src/main/kotlin/query/Document.kt +++ b/src/jvm/src/main/kotlin/query/DocumentQuery.kt @@ -3,20 +3,26 @@ 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 Document { +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 id = Configuration.idField val values = when (Configuration.dialect("create INSERT statement")) { Dialect.POSTGRESQL -> when (autoId ?: AutoId.DISABLED) { AutoId.DISABLED -> ":data" @@ -42,7 +48,10 @@ object Document { * * @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" @@ -53,6 +62,7 @@ object Document { * @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/jvm/src/main/kotlin/query/Exists.kt b/src/jvm/src/main/kotlin/query/ExistsQuery.kt similarity index 84% rename from src/jvm/src/main/kotlin/query/Exists.kt rename to src/jvm/src/main/kotlin/query/ExistsQuery.kt index 54689d0..a77457e 100644 --- a/src/jvm/src/main/kotlin/query/Exists.kt +++ b/src/jvm/src/main/kotlin/query/ExistsQuery.kt @@ -1,12 +1,14 @@ 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 Exists { +object ExistsQuery { /** * Query to check for document existence in a table @@ -25,6 +27,8 @@ object Exists { * @param docId The ID of the document (optional, used for type checking) * @return A query to determine document existence by ID */ + @JvmStatic + @JvmOverloads fun byId(tableName: String, docId: TKey? = null) = exists(tableName, Where.byId(docId = docId)) @@ -36,6 +40,8 @@ object Exists { * @param howMatched How fields should be compared (optional, defaults to ALL) * @return A query to determine document existence for the given fields */ + @JvmStatic + @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = exists(tableName, Where.byFields(fields, howMatched)) @@ -44,7 +50,10 @@ object Exists { * * @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()) @@ -53,7 +62,10 @@ object Exists { * * @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/jvm/src/main/kotlin/query/Find.kt b/src/jvm/src/main/kotlin/query/FindQuery.kt similarity index 84% rename from src/jvm/src/main/kotlin/query/Find.kt rename to src/jvm/src/main/kotlin/query/FindQuery.kt index 6d57232..3606a2b 100644 --- a/src/jvm/src/main/kotlin/query/Find.kt +++ b/src/jvm/src/main/kotlin/query/FindQuery.kt @@ -1,14 +1,16 @@ 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 Find { +object FindQuery { /** * Query to retrieve all documents from a table @@ -16,6 +18,7 @@ object Find { * @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" @@ -26,6 +29,8 @@ object Find { * @param docId The ID of the document (optional, used for type checking) * @return A query to retrieve a document by its ID */ + @JvmStatic + @JvmOverloads fun byId(tableName: String, docId: TKey? = null) = byIdBase(all(tableName), docId) @@ -37,6 +42,8 @@ object Find { * @param howMatched How fields should be compared (optional, defaults to ALL) * @return A query to retrieve documents matching the given fields */ + @JvmStatic + @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = byFieldsBase(all(tableName), fields, howMatched) @@ -45,7 +52,10 @@ object Find { * * @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()) @@ -54,7 +64,10 @@ object Find { * * @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/jvm/src/main/kotlin/query/Patch.kt b/src/jvm/src/main/kotlin/query/PatchQuery.kt similarity index 79% rename from src/jvm/src/main/kotlin/query/Patch.kt rename to src/jvm/src/main/kotlin/query/PatchQuery.kt index dc1e699..dbde8fe 100644 --- a/src/jvm/src/main/kotlin/query/Patch.kt +++ b/src/jvm/src/main/kotlin/query/PatchQuery.kt @@ -1,16 +1,14 @@ package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch +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 Patch { +object PatchQuery { /** * Create an `UPDATE` statement to patch documents @@ -30,7 +28,11 @@ object Patch { * @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) @@ -41,7 +43,11 @@ object Patch { * @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) @@ -50,7 +56,10 @@ object Patch { * * @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()) @@ -59,7 +68,10 @@ object Patch { * * @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/jvm/src/main/kotlin/query/Query.kt b/src/jvm/src/main/kotlin/query/Query.kt index 39784bc..c79203e 100644 --- a/src/jvm/src/main/kotlin/query/Query.kt +++ b/src/jvm/src/main/kotlin/query/Query.kt @@ -1,3 +1,4 @@ +@file:JvmName("QueryUtils") package solutions.bitbadger.documents.query import solutions.bitbadger.documents.Configuration @@ -35,6 +36,7 @@ fun byId(statement: String, docId: TKey) = * @param howMatched Whether to match any or all of the field conditions (optional; default ALL) * @return A query addressing documents by field matching conditions */ +@JvmOverloads fun byFields(statement: String, fields: Collection>, howMatched: FieldMatch? = null) = statementWhere(statement, Where.byFields(fields, howMatched)) @@ -45,6 +47,7 @@ fun byFields(statement: String, fields: Collection>, howMatched: FieldM * @param dialect The SQL dialect for the generated clause * @return An `ORDER BY` clause for the given fields */ +@JvmOverloads fun orderBy(fields: Collection>, dialect: Dialect? = null): String { val mode = dialect ?: Configuration.dialect("generate ORDER BY clause") if (fields.isEmpty()) return "" diff --git a/src/jvm/src/main/kotlin/query/RemoveFields.kt b/src/jvm/src/main/kotlin/query/RemoveFieldsQuery.kt similarity index 84% rename from src/jvm/src/main/kotlin/query/RemoveFields.kt rename to src/jvm/src/main/kotlin/query/RemoveFieldsQuery.kt index b3842c4..fd036ee 100644 --- a/src/jvm/src/main/kotlin/query/RemoveFields.kt +++ b/src/jvm/src/main/kotlin/query/RemoveFieldsQuery.kt @@ -1,13 +1,14 @@ 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 RemoveFields { +object RemoveFieldsQuery { /** * Create a query to remove fields based on the given parameters @@ -31,7 +32,11 @@ object RemoveFields { * @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) @@ -43,7 +48,11 @@ object RemoveFields { * @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>, @@ -58,7 +67,10 @@ object RemoveFields { * @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()) @@ -68,7 +80,10 @@ object RemoveFields { * @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/jvm/src/main/kotlin/query/Where.kt b/src/jvm/src/main/kotlin/query/Where.kt index dce4a32..626002e 100644 --- a/src/jvm/src/main/kotlin/query/Where.kt +++ b/src/jvm/src/main/kotlin/query/Where.kt @@ -5,6 +5,7 @@ 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 @@ -18,6 +19,8 @@ object Where { * @param howMatched How the fields should be matched (optional, defaults to `ALL`) * @return A `WHERE` clause fragment to match the given fields */ + @JvmStatic + @JvmOverloads fun byFields(fields: Collection>, howMatched: FieldMatch? = null) = fields.joinToString(" ${(howMatched ?: FieldMatch.ALL).sql} ") { it.toWhere() } @@ -27,6 +30,8 @@ object Where { * @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) */ + @JvmStatic + @JvmOverloads fun byId(parameterName: String = ":id", docId: TKey? = null) = byFields(listOf(Field.equal(Configuration.idField, docId ?: "", parameterName))) @@ -37,6 +42,9 @@ object Where { * @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" @@ -50,6 +58,9 @@ object Where { * @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)" diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java index 511d621..b157359 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `AutoId` enum */ -@DisplayName("Java | Common | AutoId") +@DisplayName("JVM | Java | AutoId") final public class AutoIdTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java index 47b9543..72b3d79 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `DocumentIndex` enum */ -@DisplayName("Java | Common | DocumentIndex") +@DisplayName("JVM | Java | DocumentIndex") final public class DocumentIndexTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java index 6854747..f9dab5f 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `FieldMatch` enum */ -@DisplayName("Java | Common | FieldMatch") +@DisplayName("JVM | Java | FieldMatch") final public class FieldMatchTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java index 9af8b6d..2a7572d 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java @@ -14,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Field` class */ -@DisplayName("Java | Common | Field") +@DisplayName("JVM | Java | Field") final public class FieldTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java index 3f800c8..f216ee4 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `Op` enum */ -@DisplayName("Java | Common | Op") +@DisplayName("JVM | Java | Op") final public class OpTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java index 4a38121..1c18044 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `ParameterName` class */ -@DisplayName("Java | Common | ParameterName") +@DisplayName("JVM | Java | ParameterName") final public class ParameterNameTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java index acbf0cf..9ca4432 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Parameter` class */ -@DisplayName("Java | Common | Parameter") +@DisplayName("JVM | Java | Parameter") final public class ParameterTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java index 59902e7..4c5f39f 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java @@ -13,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Parameters` object */ -@DisplayName("Java | Java | Parameters") +@DisplayName("JVM | Java | Parameters") final public class ParametersTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java index f3dfd47..c32598d 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java @@ -1,14 +1,15 @@ package solutions.bitbadger.documents.java.jvm.integration.common; +import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.Field; -import solutions.bitbadger.documents.jvm.Count; -import solutions.bitbadger.documents.support.ThrowawayDatabase; import solutions.bitbadger.documents.java.support.JsonDocument; +import solutions.bitbadger.documents.support.ThrowawayDatabase; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static solutions.bitbadger.documents.extensions.ConnExt.*; import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; /** @@ -18,42 +19,42 @@ final public class CountFunctions { public static void all(ThrowawayDatabase db) { JsonDocument.load(db); - assertEquals(5L, Count.all(TEST_TABLE, db.getConn()), "There should have been 5 documents in the table"); + assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should have been 5 documents in the table"); } public static void byFieldsNumeric(ThrowawayDatabase db) { JsonDocument.load(db); - assertEquals(3L, Count.byFields(TEST_TABLE, List.of(Field.between("numValue", 10, 20)), db.getConn()), + 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) { JsonDocument.load(db); - assertEquals(1L, Count.byFields(TEST_TABLE, List.of(Field.between("value", "aardvark", "apple")), db.getConn()), + 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) { + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals(2L, Count.byContains(TEST_TABLE, Map.of("value", "purple"), db.getConn()), + assertEquals(2L, countByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")), "There should have been 2 matching documents"); } - public static void byContainsNoMatch(ThrowawayDatabase db) { + public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals(0L, Count.byContains(TEST_TABLE, Map.of("value", "magenta"), db.getConn()), + assertEquals(0L, countByContains(db.getConn(), TEST_TABLE, Map.of("value", "magenta")), "There should have been no matching documents"); } - public static void byJsonPathMatch(ThrowawayDatabase db) { + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals(2L, Count.byJsonPath(TEST_TABLE, "$.numValue ? (@ < 5)", db.getConn()), + assertEquals(2L, countByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ < 5)"), "There should have been 2 matching documents"); } - public static void byJsonPathNoMatch(ThrowawayDatabase db) { + public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals(0L, Count.byJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)", db.getConn()), + assertEquals(0L, countByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"), "There should have been no matching documents"); } diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java index eb07bec..97c1d3f 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java @@ -2,13 +2,14 @@ package solutions.bitbadger.documents.java.jvm.integration.postgresql; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.java.jvm.integration.common.CountFunctions; import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB; /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("Java | Java | PostgreSQL: Count") +@DisplayName("JVM | Java | PostgreSQL: Count") public class CountIT { @Test @@ -37,7 +38,7 @@ public class CountIT { @Test @DisplayName("byContains counts documents when matches are found") - public void byContainsMatch() { + public void byContainsMatch() throws DocumentException { try (PgDB db = new PgDB()) { CountFunctions.byContainsMatch(db); } @@ -45,7 +46,7 @@ public class CountIT { @Test @DisplayName("byContains counts documents when no matches are found") - public void byContainsNoMatch() { + public void byContainsNoMatch() throws DocumentException { try (PgDB db = new PgDB()) { CountFunctions.byContainsNoMatch(db); } @@ -53,7 +54,7 @@ public class CountIT { @Test @DisplayName("byJsonPath counts documents when matches are found") - public void byJsonPathMatch() { + public void byJsonPathMatch() throws DocumentException { try (PgDB db = new PgDB()) { CountFunctions.byJsonPathMatch(db); } @@ -61,7 +62,7 @@ public class CountIT { @Test @DisplayName("byJsonPath counts documents when no matches are found") - public void byJsonPathNoMatch() { + public void byJsonPathNoMatch() throws DocumentException { try (PgDB db = new PgDB()) { CountFunctions.byJsonPathNoMatch(db); } diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java index 18a4d70..38fda0b 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; /** * SQLite integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("Java | Java | SQLite: Count") +@DisplayName("JVM | Java | SQLite: Count") public class CountIT { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java index d18acaa..4d5b085 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java @@ -1,6 +1,5 @@ package solutions.bitbadger.documents.java.support; -import kotlinx.serialization.Serializable; import solutions.bitbadger.documents.jvm.Document; import solutions.bitbadger.documents.support.ThrowawayDatabase; @@ -8,7 +7,6 @@ import java.util.List; import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; -@Serializable public class JsonDocument { private String id; diff --git a/src/jvm/src/test/kotlin/AutoIdTest.kt b/src/jvm/src/test/kotlin/AutoIdTest.kt index 4a1aeff..87099ae 100644 --- a/src/jvm/src/test/kotlin/AutoIdTest.kt +++ b/src/jvm/src/test/kotlin/AutoIdTest.kt @@ -3,6 +3,7 @@ package solutions.bitbadger.documents import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import solutions.bitbadger.documents.support.* import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals @@ -11,7 +12,7 @@ import kotlin.test.assertTrue /** * Unit tests for the `AutoId` enum */ -@DisplayName("Kotlin | Common | AutoId") +@DisplayName("JVM | Kotlin | AutoId") class AutoIdTest { @Test @@ -157,9 +158,3 @@ class AutoIdTest { 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/jvm/src/test/kotlin/ComparisonTest.kt b/src/jvm/src/test/kotlin/ComparisonTest.kt index 912000b..46c3cae 100644 --- a/src/jvm/src/test/kotlin/ComparisonTest.kt +++ b/src/jvm/src/test/kotlin/ComparisonTest.kt @@ -9,7 +9,7 @@ import kotlin.test.assertTrue /** * Unit tests for the `ComparisonBetween` class */ -@DisplayName("ComparisonBetween") +@DisplayName("JVM | Kotlin | ComparisonBetween") class ComparisonBetweenTest { @Test @@ -50,7 +50,7 @@ class ComparisonBetweenTest { /** * Unit tests for the `ComparisonIn` class */ -@DisplayName("ComparisonIn") +@DisplayName("JVM | Kotlin | ComparisonIn") class ComparisonInTest { @Test @@ -92,7 +92,7 @@ class ComparisonInTest { /** * Unit tests for the `ComparisonInArray` class */ -@DisplayName("ComparisonInArray") +@DisplayName("JVM | Kotlin | ComparisonInArray") class ComparisonInArrayTest { @Test @@ -138,7 +138,7 @@ class ComparisonInArrayTest { /** * Unit tests for the `ComparisonSingle` class */ -@DisplayName("ComparisonSingle") +@DisplayName("JVM | Kotlin | ComparisonSingle") class ComparisonSingleTest { @Test diff --git a/src/jvm/src/test/kotlin/ConfigurationTest.kt b/src/jvm/src/test/kotlin/ConfigurationTest.kt index a9255a5..bdc8e97 100644 --- a/src/jvm/src/test/kotlin/ConfigurationTest.kt +++ b/src/jvm/src/test/kotlin/ConfigurationTest.kt @@ -8,7 +8,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `Configuration` object */ -@DisplayName("Kotlin | Common | Configuration") +@DisplayName("JVM | Kotlin | Configuration") class ConfigurationTest { @Test diff --git a/src/jvm/src/test/kotlin/DialectTest.kt b/src/jvm/src/test/kotlin/DialectTest.kt index 7dbdce0..f6b4025 100644 --- a/src/jvm/src/test/kotlin/DialectTest.kt +++ b/src/jvm/src/test/kotlin/DialectTest.kt @@ -9,7 +9,7 @@ import kotlin.test.assertTrue /** * Unit tests for the `Dialect` enum */ -@DisplayName("Kotlin | Common | Dialect") +@DisplayName("JVM | Kotlin | Dialect") class DialectTest { @Test diff --git a/src/jvm/src/test/kotlin/DocumentIndexTest.kt b/src/jvm/src/test/kotlin/DocumentIndexTest.kt index f23d6a0..fccbb44 100644 --- a/src/jvm/src/test/kotlin/DocumentIndexTest.kt +++ b/src/jvm/src/test/kotlin/DocumentIndexTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `DocumentIndex` enum */ -@DisplayName("Kotlin | Common | DocumentIndex") +@DisplayName("JVM | Kotlin | DocumentIndex") class DocumentIndexTest { @Test diff --git a/src/jvm/src/test/kotlin/FieldMatchTest.kt b/src/jvm/src/test/kotlin/FieldMatchTest.kt index 2ee51d7..3976773 100644 --- a/src/jvm/src/test/kotlin/FieldMatchTest.kt +++ b/src/jvm/src/test/kotlin/FieldMatchTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `FieldMatch` enum */ -@DisplayName("Kotlin | Common | FieldMatch") +@DisplayName("JVM | Kotlin | FieldMatch") class FieldMatchTest { @Test diff --git a/src/jvm/src/test/kotlin/FieldTest.kt b/src/jvm/src/test/kotlin/FieldTest.kt index a48dbf1..12b71e5 100644 --- a/src/jvm/src/test/kotlin/FieldTest.kt +++ b/src/jvm/src/test/kotlin/FieldTest.kt @@ -11,7 +11,7 @@ import kotlin.test.assertNull /** * Unit tests for the `Field` class */ -@DisplayName("Kotlin | Common | Field") +@DisplayName("JVM | Kotlin | Field") class FieldTest { /** diff --git a/src/jvm/src/test/kotlin/OpTest.kt b/src/jvm/src/test/kotlin/OpTest.kt index 095e9a2..19e0d91 100644 --- a/src/jvm/src/test/kotlin/OpTest.kt +++ b/src/jvm/src/test/kotlin/OpTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `Op` enum */ -@DisplayName("Kotlin | Common | Op") +@DisplayName("JVM | Kotlin | Op") class OpTest { @Test diff --git a/src/jvm/src/test/kotlin/ParameterNameTest.kt b/src/jvm/src/test/kotlin/ParameterNameTest.kt index d8f1d6d..4e67e4a 100644 --- a/src/jvm/src/test/kotlin/ParameterNameTest.kt +++ b/src/jvm/src/test/kotlin/ParameterNameTest.kt @@ -7,7 +7,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `ParameterName` class */ -@DisplayName("Kotlin | Common | ParameterName") +@DisplayName("JVM | Kotlin | ParameterName") class ParameterNameTest { @Test diff --git a/src/jvm/src/test/kotlin/ParameterTest.kt b/src/jvm/src/test/kotlin/ParameterTest.kt index 0dadc04..0ec84ae 100644 --- a/src/jvm/src/test/kotlin/ParameterTest.kt +++ b/src/jvm/src/test/kotlin/ParameterTest.kt @@ -9,7 +9,7 @@ import kotlin.test.assertNull /** * Unit tests for the `Parameter` class */ -@DisplayName("Kotlin | Common | Parameter") +@DisplayName("JVM | Kotlin | Parameter") class ParameterTest { @Test diff --git a/src/jvm/src/test/kotlin/jvm/ParametersTest.kt b/src/jvm/src/test/kotlin/jvm/ParametersTest.kt index 30ef568..ec435f0 100644 --- a/src/jvm/src/test/kotlin/jvm/ParametersTest.kt +++ b/src/jvm/src/test/kotlin/jvm/ParametersTest.kt @@ -12,7 +12,7 @@ import kotlin.test.assertSame /** * Unit tests for the `Parameters` object */ -@DisplayName("Kotlin | Java | Parameters") +@DisplayName("JVM | Kotlin | Parameters") class ParametersTest { /** diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt b/src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt index 34a8160..19e0404 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt @@ -3,9 +3,9 @@ package solutions.bitbadger.documents.jvm.integration.common import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.* import solutions.bitbadger.documents.jvm.Results -import solutions.bitbadger.documents.query.Count -import solutions.bitbadger.documents.query.Delete -import solutions.bitbadger.documents.query.Find +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.query.FindQuery import solutions.bitbadger.documents.support.* import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -19,26 +19,26 @@ object Custom { fun listEmpty(db: ThrowawayDatabase) { JsonDocument.load(db) db.conn.deleteByFields(TEST_TABLE, listOf(Field.exists(Configuration.idField))) - val result = db.conn.customList(Find.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData) + 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(Find.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData) + 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 singleNone(db: ThrowawayDatabase) = assertNull( - db.conn.customSingle(Find.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData), + db.conn.customSingle(FindQuery.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData), "There should not have been a document returned" ) fun singleOne(db: ThrowawayDatabase) { JsonDocument.load(db) assertNotNull( - db.conn.customSingle(Find.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData), + db.conn.customSingle(FindQuery.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData), "There should not have been a document returned" ) } @@ -46,12 +46,12 @@ object Custom { fun nonQueryChanges(db: ThrowawayDatabase) { JsonDocument.load(db) assertEquals( - 5L, db.conn.customScalar(Count.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), + 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(Count.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), + 0L, db.conn.customScalar(CountQuery.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), "There should have been no documents in the table" ) } @@ -59,15 +59,15 @@ object Custom { fun nonQueryNoChanges(db: ThrowawayDatabase) { JsonDocument.load(db) assertEquals( - 5L, db.conn.customScalar(Count.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), + 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.byId(TEST_TABLE, "eighty-two"), + DeleteQuery.byId(TEST_TABLE, "eighty-two"), listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) ) assertEquals( - 5L, db.conn.customScalar(Count.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), + 5L, db.conn.customScalar(CountQuery.all(TEST_TABLE), listOf(), Long::class.java, Results::toCount), "There should still have been 5 documents in the table" ) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt index 298fb93..bb01c6c 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Definition") +@DisplayName("JVM | Kotlin | PostgreSQL: Definition") class DefinitionIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt index be56697..8b322d0 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Delete") +@DisplayName("JVM | Kotlin | PostgreSQL: Delete") class DeleteIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt index 478e9e1..ab3955c 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Document") +@DisplayName("JVM | Kotlin | PostgreSQL: Document") class DocumentIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt index 848962a..c41b14e 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Exists") +@DisplayName("JVM | Kotlin | PostgreSQL: Exists") class ExistsIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt index b6d8e98..32bf564 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Find") +@DisplayName("JVM | Kotlin | PostgreSQL: Find") class FindIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt index 7cedf46..4a9c2ee 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Patch") +@DisplayName("JVM | Kotlin | PostgreSQL: Patch") class PatchIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt index 4919ce8..968106f 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: RemoveFields") +@DisplayName("JVM | Kotlin | PostgreSQL: RemoveFields") class RemoveFieldsIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt index 313eab5..bc47dc0 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Definition") +@DisplayName("JVM | Kotlin | SQLite: Definition") class DefinitionIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt index 7975e1a..bbdda99 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Delete") +@DisplayName("JVM | Kotlin | SQLite: Delete") class DeleteIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt index 414dbcd..1531117 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Document") +@DisplayName("JVM | Kotlin | SQLite: Document") class DocumentIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt index 36b0bd4..b4b65f2 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Exists") +@DisplayName("JVM | Kotlin | SQLite: Exists") class ExistsIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt index abe0aaa..8dab546 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Find` object / `find*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Find") +@DisplayName("JVM | Kotlin | SQLite: Find") class FindIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt index ac1d2c4..7918bef 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Patch") +@DisplayName("JVM | Kotlin | SQLite: Patch") class PatchIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt index 7518ab4..26e51b6 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: RemoveFields") +@DisplayName("JVM | Kotlin | SQLite: RemoveFields") class RemoveFieldsIT { @Test diff --git a/src/jvm/src/test/kotlin/query/CountTest.kt b/src/jvm/src/test/kotlin/query/CountQueryTest.kt similarity index 79% rename from src/jvm/src/test/kotlin/query/CountTest.kt rename to src/jvm/src/test/kotlin/query/CountQueryTest.kt index 7943110..9d2de61 100644 --- a/src/jvm/src/test/kotlin/query/CountTest.kt +++ b/src/jvm/src/test/kotlin/query/CountQueryTest.kt @@ -13,8 +13,8 @@ import kotlin.test.assertEquals /** * Unit tests for the `Count` object */ -@DisplayName("Kotlin | Common | Query: Count") -class CountTest { +@DisplayName("JVM | Kotlin | Query | CountQuery") +class CountQueryTest { /** Test table name */ private val tbl = "test_table" @@ -30,7 +30,7 @@ class CountTest { @Test @DisplayName("all generates correctly") fun all() = - assertEquals("SELECT COUNT(*) AS it FROM $tbl", Count.all(tbl), "Count query not constructed correctly") + assertEquals("SELECT COUNT(*) AS it FROM $tbl", CountQuery.all(tbl), "Count query not constructed correctly") @Test @DisplayName("byFields generates correctly (PostgreSQL)") @@ -38,7 +38,7 @@ class CountTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0", - Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))), + CountQuery.byFields(tbl, listOf(Field.equal("test", "", ":field0"))), "Count query not constructed correctly" ) } @@ -49,7 +49,7 @@ class CountTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0", - Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))), + CountQuery.byFields(tbl, listOf(Field.equal("test", "", ":field0"))), "Count query not constructed correctly" ) } @@ -59,7 +59,7 @@ class CountTest { fun byContainsPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", Count.byContains(tbl), + "SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", CountQuery.byContains(tbl), "Count query not constructed correctly" ) } @@ -68,7 +68,7 @@ class CountTest { @DisplayName("byContains fails (SQLite)") fun byContainsSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Count.byContains(tbl) } + assertThrows { CountQuery.byContains(tbl) } } @Test @@ -77,7 +77,7 @@ class CountTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", - Count.byJsonPath(tbl), "Count query not constructed correctly" + CountQuery.byJsonPath(tbl), "Count query not constructed correctly" ) } @@ -85,6 +85,6 @@ class CountTest { @DisplayName("byJsonPath fails (SQLite)") fun byJsonPathSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Count.byJsonPath(tbl) } + assertThrows { CountQuery.byJsonPath(tbl) } } } diff --git a/src/jvm/src/test/kotlin/query/DefinitionTest.kt b/src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt similarity index 80% rename from src/jvm/src/test/kotlin/query/DefinitionTest.kt rename to src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt index 4a37200..71d1fef 100644 --- a/src/jvm/src/test/kotlin/query/DefinitionTest.kt +++ b/src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt @@ -13,8 +13,8 @@ import kotlin.test.assertEquals /** * Unit tests for the `Definition` object */ -@DisplayName("Kotlin | Common | Query: Definition") -class DefinitionTest { +@DisplayName("JVM | Kotlin | Query | DefinitionQuery") +class DefinitionQueryTest { /** Test table name */ private val tbl = "test_table" @@ -32,27 +32,27 @@ class DefinitionTest { fun ensureTableFor() = assertEquals( "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", - Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly" + DefinitionQuery.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly" ) @Test @DisplayName("ensureTable generates correctly (PostgreSQL)") fun ensureTablePostgres() { Configuration.dialectValue = Dialect.POSTGRESQL - assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data JSONB NOT NULL)", Definition.ensureTable(tbl)) + assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data JSONB NOT NULL)", DefinitionQuery.ensureTable(tbl)) } @Test @DisplayName("ensureTable generates correctly (SQLite)") fun ensureTableSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data TEXT NOT NULL)", Definition.ensureTable(tbl)) + assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data TEXT NOT NULL)", DefinitionQuery.ensureTable(tbl)) } @Test @DisplayName("ensureTable fails when no dialect is set") fun ensureTableFailsUnknown() { - assertThrows { Definition.ensureTable(tbl) } + assertThrows { DefinitionQuery.ensureTable(tbl) } } @Test @@ -60,7 +60,7 @@ class DefinitionTest { fun ensureKeyWithSchema() = assertEquals( "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", - Definition.ensureKey("test.table", Dialect.POSTGRESQL), + DefinitionQuery.ensureKey("test.table", Dialect.POSTGRESQL), "CREATE INDEX for key statement with schema not constructed correctly" ) @@ -69,7 +69,7 @@ class DefinitionTest { fun ensureKeyWithoutSchema() = assertEquals( "CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))", - Definition.ensureKey(tbl, Dialect.SQLITE), + DefinitionQuery.ensureKey(tbl, Dialect.SQLITE), "CREATE INDEX for key statement without schema not constructed correctly" ) @@ -78,7 +78,7 @@ class DefinitionTest { fun ensureIndexOnMultipleFields() = assertEquals( "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", - Definition.ensureIndexOn( + DefinitionQuery.ensureIndexOn( "test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"), Dialect.POSTGRESQL ), @@ -90,7 +90,7 @@ class DefinitionTest { fun ensureIndexOnNestedPostgres() = assertEquals( "CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))", - Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL), + DefinitionQuery.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL), "CREATE INDEX for nested PostgreSQL field incorrect" ) @@ -99,7 +99,7 @@ class DefinitionTest { fun ensureIndexOnNestedSQLite() = assertEquals( "CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))", - Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE), + DefinitionQuery.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE), "CREATE INDEX for nested SQLite field incorrect" ) @@ -109,7 +109,7 @@ class DefinitionTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tbl USING GIN (data)", - Definition.ensureDocumentIndexOn(tbl, DocumentIndex.FULL), + DefinitionQuery.ensureDocumentIndexOn(tbl, DocumentIndex.FULL), "CREATE INDEX for full document index incorrect" ) } @@ -120,7 +120,7 @@ class DefinitionTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tbl USING GIN (data jsonb_path_ops)", - Definition.ensureDocumentIndexOn(tbl, DocumentIndex.OPTIMIZED), + DefinitionQuery.ensureDocumentIndexOn(tbl, DocumentIndex.OPTIMIZED), "CREATE INDEX for optimized document index incorrect" ) } @@ -129,6 +129,6 @@ class DefinitionTest { @DisplayName("ensureDocumentIndexOn fails for SQLite") fun ensureDocumentIndexOnFailsSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Definition.ensureDocumentIndexOn(tbl, DocumentIndex.FULL) } + assertThrows { DefinitionQuery.ensureDocumentIndexOn(tbl, DocumentIndex.FULL) } } } diff --git a/src/jvm/src/test/kotlin/query/DeleteTest.kt b/src/jvm/src/test/kotlin/query/DeleteQueryTest.kt similarity index 75% rename from src/jvm/src/test/kotlin/query/DeleteTest.kt rename to src/jvm/src/test/kotlin/query/DeleteQueryTest.kt index dfdd80a..330b816 100644 --- a/src/jvm/src/test/kotlin/query/DeleteTest.kt +++ b/src/jvm/src/test/kotlin/query/DeleteQueryTest.kt @@ -13,8 +13,8 @@ import kotlin.test.assertEquals /** * Unit tests for the `Delete` object */ -@DisplayName("Kotlin | Common | Query: Delete") -class DeleteTest { +@DisplayName("JVM | Kotlin | Query | DeleteQuery") +class DeleteQueryTest { /** Test table name */ private val tbl = "test_table" @@ -33,7 +33,7 @@ class DeleteTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "DELETE FROM $tbl WHERE data->>'id' = :id", - Delete.byId(tbl), "Delete query not constructed correctly" + DeleteQuery.byId(tbl), "Delete query not constructed correctly" ) } @@ -43,7 +43,7 @@ class DeleteTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "DELETE FROM $tbl WHERE data->>'id' = :id", - Delete.byId(tbl), "Delete query not constructed correctly" + DeleteQuery.byId(tbl), "Delete query not constructed correctly" ) } @@ -52,7 +52,7 @@ class DeleteTest { fun byFieldsPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "DELETE FROM $tbl WHERE data->>'a' = :b", Delete.byFields(tbl, listOf(Field.equal("a", "", ":b"))), + "DELETE FROM $tbl WHERE data->>'a' = :b", DeleteQuery.byFields(tbl, listOf(Field.equal("a", "", ":b"))), "Delete query not constructed correctly" ) } @@ -62,7 +62,7 @@ class DeleteTest { fun byFieldsSQLite() { Configuration.dialectValue = Dialect.SQLITE assertEquals( - "DELETE FROM $tbl WHERE data->>'a' = :b", Delete.byFields(tbl, listOf(Field.equal("a", "", ":b"))), + "DELETE FROM $tbl WHERE data->>'a' = :b", DeleteQuery.byFields(tbl, listOf(Field.equal("a", "", ":b"))), "Delete query not constructed correctly" ) } @@ -72,7 +72,7 @@ class DeleteTest { fun byContainsPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "DELETE FROM $tbl WHERE data @> :criteria", Delete.byContains(tbl), "Delete query not constructed correctly" + "DELETE FROM $tbl WHERE data @> :criteria", DeleteQuery.byContains(tbl), "Delete query not constructed correctly" ) } @@ -80,7 +80,7 @@ class DeleteTest { @DisplayName("byContains fails (SQLite)") fun byContainsSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Delete.byContains(tbl) } + assertThrows { DeleteQuery.byContains(tbl) } } @Test @@ -88,7 +88,7 @@ class DeleteTest { fun byJsonPathPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "DELETE FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", Delete.byJsonPath(tbl), + "DELETE FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", DeleteQuery.byJsonPath(tbl), "Delete query not constructed correctly" ) } @@ -97,6 +97,6 @@ class DeleteTest { @DisplayName("byJsonPath fails (SQLite)") fun byJsonPathSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Delete.byJsonPath(tbl) } + assertThrows { DeleteQuery.byJsonPath(tbl) } } } diff --git a/src/jvm/src/test/kotlin/query/DocumentTest.kt b/src/jvm/src/test/kotlin/query/DocumentQueryTest.kt similarity index 85% rename from src/jvm/src/test/kotlin/query/DocumentTest.kt rename to src/jvm/src/test/kotlin/query/DocumentQueryTest.kt index 6566c7f..bb6d5bc 100644 --- a/src/jvm/src/test/kotlin/query/DocumentTest.kt +++ b/src/jvm/src/test/kotlin/query/DocumentQueryTest.kt @@ -14,8 +14,8 @@ import kotlin.test.assertTrue /** * Unit tests for the `Document` object */ -@DisplayName("Kotlin | Common | Query: Document") -class DocumentTest { +@DisplayName("JVM | Kotlin | Query | DocumentQuery") +class DocumentQueryTest { /** Test table name */ private val tbl = "test_table" @@ -32,14 +32,14 @@ class DocumentTest { @DisplayName("insert generates no auto ID (PostgreSQL)") fun insertNoAutoPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL - assertEquals("INSERT INTO $tbl VALUES (:data)", Document.insert(tbl)) + assertEquals("INSERT INTO $tbl VALUES (:data)", DocumentQuery.insert(tbl)) } @Test @DisplayName("insert generates no auto ID (SQLite)") fun insertNoAutoSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertEquals("INSERT INTO $tbl VALUES (:data)", Document.insert(tbl)) + assertEquals("INSERT INTO $tbl VALUES (:data)", DocumentQuery.insert(tbl)) } @Test @@ -49,7 +49,7 @@ class DocumentTest { assertEquals( "INSERT INTO $tbl VALUES (:data::jsonb || ('{\"id\":' " + "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $tbl) || '}')::jsonb)", - Document.insert(tbl, AutoId.NUMBER) + DocumentQuery.insert(tbl, AutoId.NUMBER) ) } @@ -60,7 +60,7 @@ class DocumentTest { assertEquals( "INSERT INTO $tbl VALUES (json_set(:data, '$.id', " + "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $tbl)))", - Document.insert(tbl, AutoId.NUMBER) + DocumentQuery.insert(tbl, AutoId.NUMBER) ) } @@ -68,7 +68,7 @@ class DocumentTest { @DisplayName("insert generates auto UUID (PostgreSQL)") fun insertAutoUUIDPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL - val query = Document.insert(tbl, AutoId.UUID) + val query = DocumentQuery.insert(tbl, AutoId.UUID) assertTrue( query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""), "Query start not correct (actual: $query)" @@ -80,7 +80,7 @@ class DocumentTest { @DisplayName("insert generates auto UUID (SQLite)") fun insertAutoUUIDSQLite() { Configuration.dialectValue = Dialect.SQLITE - val query = Document.insert(tbl, AutoId.UUID) + val query = DocumentQuery.insert(tbl, AutoId.UUID) assertTrue( query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"), "Query start not correct (actual: $query)" @@ -94,7 +94,7 @@ class DocumentTest { try { Configuration.dialectValue = Dialect.POSTGRESQL Configuration.idStringLength = 8 - val query = Document.insert(tbl, AutoId.RANDOM_STRING) + val query = DocumentQuery.insert(tbl, AutoId.RANDOM_STRING) assertTrue( query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""), "Query start not correct (actual: $query)" @@ -114,7 +114,7 @@ class DocumentTest { @DisplayName("insert generates auto random string (SQLite)") fun insertAutoRandomSQLite() { Configuration.dialectValue = Dialect.SQLITE - val query = Document.insert(tbl, AutoId.RANDOM_STRING) + val query = DocumentQuery.insert(tbl, AutoId.RANDOM_STRING) assertTrue( query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"), "Query start not correct (actual: $query)" @@ -130,7 +130,7 @@ class DocumentTest { @Test @DisplayName("insert fails when no dialect is set") fun insertFailsUnknown() { - assertThrows { Document.insert(tbl) } + assertThrows { DocumentQuery.insert(tbl) } } @Test @@ -139,12 +139,12 @@ class DocumentTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", - Document.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly" + DocumentQuery.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly" ) } @Test @DisplayName("update generates successfully") fun update() = - assertEquals("UPDATE $tbl SET data = :data", Document.update(tbl), "Update query not constructed correctly") + assertEquals("UPDATE $tbl SET data = :data", DocumentQuery.update(tbl), "Update query not constructed correctly") } diff --git a/src/jvm/src/test/kotlin/query/ExistsTest.kt b/src/jvm/src/test/kotlin/query/ExistsQueryTest.kt similarity index 82% rename from src/jvm/src/test/kotlin/query/ExistsTest.kt rename to src/jvm/src/test/kotlin/query/ExistsQueryTest.kt index fb22189..d9d09e2 100644 --- a/src/jvm/src/test/kotlin/query/ExistsTest.kt +++ b/src/jvm/src/test/kotlin/query/ExistsQueryTest.kt @@ -14,7 +14,7 @@ import kotlin.test.assertEquals * Unit tests for the `Exists` object */ @DisplayName("Kotlin | Common | Query: Exists") -class ExistsTest { +class ExistsQueryTest { /** Test table name */ private val tbl = "test_table" @@ -33,7 +33,7 @@ class ExistsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it", - Exists.byId(tbl), "Exists query not constructed correctly" + ExistsQuery.byId(tbl), "Exists query not constructed correctly" ) } @@ -43,7 +43,7 @@ class ExistsTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it", - Exists.byId(tbl), "Exists query not constructed correctly" + ExistsQuery.byId(tbl), "Exists query not constructed correctly" ) } @@ -53,7 +53,7 @@ class ExistsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "SELECT EXISTS (SELECT 1 FROM $tbl WHERE (data->>'it')::numeric = :test) AS it", - Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))), + ExistsQuery.byFields(tbl, listOf(Field.equal("it", 7, ":test"))), "Exists query not constructed correctly" ) } @@ -64,7 +64,7 @@ class ExistsTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'it' = :test) AS it", - Exists.byFields(tbl, listOf(Field.equal("it", 7, ":test"))), + ExistsQuery.byFields(tbl, listOf(Field.equal("it", 7, ":test"))), "Exists query not constructed correctly" ) } @@ -74,7 +74,7 @@ class ExistsTest { fun byContainsPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data @> :criteria) AS it", Exists.byContains(tbl), + "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data @> :criteria) AS it", ExistsQuery.byContains(tbl), "Exists query not constructed correctly" ) } @@ -83,7 +83,7 @@ class ExistsTest { @DisplayName("byContains fails (SQLite)") fun byContainsSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Exists.byContains(tbl) } + assertThrows { ExistsQuery.byContains(tbl) } } @Test @@ -92,7 +92,7 @@ class ExistsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "SELECT EXISTS (SELECT 1 FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)) AS it", - Exists.byJsonPath(tbl), "Exists query not constructed correctly" + ExistsQuery.byJsonPath(tbl), "Exists query not constructed correctly" ) } @@ -100,6 +100,6 @@ class ExistsTest { @DisplayName("byJsonPath fails (SQLite)") fun byJsonPathSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Exists.byJsonPath(tbl) } + assertThrows { ExistsQuery.byJsonPath(tbl) } } } diff --git a/src/jvm/src/test/kotlin/query/FindTest.kt b/src/jvm/src/test/kotlin/query/FindQueryTest.kt similarity index 63% rename from src/jvm/src/test/kotlin/query/FindTest.kt rename to src/jvm/src/test/kotlin/query/FindQueryTest.kt index 5ad3e5a..faeb505 100644 --- a/src/jvm/src/test/kotlin/query/FindTest.kt +++ b/src/jvm/src/test/kotlin/query/FindQueryTest.kt @@ -8,16 +8,14 @@ import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals /** * Unit tests for the `Find` object */ -@DisplayName("Kotlin | Common | Query: Find") -class FindTest { - - /** Test table name */ - private val tbl = "test_table" +@DisplayName("JVM | Kotlin | Query | FindQuery") +class FindQueryTest { /** * Clear the connection string (resets Dialect) @@ -30,15 +28,15 @@ class FindTest { @Test @DisplayName("all generates correctly") fun all() = - assertEquals("SELECT data FROM $tbl", Find.all(tbl), "Find query not constructed correctly") + assertEquals("SELECT data FROM $TEST_TABLE", FindQuery.all(TEST_TABLE), "Find query not constructed correctly") @Test @DisplayName("byId generates correctly (PostgreSQL)") fun byIdPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "SELECT data FROM $tbl WHERE data->>'id' = :id", - Find.byId(tbl), "Find query not constructed correctly" + "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id", + FindQuery.byId(TEST_TABLE), "Find query not constructed correctly" ) } @@ -47,8 +45,8 @@ class FindTest { fun byIdSQLite() { Configuration.dialectValue = Dialect.SQLITE assertEquals( - "SELECT data FROM $tbl WHERE data->>'id' = :id", - Find.byId(tbl), "Find query not constructed correctly" + "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id", + FindQuery.byId(TEST_TABLE), "Find query not constructed correctly" ) } @@ -57,8 +55,8 @@ class FindTest { fun byFieldsPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "SELECT data FROM $tbl WHERE data->>'a' = :b AND (data->>'c')::numeric < :d", - Find.byFields(tbl, listOf(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))), + "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" ) } @@ -68,8 +66,8 @@ class FindTest { fun byFieldsSQLite() { Configuration.dialectValue = Dialect.SQLITE assertEquals( - "SELECT data FROM $tbl WHERE data->>'a' = :b AND data->>'c' < :d", - Find.byFields(tbl, listOf(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))), + "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" ) } @@ -79,7 +77,7 @@ class FindTest { fun byContainsPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "SELECT data FROM $tbl WHERE data @> :criteria", Find.byContains(tbl), + "SELECT data FROM $TEST_TABLE WHERE data @> :criteria", FindQuery.byContains(TEST_TABLE), "Find query not constructed correctly" ) } @@ -88,7 +86,7 @@ class FindTest { @DisplayName("byContains fails (SQLite)") fun byContainsSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Find.byContains(tbl) } + assertThrows { FindQuery.byContains(TEST_TABLE) } } @Test @@ -96,7 +94,8 @@ class FindTest { fun byJsonPathPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "SELECT data FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", Find.byJsonPath(tbl), + "SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)", + FindQuery.byJsonPath(TEST_TABLE), "Find query not constructed correctly" ) } @@ -105,6 +104,6 @@ class FindTest { @DisplayName("byJsonPath fails (SQLite)") fun byJsonPathSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Find.byJsonPath(tbl) } + assertThrows { FindQuery.byJsonPath(TEST_TABLE) } } } diff --git a/src/jvm/src/test/kotlin/query/PatchTest.kt b/src/jvm/src/test/kotlin/query/PatchQueryTest.kt similarity index 80% rename from src/jvm/src/test/kotlin/query/PatchTest.kt rename to src/jvm/src/test/kotlin/query/PatchQueryTest.kt index fd0cfad..0afcdf8 100644 --- a/src/jvm/src/test/kotlin/query/PatchTest.kt +++ b/src/jvm/src/test/kotlin/query/PatchQueryTest.kt @@ -13,8 +13,8 @@ import kotlin.test.assertEquals /** * Unit tests for the `Patch` object */ -@DisplayName("Kotlin | Common | Query: Patch") -class PatchTest { +@DisplayName("JVM | Kotlin | Query | PatchQuery") +class PatchQueryTest { /** Test table name */ private val tbl = "test_table" @@ -33,7 +33,7 @@ class PatchTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data || :data WHERE data->>'id' = :id", - Patch.byId(tbl), "Patch query not constructed correctly" + PatchQuery.byId(tbl), "Patch query not constructed correctly" ) } @@ -43,7 +43,7 @@ class PatchTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "UPDATE $tbl SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id", - Patch.byId(tbl), "Patch query not constructed correctly" + PatchQuery.byId(tbl), "Patch query not constructed correctly" ) } @@ -53,7 +53,7 @@ class PatchTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data || :data WHERE data->>'z' = :y", - Patch.byFields(tbl, listOf(Field.equal("z", "", ":y"))), + PatchQuery.byFields(tbl, listOf(Field.equal("z", "", ":y"))), "Patch query not constructed correctly" ) } @@ -64,7 +64,7 @@ class PatchTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "UPDATE $tbl SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y", - Patch.byFields(tbl, listOf(Field.equal("z", "", ":y"))), + PatchQuery.byFields(tbl, listOf(Field.equal("z", "", ":y"))), "Patch query not constructed correctly" ) } @@ -74,7 +74,7 @@ class PatchTest { fun byContainsPostgres() { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( - "UPDATE $tbl SET data = data || :data WHERE data @> :criteria", Patch.byContains(tbl), + "UPDATE $tbl SET data = data || :data WHERE data @> :criteria", PatchQuery.byContains(tbl), "Patch query not constructed correctly" ) } @@ -83,7 +83,7 @@ class PatchTest { @DisplayName("byContains fails (SQLite)") fun byContainsSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Patch.byContains(tbl) } + assertThrows { PatchQuery.byContains(tbl) } } @Test @@ -92,7 +92,7 @@ class PatchTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)", - Patch.byJsonPath(tbl), "Patch query not constructed correctly" + PatchQuery.byJsonPath(tbl), "Patch query not constructed correctly" ) } @@ -100,6 +100,6 @@ class PatchTest { @DisplayName("byJsonPath fails (SQLite)") fun byJsonPathSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { Patch.byJsonPath(tbl) } + assertThrows { PatchQuery.byJsonPath(tbl) } } } diff --git a/src/jvm/src/test/kotlin/query/QueryTest.kt b/src/jvm/src/test/kotlin/query/QueryTest.kt index 6788ac4..c16b31a 100644 --- a/src/jvm/src/test/kotlin/query/QueryTest.kt +++ b/src/jvm/src/test/kotlin/query/QueryTest.kt @@ -10,9 +10,9 @@ import solutions.bitbadger.documents.FieldMatch import kotlin.test.assertEquals /** - * Unit tests for the top-level query functions + * Unit tests for the package-level query functions */ -@DisplayName("Kotlin | Common | Query") +@DisplayName("JVM | Kotlin | Query | Package Functions") class QueryTest { /** diff --git a/src/jvm/src/test/kotlin/query/RemoveFieldsTest.kt b/src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt similarity index 82% rename from src/jvm/src/test/kotlin/query/RemoveFieldsTest.kt rename to src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt index 34fce2d..d042f17 100644 --- a/src/jvm/src/test/kotlin/query/RemoveFieldsTest.kt +++ b/src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt @@ -10,8 +10,8 @@ import kotlin.test.assertEquals /** * Unit tests for the `RemoveFields` object */ -@DisplayName("Kotlin | Common | Query: RemoveFields") -class RemoveFieldsTest { +@DisplayName("JVM | Kotlin | Query | RemoveFieldsQuery") +class RemoveFieldsQueryTest { /** Test table name */ private val tbl = "test_table" @@ -30,7 +30,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data - :name::text[] WHERE data->>'id' = :id", - RemoveFields.byId(tbl, listOf(Parameter(":name", ParameterType.STRING, "{a,z}"))), + RemoveFieldsQuery.byId(tbl, listOf(Parameter(":name", ParameterType.STRING, "{a,z}"))), "Remove Fields query not constructed correctly" ) } @@ -41,7 +41,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "UPDATE $tbl SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id", - RemoveFields.byId( + RemoveFieldsQuery.byId( tbl, listOf( Parameter(":name0", ParameterType.STRING, "a"), @@ -58,7 +58,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data - :name::text[] WHERE data->>'f' > :g", - RemoveFields.byFields( + RemoveFieldsQuery.byFields( tbl, listOf(Parameter(":name", ParameterType.STRING, "{b,c}")), listOf(Field.greater("f", "", ":g")) @@ -73,7 +73,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.SQLITE assertEquals( "UPDATE $tbl SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g", - RemoveFields.byFields( + RemoveFieldsQuery.byFields( tbl, listOf(Parameter(":name0", ParameterType.STRING, "b"), Parameter(":name1", ParameterType.STRING, "c")), listOf(Field.greater("f", "", ":g")) @@ -88,7 +88,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data - :name::text[] WHERE data @> :criteria", - RemoveFields.byContains(tbl, listOf(Parameter(":name", ParameterType.STRING, "{m,n}"))), + RemoveFieldsQuery.byContains(tbl, listOf(Parameter(":name", ParameterType.STRING, "{m,n}"))), "Remove Field query not constructed correctly" ) } @@ -97,7 +97,7 @@ class RemoveFieldsTest { @DisplayName("byContains fails (SQLite)") fun byContainsSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { RemoveFields.byContains(tbl, listOf()) } + assertThrows { RemoveFieldsQuery.byContains(tbl, listOf()) } } @Test @@ -106,7 +106,7 @@ class RemoveFieldsTest { Configuration.dialectValue = Dialect.POSTGRESQL assertEquals( "UPDATE $tbl SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)", - RemoveFields.byJsonPath(tbl, listOf(Parameter(":name", ParameterType.STRING, "{o,p}"))), + RemoveFieldsQuery.byJsonPath(tbl, listOf(Parameter(":name", ParameterType.STRING, "{o,p}"))), "Remove Field query not constructed correctly" ) } @@ -115,6 +115,6 @@ class RemoveFieldsTest { @DisplayName("byJsonPath fails (SQLite)") fun byJsonPathSQLite() { Configuration.dialectValue = Dialect.SQLITE - assertThrows { RemoveFields.byJsonPath(tbl, listOf()) } + assertThrows { RemoveFieldsQuery.byJsonPath(tbl, listOf()) } } } \ No newline at end of file diff --git a/src/jvm/src/test/kotlin/query/WhereTest.kt b/src/jvm/src/test/kotlin/query/WhereTest.kt index cdac60d..95d68d8 100644 --- a/src/jvm/src/test/kotlin/query/WhereTest.kt +++ b/src/jvm/src/test/kotlin/query/WhereTest.kt @@ -10,7 +10,7 @@ import kotlin.test.assertEquals /** * Unit tests for the `Where` object */ -@DisplayName("Kotlin | Common | Query: Where") +@DisplayName("JVM | Kotlin | Query | Where") class WhereTest { /** diff --git a/src/jvm/src/test/kotlin/support/Types.kt b/src/jvm/src/test/kotlin/support/Types.kt index 97385e2..d56f23b 100644 --- a/src/jvm/src/test/kotlin/support/Types.kt +++ b/src/jvm/src/test/kotlin/support/Types.kt @@ -1,22 +1,18 @@ package solutions.bitbadger.documents.support -import kotlinx.serialization.Serializable import solutions.bitbadger.documents.extensions.insert /** 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()) @@ -31,7 +27,6 @@ data class ArrayDocument(val id: String, val values: List) { } } -@Serializable data class JsonDocument(val id: String, val value: String = "", val numValue: Int = 0, val sub: SubDocument? = null) { constructor() : this("") @@ -50,3 +45,11 @@ data class JsonDocument(val id: String, val value: String = "", val numValue: In testDocuments.forEach { db.conn.insert(tableName, it) } } } + +// Test classes for AutoId generation + +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/kotlin/pom.xml b/src/kotlin/pom.xml index 9889629..5549ee3 100644 --- a/src/kotlin/pom.xml +++ b/src/kotlin/pom.xml @@ -30,8 +30,6 @@ solutions.bitbadger.documents jvm 4.0.0-alpha1-SNAPSHOT - system - ${project.basedir}/../jvm/target/jvm-4.0.0-alpha1-SNAPSHOT.jar jar diff --git a/src/kotlin/src/main/kotlin/Count.kt b/src/kotlin/src/main/kotlin/Count.kt index d8fbf7b..c3b3ef5 100644 --- a/src/kotlin/src/main/kotlin/Count.kt +++ b/src/kotlin/src/main/kotlin/Count.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.kotlin import solutions.bitbadger.documents.* import solutions.bitbadger.documents.kotlin.extensions.* -import solutions.bitbadger.documents.query.Count +import solutions.bitbadger.documents.query.CountQuery import java.sql.Connection @@ -19,7 +19,7 @@ object Count { * @return A count of the documents in the table */ fun all(tableName: String, conn: Connection) = - conn.customScalar(Count.all(tableName), mapFunc = Results::toCount) + conn.customScalar(CountQuery.all(tableName), mapFunc = Results::toCount) /** * Count all documents in the table @@ -47,7 +47,7 @@ object Count { ): Long { val named = Parameters.nameFields(fields) return conn.customScalar( - Count.byFields(tableName, named, howMatched), + CountQuery.byFields(tableName, named, howMatched), Parameters.addFields(named), Results::toCount ) @@ -74,7 +74,7 @@ object Count { * @throws DocumentException If called on a SQLite connection */ inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = - conn.customScalar(Count.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Results::toCount) + conn.customScalar(CountQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Results::toCount) /** * Count documents using a JSON containment query (PostgreSQL only) @@ -98,7 +98,7 @@ object Count { */ fun byJsonPath(tableName: String, path: String, conn: Connection) = conn.customScalar( - Count.byJsonPath(tableName), + CountQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), Results::toCount ) diff --git a/src/kotlin/src/main/kotlin/Delete.kt b/src/kotlin/src/main/kotlin/Delete.kt index e670d48..9cdaa42 100644 --- a/src/kotlin/src/main/kotlin/Delete.kt +++ b/src/kotlin/src/main/kotlin/Delete.kt @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.kotlin import solutions.bitbadger.documents.* import solutions.bitbadger.documents.jvm.Delete as JvmDelete import solutions.bitbadger.documents.kotlin.extensions.* -import solutions.bitbadger.documents.query.Delete +import solutions.bitbadger.documents.query.DeleteQuery import java.sql.Connection /** @@ -60,7 +60,7 @@ object Delete { * @throws DocumentException If called on a SQLite connection */ inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = - conn.customNonQuery(Delete.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) + conn.customNonQuery(DeleteQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) /** * Delete documents using a JSON containment query (PostgreSQL only) diff --git a/src/kotlin/src/main/kotlin/Document.kt b/src/kotlin/src/main/kotlin/Document.kt index 2c2547e..4b391b3 100644 --- a/src/kotlin/src/main/kotlin/Document.kt +++ b/src/kotlin/src/main/kotlin/Document.kt @@ -5,7 +5,7 @@ import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.extensions.customNonQuery -import solutions.bitbadger.documents.query.Document +import solutions.bitbadger.documents.query.DocumentQuery import solutions.bitbadger.documents.query.Where import solutions.bitbadger.documents.query.statementWhere import java.sql.Connection @@ -25,7 +25,7 @@ object Document { inline fun insert(tableName: String, document: TDoc, conn: Connection) { val strategy = Configuration.autoIdStrategy val query = if (strategy == AutoId.DISABLED) { - Document.insert(tableName) + DocumentQuery.insert(tableName) } else { val idField = Configuration.idField val dialect = Configuration.dialect("Create auto-ID insert query") @@ -52,7 +52,7 @@ object Document { ":data" } - Document.insert(tableName).replace(":data", dataParam) + DocumentQuery.insert(tableName).replace(":data", dataParam) } conn.customNonQuery(query, listOf(Parameters.json(":data", document))) } @@ -74,7 +74,7 @@ object Document { * @param conn The connection on which the query should be executed */ inline fun save(tableName: String, document: TDoc, conn: Connection) = - conn.customNonQuery(Document.save(tableName), listOf(Parameters.json(":data", document))) + 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") @@ -95,7 +95,7 @@ object Document { */ inline fun update(tableName: String, docId: TKey, document: TDoc, conn: Connection) = conn.customNonQuery( - statementWhere(Document.update(tableName), Where.byId(":id", docId)), + statementWhere(DocumentQuery.update(tableName), Where.byId(":id", docId)), Parameters.addFields( listOf(Field.equal(Configuration.idField, docId, ":id")), mutableListOf(Parameters.json(":data", document)) diff --git a/src/kotlin/src/main/kotlin/Exists.kt b/src/kotlin/src/main/kotlin/Exists.kt index e253bbb..4cbc05b 100644 --- a/src/kotlin/src/main/kotlin/Exists.kt +++ b/src/kotlin/src/main/kotlin/Exists.kt @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.kotlin import solutions.bitbadger.documents.* import solutions.bitbadger.documents.jvm.Exists as JvmExists import solutions.bitbadger.documents.kotlin.extensions.* -import solutions.bitbadger.documents.query.Exists +import solutions.bitbadger.documents.query.ExistsQuery import java.sql.Connection /** @@ -66,7 +66,7 @@ object Exists { */ inline fun byContains(tableName: String, criteria: TContains, conn: Connection) = conn.customScalar( - Exists.byContains(tableName), + ExistsQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Results::toExists ) diff --git a/src/kotlin/src/main/kotlin/Find.kt b/src/kotlin/src/main/kotlin/Find.kt index a9ecf18..37af42a 100644 --- a/src/kotlin/src/main/kotlin/Find.kt +++ b/src/kotlin/src/main/kotlin/Find.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.kotlin import solutions.bitbadger.documents.* import solutions.bitbadger.documents.kotlin.extensions.* -import solutions.bitbadger.documents.query.Find +import solutions.bitbadger.documents.query.FindQuery import solutions.bitbadger.documents.query.orderBy import java.sql.Connection @@ -20,7 +20,7 @@ object Find { * @return A list of documents from the given table */ inline fun all(tableName: String, orderBy: Collection>? = null, conn: Connection) = - conn.customList(Find.all(tableName) + (orderBy?.let(::orderBy) ?: ""), mapFunc = Results::fromData) + conn.customList(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), mapFunc = Results::fromData) /** * Retrieve all documents in the given table @@ -52,7 +52,7 @@ object Find { */ inline fun byId(tableName: String, docId: TKey, conn: Connection) = conn.customSingle( - Find.byId(tableName, docId), + FindQuery.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), Results::fromData ) @@ -86,7 +86,7 @@ object Find { ): List { val named = Parameters.nameFields(fields) return conn.customList( - Find.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), Parameters.addFields(named), Results::fromData ) @@ -158,7 +158,7 @@ object Find { conn: Connection ) = conn.customList( - Find.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameters.json(":criteria", criteria)), Results::fromData ) @@ -223,7 +223,7 @@ object Find { conn: Connection ) = conn.customList( - Find.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameter(":path", ParameterType.STRING, path)), Results::fromData ) @@ -271,7 +271,7 @@ object Find { ): TDoc? { val named = Parameters.nameFields(fields) return conn.customSingle( - Find.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), Parameters.addFields(named), Results::fromData ) @@ -328,7 +328,7 @@ object Find { conn: Connection ) = conn.customSingle( - Find.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameters.json(":criteria", criteria)), Results::fromData ) @@ -382,7 +382,7 @@ object Find { conn: Connection ) = conn.customSingle( - Find.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), + FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameter(":path", ParameterType.STRING, path)), Results::fromData ) diff --git a/src/kotlin/src/main/kotlin/Patch.kt b/src/kotlin/src/main/kotlin/Patch.kt index 097eb2d..e4e4041 100644 --- a/src/kotlin/src/main/kotlin/Patch.kt +++ b/src/kotlin/src/main/kotlin/Patch.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.kotlin import solutions.bitbadger.documents.* import solutions.bitbadger.documents.kotlin.extensions.* -import solutions.bitbadger.documents.query.Patch +import solutions.bitbadger.documents.query.PatchQuery import java.sql.Connection /** @@ -20,7 +20,7 @@ object Patch { */ inline fun byId(tableName: String, docId: TKey, patch: TPatch, conn: Connection) = conn.customNonQuery( - Patch.byId(tableName, docId), + PatchQuery.byId(tableName, docId), Parameters.addFields( listOf(Field.equal(Configuration.idField, docId, ":id")), mutableListOf(Parameters.json(":data", patch)) @@ -55,7 +55,7 @@ object Patch { ) { val named = Parameters.nameFields(fields) conn.customNonQuery( - Patch.byFields(tableName, named, howMatched), Parameters.addFields( + PatchQuery.byFields(tableName, named, howMatched), Parameters.addFields( named, mutableListOf(Parameters.json(":data", patch)) ) @@ -94,7 +94,7 @@ object Patch { conn: Connection ) = conn.customNonQuery( - Patch.byContains(tableName), + PatchQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch)) ) @@ -120,7 +120,7 @@ object Patch { */ inline fun byJsonPath(tableName: String, path: String, patch: TPatch, conn: Connection) = conn.customNonQuery( - Patch.byJsonPath(tableName), + PatchQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch)) ) diff --git a/src/kotlin/src/main/kotlin/RemoveFields.kt b/src/kotlin/src/main/kotlin/RemoveFields.kt index f1c7675..f24d855 100644 --- a/src/kotlin/src/main/kotlin/RemoveFields.kt +++ b/src/kotlin/src/main/kotlin/RemoveFields.kt @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.kotlin import solutions.bitbadger.documents.* import solutions.bitbadger.documents.jvm.RemoveFields as JvmRemoveFields import solutions.bitbadger.documents.kotlin.extensions.* -import solutions.bitbadger.documents.query.RemoveFields +import solutions.bitbadger.documents.query.RemoveFieldsQuery import java.sql.Connection /** @@ -83,7 +83,7 @@ object RemoveFields { ) { val nameParams = Parameters.fieldNames(toRemove) conn.customNonQuery( - RemoveFields.byContains(tableName, nameParams), + RemoveFieldsQuery.byContains(tableName, nameParams), listOf(Parameters.json(":criteria", criteria), *nameParams.toTypedArray()) ) } diff --git a/src/kotlin/src/test/kotlin/Types.kt b/src/kotlin/src/test/kotlin/Types.kt new file mode 100644 index 0000000..51d0b38 --- /dev/null +++ b/src/kotlin/src/test/kotlin/Types.kt @@ -0,0 +1,52 @@ +package solutions.bitbadger.documents.kotlin + +import kotlinx.serialization.Serializable +import solutions.bitbadger.documents.extensions.insert + +/** 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) } + } +} -- 2.47.2 From eda312fe3bac573954da21d822d919572bf4c1a8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 15 Mar 2025 14:09:05 -0400 Subject: [PATCH 47/88] WIP on Java query tests --- .../documents/java/query/CountQueryTest.java | 87 ++++++++++++ .../java/query/DefinitionQueryTest.java | 133 ++++++++++++++++++ .../src/test/kotlin/query/CountQueryTest.kt | 53 ++++--- .../test/kotlin/query/DefinitionQueryTest.kt | 66 +++++---- .../src/test/kotlin/support/ForceDialect.kt | 24 ++++ 5 files changed, 304 insertions(+), 59 deletions(-) create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java create mode 100644 src/jvm/src/test/kotlin/support/ForceDialect.kt diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java new file mode 100644 index 0000000..1389ba9 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java @@ -0,0 +1,87 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.query.CountQuery; +import solutions.bitbadger.documents.support.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Count` object + */ +@DisplayName("JVM | Java | Query | CountQuery") +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() { + 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() { + 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/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java new file mode 100644 index 0000000..f5e0cd2 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java @@ -0,0 +1,133 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.Dialect; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.DocumentIndex; +import solutions.bitbadger.documents.query.DefinitionQuery; +import solutions.bitbadger.documents.support.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Definition` object + */ +@DisplayName("JVM | Java | Query | DefinitionQuery") +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/jvm/src/test/kotlin/query/CountQueryTest.kt b/src/jvm/src/test/kotlin/query/CountQueryTest.kt index 9d2de61..9c3d413 100644 --- a/src/jvm/src/test/kotlin/query/CountQueryTest.kt +++ b/src/jvm/src/test/kotlin/query/CountQueryTest.kt @@ -4,10 +4,10 @@ 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.Configuration -import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals /** @@ -16,75 +16,72 @@ import kotlin.test.assertEquals @DisplayName("JVM | Kotlin | Query | CountQuery") class CountQueryTest { - /** Test table name */ - private val tbl = "test_table" - /** * Clear the connection string (resets Dialect) */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test @DisplayName("all generates correctly") fun all() = - assertEquals("SELECT COUNT(*) AS it FROM $tbl", CountQuery.all(tbl), "Count query not constructed correctly") + assertEquals("SELECT COUNT(*) AS it FROM $TEST_TABLE", CountQuery.all(TEST_TABLE), "Count query not constructed correctly") @Test - @DisplayName("byFields generates correctly (PostgreSQL)") + @DisplayName("byFields generates correctly | PostgreSQL") fun byFieldsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0", - CountQuery.byFields(tbl, listOf(Field.equal("test", "", ":field0"))), + "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 (PostgreSQL)") + @DisplayName("byFields generates correctly | SQLite") fun byFieldsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0", - CountQuery.byFields(tbl, listOf(Field.equal("test", "", ":field0"))), + "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)") + @DisplayName("byContains generates correctly | PostgreSQL") fun byContainsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", CountQuery.byContains(tbl), + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria", CountQuery.byContains(TEST_TABLE), "Count query not constructed correctly" ) } @Test - @DisplayName("byContains fails (SQLite)") + @DisplayName("byContains fails | SQLite") fun byContainsSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { CountQuery.byContains(tbl) } + ForceDialect.sqlite() + assertThrows { CountQuery.byContains(TEST_TABLE) } } @Test - @DisplayName("byJsonPath generates correctly (PostgreSQL)") + @DisplayName("byJsonPath generates correctly | PostgreSQL") fun byJsonPathPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", - CountQuery.byJsonPath(tbl), "Count query not constructed correctly" + "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)") + @DisplayName("byJsonPath fails | SQLite") fun byJsonPathSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { CountQuery.byJsonPath(tbl) } + ForceDialect.sqlite() + assertThrows { CountQuery.byJsonPath(TEST_TABLE) } } } diff --git a/src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt b/src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt index 71d1fef..a0ccf41 100644 --- a/src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt +++ b/src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt @@ -4,10 +4,11 @@ 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.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals /** @@ -16,15 +17,12 @@ import kotlin.test.assertEquals @DisplayName("JVM | Kotlin | Query | DefinitionQuery") class DefinitionQueryTest { - /** Test table name */ - private val tbl = "test_table" - /** * Clear the connection string (resets Dialect) */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test @@ -36,23 +34,29 @@ class DefinitionQueryTest { ) @Test - @DisplayName("ensureTable generates correctly (PostgreSQL)") + @DisplayName("ensureTable generates correctly | PostgreSQL") fun ensureTablePostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL - assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data JSONB NOT NULL)", DefinitionQuery.ensureTable(tbl)) + ForceDialect.postgres() + assertEquals( + "CREATE TABLE IF NOT EXISTS $TEST_TABLE (data JSONB NOT NULL)", + DefinitionQuery.ensureTable(TEST_TABLE) + ) } @Test - @DisplayName("ensureTable generates correctly (SQLite)") + @DisplayName("ensureTable generates correctly | SQLite") fun ensureTableSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertEquals("CREATE TABLE IF NOT EXISTS $tbl (data TEXT NOT NULL)", DefinitionQuery.ensureTable(tbl)) + 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(tbl) } + assertThrows { DefinitionQuery.ensureTable(TEST_TABLE) } } @Test @@ -68,8 +72,8 @@ class DefinitionQueryTest { @DisplayName("ensureKey generates correctly without schema") fun ensureKeyWithoutSchema() = assertEquals( - "CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))", - DefinitionQuery.ensureKey(tbl, Dialect.SQLITE), + "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" ) @@ -86,49 +90,49 @@ class DefinitionQueryTest { ) @Test - @DisplayName("ensureIndexOn generates nested PostgreSQL field") + @DisplayName("ensureIndexOn generates nested field | PostgreSQL") fun ensureIndexOnNestedPostgres() = assertEquals( - "CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))", - DefinitionQuery.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL), + "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 SQLite field") + @DisplayName("ensureIndexOn generates nested field | SQLite") fun ensureIndexOnNestedSQLite() = assertEquals( - "CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))", - DefinitionQuery.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE), + "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 for PostgreSQL") + @DisplayName("ensureDocumentIndexOn generates Full | PostgreSQL") fun ensureDocumentIndexOnFullPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tbl USING GIN (data)", - DefinitionQuery.ensureDocumentIndexOn(tbl, DocumentIndex.FULL), + "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 for PostgreSQL") + @DisplayName("ensureDocumentIndexOn generates Optimized | PostgreSQL") fun ensureDocumentIndexOnOptimizedPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tbl USING GIN (data jsonb_path_ops)", - DefinitionQuery.ensureDocumentIndexOn(tbl, DocumentIndex.OPTIMIZED), + "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 for SQLite") + @DisplayName("ensureDocumentIndexOn fails | SQLite") fun ensureDocumentIndexOnFailsSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { DefinitionQuery.ensureDocumentIndexOn(tbl, DocumentIndex.FULL) } + ForceDialect.sqlite() + assertThrows { DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) } } } diff --git a/src/jvm/src/test/kotlin/support/ForceDialect.kt b/src/jvm/src/test/kotlin/support/ForceDialect.kt new file mode 100644 index 0000000..681361c --- /dev/null +++ b/src/jvm/src/test/kotlin/support/ForceDialect.kt @@ -0,0 +1,24 @@ +package solutions.bitbadger.documents.support + +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 -- 2.47.2 From 9f7f2d7859475f85de66c06d523409a92037849d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 16 Mar 2025 07:46:47 -0400 Subject: [PATCH 48/88] More WIP on Java query tests --- src/jvm/src/main/kotlin/Field.kt | 4 + src/jvm/src/main/kotlin/query/CountQuery.kt | 2 + src/jvm/src/main/kotlin/query/DeleteQuery.kt | 4 + src/jvm/src/main/kotlin/query/ExistsQuery.kt | 4 + src/jvm/src/main/kotlin/query/FindQuery.kt | 4 + src/jvm/src/main/kotlin/query/Query.kt | 10 +- src/jvm/src/main/kotlin/query/Where.kt | 5 + .../bitbadger/documents/java/FieldTest.java | 193 +++++++++--------- .../documents/java/query/CountQueryTest.java | 6 +- .../java/query/DefinitionQueryTest.java | 2 +- .../documents/java/query/DeleteQueryTest.java | 94 +++++++++ .../java/query/DocumentQueryTest.java | 134 ++++++++++++ .../documents/java/query/ExistsQueryTest.java | 97 +++++++++ .../documents/java/query/FindQueryTest.java | 102 +++++++++ .../documents/java/query/PatchQueryTest.java | 97 +++++++++ .../documents/java/query/QueryUtilsTest.java | 166 +++++++++++++++ .../java/query/RemoveFieldsQueryTest.java | 110 ++++++++++ .../src/test/kotlin/query/DeleteQueryTest.kt | 86 ++++---- .../test/kotlin/query/DocumentQueryTest.kt | 91 +++++---- .../src/test/kotlin/query/ExistsQueryTest.kt | 68 +++--- .../src/test/kotlin/query/FindQueryTest.kt | 37 ++-- .../src/test/kotlin/query/PatchQueryTest.kt | 67 +++--- src/jvm/src/test/kotlin/query/QueryTest.kt | 48 ++--- .../kotlin/query/RemoveFieldsQueryTest.kt | 69 +++---- 24 files changed, 1166 insertions(+), 334 deletions(-) create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DeleteQueryTest.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DocumentQueryTest.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/ExistsQueryTest.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/FindQueryTest.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/PatchQueryTest.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/QueryUtilsTest.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/RemoveFieldsQueryTest.java diff --git a/src/jvm/src/main/kotlin/Field.kt b/src/jvm/src/main/kotlin/Field.kt index d15a838..9a4c899 100644 --- a/src/jvm/src/main/kotlin/Field.kt +++ b/src/jvm/src/main/kotlin/Field.kt @@ -1,5 +1,7 @@ package solutions.bitbadger.documents +import kotlin.jvm.Throws + /** * A field and its comparison * @@ -44,6 +46,7 @@ class Field private constructor( * @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) @@ -65,6 +68,7 @@ class Field private constructor( * @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") diff --git a/src/jvm/src/main/kotlin/query/CountQuery.kt b/src/jvm/src/main/kotlin/query/CountQuery.kt index 82b97b4..6439f6c 100644 --- a/src/jvm/src/main/kotlin/query/CountQuery.kt +++ b/src/jvm/src/main/kotlin/query/CountQuery.kt @@ -28,7 +28,9 @@ object CountQuery { * @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) = diff --git a/src/jvm/src/main/kotlin/query/DeleteQuery.kt b/src/jvm/src/main/kotlin/query/DeleteQuery.kt index 8241a6e..7dd1b07 100644 --- a/src/jvm/src/main/kotlin/query/DeleteQuery.kt +++ b/src/jvm/src/main/kotlin/query/DeleteQuery.kt @@ -27,7 +27,9 @@ object DeleteQuery { * @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) = @@ -40,7 +42,9 @@ object DeleteQuery { * @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) = diff --git a/src/jvm/src/main/kotlin/query/ExistsQuery.kt b/src/jvm/src/main/kotlin/query/ExistsQuery.kt index a77457e..e15f5ca 100644 --- a/src/jvm/src/main/kotlin/query/ExistsQuery.kt +++ b/src/jvm/src/main/kotlin/query/ExistsQuery.kt @@ -26,7 +26,9 @@ object ExistsQuery { * @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) = @@ -39,7 +41,9 @@ object ExistsQuery { * @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) = diff --git a/src/jvm/src/main/kotlin/query/FindQuery.kt b/src/jvm/src/main/kotlin/query/FindQuery.kt index 3606a2b..13077bf 100644 --- a/src/jvm/src/main/kotlin/query/FindQuery.kt +++ b/src/jvm/src/main/kotlin/query/FindQuery.kt @@ -28,7 +28,9 @@ object FindQuery { * @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) = @@ -41,7 +43,9 @@ object FindQuery { * @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) = diff --git a/src/jvm/src/main/kotlin/query/Query.kt b/src/jvm/src/main/kotlin/query/Query.kt index c79203e..9381e81 100644 --- a/src/jvm/src/main/kotlin/query/Query.kt +++ b/src/jvm/src/main/kotlin/query/Query.kt @@ -1,10 +1,8 @@ @file:JvmName("QueryUtils") package solutions.bitbadger.documents.query -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Dialect -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.* +import kotlin.jvm.Throws // ~~~ TOP-LEVEL FUNCTIONS FOR THE QUERY PACKAGE ~~~ @@ -24,7 +22,9 @@ fun statementWhere(statement: String, where: String) = * @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)) @@ -36,6 +36,7 @@ fun byId(statement: String, docId: TKey) = * @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)) @@ -47,6 +48,7 @@ fun byFields(statement: String, fields: Collection>, howMatched: FieldM * @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") diff --git a/src/jvm/src/main/kotlin/query/Where.kt b/src/jvm/src/main/kotlin/query/Where.kt index 626002e..da232ed 100644 --- a/src/jvm/src/main/kotlin/query/Where.kt +++ b/src/jvm/src/main/kotlin/query/Where.kt @@ -18,7 +18,9 @@ object Where { * @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) = @@ -29,7 +31,10 @@ object Where { * * @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) = diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java index 2a7572d..0c2f668 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java @@ -5,6 +5,7 @@ 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.support.ForceDialect; import java.util.Collection; import java.util.List; @@ -22,7 +23,7 @@ final public class FieldTest { */ @AfterEach public void cleanUp() { - Configuration.setConnectionString(null); + ForceDialect.none(); } // ~~~ INSTANCE METHODS ~~~ @@ -71,7 +72,7 @@ final public class FieldTest { } @Test - @DisplayName("path generates for simple unqualified PostgreSQL field") + @DisplayName("path generates for simple unqualified field | PostgreSQL") public void pathPostgresSimpleUnqualified() { assertEquals("data->>'SomethingCool'", Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL), @@ -79,7 +80,7 @@ final public class FieldTest { } @Test - @DisplayName("path generates for simple qualified PostgreSQL field") + @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), @@ -87,14 +88,14 @@ final public class FieldTest { } @Test - @DisplayName("path generates for nested unqualified PostgreSQL field") + @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 PostgreSQL field") + @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), @@ -102,14 +103,14 @@ final public class FieldTest { } @Test - @DisplayName("path generates for simple unqualified SQLite field") + @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 SQLite field") + @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), @@ -117,14 +118,14 @@ final public class FieldTest { } @Test - @DisplayName("path generates for nested unqualified SQLite field") + @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 SQLite field") + @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), @@ -132,190 +133,196 @@ final public class FieldTest { } @Test - @DisplayName("toWhere generates for exists w/o qualifier (PostgreSQL)") - public void toWhereExistsNoQualPostgres() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Configuration.setConnectionString(":postgresql:"); + @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() { - Configuration.setConnectionString(":sqlite:"); + @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() { - Field field = Field.equal("Test", 14); + 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"); @@ -326,7 +333,7 @@ final public class FieldTest { @Test @DisplayName("equal constructs a field w/ parameter name") public void equalParameterCtor() { - Field field = Field.equal("Test", 14, ":w"); + 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"); @@ -337,7 +344,7 @@ final public class FieldTest { @Test @DisplayName("greater constructs a field w/o parameter name") public void greaterCtor() { - Field field = Field.greater("Great", "night"); + 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"); @@ -348,7 +355,7 @@ final public class FieldTest { @Test @DisplayName("greater constructs a field w/ parameter name") public void greaterParameterCtor() { - Field field = Field.greater("Great", "night", ":yeah"); + 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"); @@ -359,7 +366,7 @@ final public class FieldTest { @Test @DisplayName("greaterOrEqual constructs a field w/o parameter name") public void greaterOrEqualCtor() { - Field field = Field.greaterOrEqual("Nice", 88L); + 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"); @@ -371,7 +378,7 @@ final public class FieldTest { @Test @DisplayName("greaterOrEqual constructs a field w/ parameter name") public void greaterOrEqualParameterCtor() { - Field field = Field.greaterOrEqual("Nice", 88L, ":nice"); + 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"); @@ -383,7 +390,7 @@ final public class FieldTest { @Test @DisplayName("less constructs a field w/o parameter name") public void lessCtor() { - Field field = Field.less("Lesser", "seven"); + 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"); @@ -394,7 +401,7 @@ final public class FieldTest { @Test @DisplayName("less constructs a field w/ parameter name") public void lessParameterCtor() { - Field field = Field.less("Lesser", "seven", ":max"); + 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"); @@ -405,7 +412,7 @@ final public class FieldTest { @Test @DisplayName("lessOrEqual constructs a field w/o parameter name") public void lessOrEqualCtor() { - Field field = Field.lessOrEqual("Nobody", "KNOWS"); + 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"); @@ -417,7 +424,7 @@ final public class FieldTest { @Test @DisplayName("lessOrEqual constructs a field w/ parameter name") public void lessOrEqualParameterCtor() { - Field field = Field.lessOrEqual("Nobody", "KNOWS", ":nope"); + 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"); @@ -429,7 +436,7 @@ final public class FieldTest { @Test @DisplayName("notEqual constructs a field w/o parameter name") public void notEqualCtor() { - Field field = Field.notEqual("Park", "here"); + 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"); @@ -440,7 +447,7 @@ final public class FieldTest { @Test @DisplayName("notEqual constructs a field w/ parameter name") public void notEqualParameterCtor() { - Field field = Field.notEqual("Park", "here", ":now"); + 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"); @@ -451,7 +458,7 @@ final public class FieldTest { @Test @DisplayName("between constructs a field w/o parameter name") public void betweenCtor() { - Field> field = Field.between("Age", 18, 49); + 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(), @@ -465,7 +472,7 @@ final public class FieldTest { @Test @DisplayName("between constructs a field w/ parameter name") public void betweenParameterCtor() { - Field> field = Field.between("Age", 18, 49, ":limit"); + 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(), @@ -479,7 +486,7 @@ final public class FieldTest { @Test @DisplayName("any constructs a field w/o parameter name") public void anyCtor() { - Field> field = Field.any("Here", List.of(8, 16, 32)); + 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(), @@ -491,7 +498,7 @@ final public class FieldTest { @Test @DisplayName("any constructs a field w/ parameter name") public void anyParameterCtor() { - Field> field = Field.any("Here", List.of(8, 16, 32), ":list"); + 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(), @@ -503,7 +510,7 @@ final public class FieldTest { @Test @DisplayName("inArray constructs a field w/o parameter name") public void inArrayCtor() { - Field>> field = Field.inArray("ArrayField", "table", List.of("z")); + 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(), @@ -517,7 +524,7 @@ final public class FieldTest { @Test @DisplayName("inArray constructs a field w/ parameter name") public void inArrayParameterCtor() { - Field>> field = Field.inArray("ArrayField", "table", List.of("z"), ":a"); + 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(), @@ -531,7 +538,7 @@ final public class FieldTest { @Test @DisplayName("exists constructs a field") public void existsCtor() { - Field field = Field.exists("Groovy"); + 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"); @@ -542,7 +549,7 @@ final public class FieldTest { @Test @DisplayName("notExists constructs a field") public void notExistsCtor() { - Field field = Field.notExists("Groovy"); + 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"); @@ -553,7 +560,7 @@ final public class FieldTest { @Test @DisplayName("named constructs a field") public void namedCtor() { - Field field = Field.named("Tacos"); + 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"); diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java index 1389ba9..02b916f 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java @@ -18,7 +18,7 @@ import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; * Unit tests for the `Count` object */ @DisplayName("JVM | Java | Query | CountQuery") -public class CountQueryTest { +final public class CountQueryTest { /** * Clear the connection string (resets Dialect) @@ -37,7 +37,7 @@ public class CountQueryTest { @Test @DisplayName("byFields generates correctly | PostgreSQL") - public void byFieldsPostgres() { + 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"))), @@ -46,7 +46,7 @@ public class CountQueryTest { @Test @DisplayName("byFields generates correctly | SQLite") - public void byFieldsSQLite() { + 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"))), diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java index f5e0cd2..3558285 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java @@ -19,7 +19,7 @@ import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; * Unit tests for the `Definition` object */ @DisplayName("JVM | Java | Query | DefinitionQuery") -public class DefinitionQueryTest { +final public class DefinitionQueryTest { /** * Clear the connection string (resets Dialect) diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DeleteQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DeleteQueryTest.java new file mode 100644 index 0000000..4922f66 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DeleteQueryTest.java @@ -0,0 +1,94 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.query.DeleteQuery; +import solutions.bitbadger.documents.support.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Delete` object + */ +@DisplayName("JVM | 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/jvm/src/test/java/solutions/bitbadger/documents/java/query/DocumentQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DocumentQueryTest.java new file mode 100644 index 0000000..26307f5 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DocumentQueryTest.java @@ -0,0 +1,134 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.AutoId; +import solutions.bitbadger.documents.Configuration; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.query.DocumentQuery; +import solutions.bitbadger.documents.support.ForceDialect; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Document` object + */ +@DisplayName("JVM | 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/jvm/src/test/java/solutions/bitbadger/documents/java/query/ExistsQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/ExistsQueryTest.java new file mode 100644 index 0000000..82a6c52 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/ExistsQueryTest.java @@ -0,0 +1,97 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.query.ExistsQuery; +import solutions.bitbadger.documents.support.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.support.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Exists` object + */ +@DisplayName("JVM | 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/jvm/src/test/java/solutions/bitbadger/documents/java/query/FindQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/FindQueryTest.java new file mode 100644 index 0000000..bcc717e --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/FindQueryTest.java @@ -0,0 +1,102 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.query.FindQuery; +import solutions.bitbadger.documents.support.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Find` object + */ +@DisplayName("JVM | 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/jvm/src/test/java/solutions/bitbadger/documents/java/query/PatchQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/PatchQueryTest.java new file mode 100644 index 0000000..56afbdc --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/PatchQueryTest.java @@ -0,0 +1,97 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.query.PatchQuery; +import solutions.bitbadger.documents.support.ForceDialect; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `Patch` object + */ +@DisplayName("JVM | 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/jvm/src/test/java/solutions/bitbadger/documents/java/query/QueryUtilsTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/QueryUtilsTest.java new file mode 100644 index 0000000..f815db0 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/QueryUtilsTest.java @@ -0,0 +1,166 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.Dialect; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.FieldMatch; +import solutions.bitbadger.documents.query.QueryUtils; +import solutions.bitbadger.documents.support.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("JVM | 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/jvm/src/test/java/solutions/bitbadger/documents/java/query/RemoveFieldsQueryTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/RemoveFieldsQueryTest.java new file mode 100644 index 0000000..027352e --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/RemoveFieldsQueryTest.java @@ -0,0 +1,110 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.Parameter; +import solutions.bitbadger.documents.ParameterType; +import solutions.bitbadger.documents.query.RemoveFieldsQuery; +import solutions.bitbadger.documents.support.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.support.TypesKt.TEST_TABLE; + +/** + * Unit tests for the `RemoveFields` object + */ +@DisplayName("JVM | 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/jvm/src/test/kotlin/query/DeleteQueryTest.kt b/src/jvm/src/test/kotlin/query/DeleteQueryTest.kt index 330b816..cc56578 100644 --- a/src/jvm/src/test/kotlin/query/DeleteQueryTest.kt +++ b/src/jvm/src/test/kotlin/query/DeleteQueryTest.kt @@ -4,10 +4,10 @@ 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.Configuration -import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals /** @@ -16,87 +16,89 @@ import kotlin.test.assertEquals @DisplayName("JVM | Kotlin | Query | DeleteQuery") class DeleteQueryTest { - /** Test table name */ - private val tbl = "test_table" - /** * Clear the connection string (resets Dialect) */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test - @DisplayName("byId generates correctly (PostgreSQL)") + @DisplayName("byId generates correctly | PostgreSQL") fun byIdPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "DELETE FROM $tbl WHERE data->>'id' = :id", - DeleteQuery.byId(tbl), "Delete query not constructed correctly" + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", + DeleteQuery.byId(TEST_TABLE), "Delete query not constructed correctly" ) } @Test - @DisplayName("byId generates correctly (SQLite)") + @DisplayName("byId generates correctly | SQLite") fun byIdSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "DELETE FROM $tbl WHERE data->>'id' = :id", - DeleteQuery.byId(tbl), "Delete query not constructed correctly" + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", + DeleteQuery.byId(TEST_TABLE), "Delete query not constructed correctly" ) } @Test - @DisplayName("byFields generates correctly (PostgreSQL)") + @DisplayName("byFields generates correctly | PostgreSQL") fun byFieldsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "DELETE FROM $tbl WHERE data->>'a' = :b", DeleteQuery.byFields(tbl, listOf(Field.equal("a", "", ":b"))), + "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)") + @DisplayName("byFields generates correctly | SQLite") fun byFieldsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "DELETE FROM $tbl WHERE data->>'a' = :b", DeleteQuery.byFields(tbl, listOf(Field.equal("a", "", ":b"))), + "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)") + @DisplayName("byContains generates correctly | PostgreSQL") fun byContainsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "DELETE FROM $tbl WHERE data @> :criteria", DeleteQuery.byContains(tbl), "Delete query not constructed correctly" - ) - } - - @Test - @DisplayName("byContains fails (SQLite)") - fun byContainsSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { DeleteQuery.byContains(tbl) } - } - - @Test - @DisplayName("byJsonPath generates correctly (PostgreSQL)") - fun byJsonPathPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL - assertEquals( - "DELETE FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)", DeleteQuery.byJsonPath(tbl), + "DELETE FROM $TEST_TABLE WHERE data @> :criteria", + DeleteQuery.byContains(TEST_TABLE), "Delete query not constructed correctly" ) } @Test - @DisplayName("byJsonPath fails (SQLite)") + @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() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { DeleteQuery.byJsonPath(tbl) } + ForceDialect.sqlite() + assertThrows { DeleteQuery.byJsonPath(TEST_TABLE) } } } diff --git a/src/jvm/src/test/kotlin/query/DocumentQueryTest.kt b/src/jvm/src/test/kotlin/query/DocumentQueryTest.kt index bb6d5bc..e57340e 100644 --- a/src/jvm/src/test/kotlin/query/DocumentQueryTest.kt +++ b/src/jvm/src/test/kotlin/query/DocumentQueryTest.kt @@ -6,8 +6,9 @@ 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 solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -17,92 +18,90 @@ import kotlin.test.assertTrue @DisplayName("JVM | Kotlin | Query | DocumentQuery") class DocumentQueryTest { - /** Test table name */ - private val tbl = "test_table" - /** * Clear the connection string (resets Dialect) */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test - @DisplayName("insert generates no auto ID (PostgreSQL)") + @DisplayName("insert generates no auto ID | PostgreSQL") fun insertNoAutoPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL - assertEquals("INSERT INTO $tbl VALUES (:data)", DocumentQuery.insert(tbl)) + ForceDialect.postgres() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data)", DocumentQuery.insert(TEST_TABLE)) } @Test - @DisplayName("insert generates no auto ID (SQLite)") + @DisplayName("insert generates no auto ID | SQLite") fun insertNoAutoSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertEquals("INSERT INTO $tbl VALUES (:data)", DocumentQuery.insert(tbl)) + ForceDialect.sqlite() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data)", DocumentQuery.insert(TEST_TABLE)) } @Test - @DisplayName("insert generates auto number (PostgreSQL)") + @DisplayName("insert generates auto number | PostgreSQL") fun insertAutoNumberPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "INSERT INTO $tbl VALUES (:data::jsonb || ('{\"id\":' " + - "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $tbl) || '}')::jsonb)", - DocumentQuery.insert(tbl, AutoId.NUMBER) + "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)") + @DisplayName("insert generates auto number | SQLite") fun insertAutoNumberSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "INSERT INTO $tbl VALUES (json_set(:data, '$.id', " + - "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $tbl)))", - DocumentQuery.insert(tbl, AutoId.NUMBER) + "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)") + @DisplayName("insert generates auto UUID | PostgreSQL") fun insertAutoUUIDPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL - val query = DocumentQuery.insert(tbl, AutoId.UUID) + ForceDialect.postgres() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) assertTrue( - query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""), + 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)") + @DisplayName("insert generates auto UUID | SQLite") fun insertAutoUUIDSQLite() { - Configuration.dialectValue = Dialect.SQLITE - val query = DocumentQuery.insert(tbl, AutoId.UUID) + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) assertTrue( - query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"), + 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)") + @DisplayName("insert generates auto random string | PostgreSQL") fun insertAutoRandomPostgres() { try { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() Configuration.idStringLength = 8 - val query = DocumentQuery.insert(tbl, AutoId.RANDOM_STRING) + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) assertTrue( - query.startsWith("INSERT INTO $tbl VALUES (:data::jsonb || '{\"id\":\""), + 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 $tbl VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "").length, + query.replace("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", "") + .replace("\"}')", "").length, "Random string length incorrect" ) } finally { @@ -111,18 +110,18 @@ class DocumentQueryTest { } @Test - @DisplayName("insert generates auto random string (SQLite)") + @DisplayName("insert generates auto random string | SQLite") fun insertAutoRandomSQLite() { - Configuration.dialectValue = Dialect.SQLITE - val query = DocumentQuery.insert(tbl, AutoId.RANDOM_STRING) + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) assertTrue( - query.startsWith("INSERT INTO $tbl VALUES (json_set(:data, '$.id', '"), + 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 $tbl VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length, + query.replace("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$.id', '", "").replace("'))", "").length, "Random string length incorrect" ) } @@ -130,21 +129,25 @@ class DocumentQueryTest { @Test @DisplayName("insert fails when no dialect is set") fun insertFailsUnknown() { - assertThrows { DocumentQuery.insert(tbl) } + assertThrows { DocumentQuery.insert(TEST_TABLE) } } @Test @DisplayName("save generates correctly") fun save() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data", - DocumentQuery.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly" + "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 $tbl SET data = :data", DocumentQuery.update(tbl), "Update query not constructed correctly") + assertEquals( + "UPDATE $TEST_TABLE SET data = :data", + DocumentQuery.update(TEST_TABLE), + "Update query not constructed correctly" + ) } diff --git a/src/jvm/src/test/kotlin/query/ExistsQueryTest.kt b/src/jvm/src/test/kotlin/query/ExistsQueryTest.kt index d9d09e2..991a036 100644 --- a/src/jvm/src/test/kotlin/query/ExistsQueryTest.kt +++ b/src/jvm/src/test/kotlin/query/ExistsQueryTest.kt @@ -8,98 +8,98 @@ import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals /** * Unit tests for the `Exists` object */ -@DisplayName("Kotlin | Common | Query: Exists") +@DisplayName("JVM | Kotlin | Query | ExistsQuery") class ExistsQueryTest { - /** Test table name */ - private val tbl = "test_table" - /** * Clear the connection string (resets Dialect) */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test - @DisplayName("byId generates correctly (PostgreSQL)") + @DisplayName("byId generates correctly | PostgreSQL") fun byIdPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it", - ExistsQuery.byId(tbl), "Exists query not constructed correctly" + "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)") + @DisplayName("byId generates correctly | SQLite") fun byIdSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'id' = :id) AS it", - ExistsQuery.byId(tbl), "Exists query not constructed correctly" + "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)") + @DisplayName("byFields generates correctly | PostgreSQL") fun byFieldsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "SELECT EXISTS (SELECT 1 FROM $tbl WHERE (data->>'it')::numeric = :test) AS it", - ExistsQuery.byFields(tbl, listOf(Field.equal("it", 7, ":test"))), + "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)") + @DisplayName("byFields generates correctly | SQLite") fun byFieldsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data->>'it' = :test) AS it", - ExistsQuery.byFields(tbl, listOf(Field.equal("it", 7, ":test"))), + "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)") + @DisplayName("byContains generates correctly | PostgreSQL") fun byContainsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "SELECT EXISTS (SELECT 1 FROM $tbl WHERE data @> :criteria) AS it", ExistsQuery.byContains(tbl), + "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)") + @DisplayName("byContains fails | SQLite") fun byContainsSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { ExistsQuery.byContains(tbl) } + ForceDialect.sqlite() + assertThrows { ExistsQuery.byContains(TEST_TABLE) } } @Test - @DisplayName("byJsonPath generates correctly (PostgreSQL)") + @DisplayName("byJsonPath generates correctly | PostgreSQL") fun byJsonPathPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "SELECT EXISTS (SELECT 1 FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)) AS it", - ExistsQuery.byJsonPath(tbl), "Exists query not constructed correctly" + "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)") + @DisplayName("byJsonPath fails | SQLite") fun byJsonPathSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { ExistsQuery.byJsonPath(tbl) } + ForceDialect.sqlite() + assertThrows { ExistsQuery.byJsonPath(TEST_TABLE) } } } diff --git a/src/jvm/src/test/kotlin/query/FindQueryTest.kt b/src/jvm/src/test/kotlin/query/FindQueryTest.kt index faeb505..e458bf2 100644 --- a/src/jvm/src/test/kotlin/query/FindQueryTest.kt +++ b/src/jvm/src/test/kotlin/query/FindQueryTest.kt @@ -4,10 +4,9 @@ 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.Configuration -import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.support.ForceDialect import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals @@ -22,7 +21,7 @@ class FindQueryTest { */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test @@ -31,9 +30,9 @@ class FindQueryTest { assertEquals("SELECT data FROM $TEST_TABLE", FindQuery.all(TEST_TABLE), "Find query not constructed correctly") @Test - @DisplayName("byId generates correctly (PostgreSQL)") + @DisplayName("byId generates correctly | PostgreSQL") fun byIdPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id", FindQuery.byId(TEST_TABLE), "Find query not constructed correctly" @@ -41,9 +40,9 @@ class FindQueryTest { } @Test - @DisplayName("byId generates correctly (SQLite)") + @DisplayName("byId generates correctly | SQLite") fun byIdSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id", FindQuery.byId(TEST_TABLE), "Find query not constructed correctly" @@ -51,9 +50,9 @@ class FindQueryTest { } @Test - @DisplayName("byFields generates correctly (PostgreSQL)") + @DisplayName("byFields generates correctly | PostgreSQL") fun byFieldsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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"))), @@ -62,9 +61,9 @@ class FindQueryTest { } @Test - @DisplayName("byFields generates correctly (SQLite)") + @DisplayName("byFields generates correctly | SQLite") fun byFieldsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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"))), @@ -73,9 +72,9 @@ class FindQueryTest { } @Test - @DisplayName("byContains generates correctly (PostgreSQL)") + @DisplayName("byContains generates correctly | PostgreSQL") fun byContainsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( "SELECT data FROM $TEST_TABLE WHERE data @> :criteria", FindQuery.byContains(TEST_TABLE), "Find query not constructed correctly" @@ -83,16 +82,16 @@ class FindQueryTest { } @Test - @DisplayName("byContains fails (SQLite)") + @DisplayName("byContains fails | SQLite") fun byContainsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertThrows { FindQuery.byContains(TEST_TABLE) } } @Test - @DisplayName("byJsonPath generates correctly (PostgreSQL)") + @DisplayName("byJsonPath generates correctly | PostgreSQL") fun byJsonPathPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( "SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)", FindQuery.byJsonPath(TEST_TABLE), @@ -101,9 +100,9 @@ class FindQueryTest { } @Test - @DisplayName("byJsonPath fails (SQLite)") + @DisplayName("byJsonPath fails | SQLite") fun byJsonPathSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertThrows { FindQuery.byJsonPath(TEST_TABLE) } } } diff --git a/src/jvm/src/test/kotlin/query/PatchQueryTest.kt b/src/jvm/src/test/kotlin/query/PatchQueryTest.kt index 0afcdf8..dbb4169 100644 --- a/src/jvm/src/test/kotlin/query/PatchQueryTest.kt +++ b/src/jvm/src/test/kotlin/query/PatchQueryTest.kt @@ -4,10 +4,10 @@ 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.Configuration -import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals /** @@ -16,90 +16,87 @@ import kotlin.test.assertEquals @DisplayName("JVM | Kotlin | Query | PatchQuery") class PatchQueryTest { - /** Test table name */ - private val tbl = "test_table" - /** * Reset the dialect */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test - @DisplayName("byId generates correctly (PostgreSQL)") + @DisplayName("byId generates correctly | PostgreSQL") fun byIdPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "UPDATE $tbl SET data = data || :data WHERE data->>'id' = :id", - PatchQuery.byId(tbl), "Patch query not constructed correctly" + "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)") + @DisplayName("byId generates correctly | SQLite") fun byIdSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "UPDATE $tbl SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id", - PatchQuery.byId(tbl), "Patch query not constructed correctly" + "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)") + @DisplayName("byFields generates correctly | PostgreSQL") fun byFieldsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "UPDATE $tbl SET data = data || :data WHERE data->>'z' = :y", - PatchQuery.byFields(tbl, listOf(Field.equal("z", "", ":y"))), + "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)") + @DisplayName("byFields generates correctly | SQLite") fun byFieldsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "UPDATE $tbl SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y", - PatchQuery.byFields(tbl, listOf(Field.equal("z", "", ":y"))), + "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)") + @DisplayName("byContains generates correctly | PostgreSQL") fun byContainsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "UPDATE $tbl SET data = data || :data WHERE data @> :criteria", PatchQuery.byContains(tbl), + "UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria", PatchQuery.byContains(TEST_TABLE), "Patch query not constructed correctly" ) } @Test - @DisplayName("byContains fails (SQLite)") + @DisplayName("byContains fails | SQLite") fun byContainsSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { PatchQuery.byContains(tbl) } + ForceDialect.sqlite() + assertThrows { PatchQuery.byContains(TEST_TABLE) } } @Test - @DisplayName("byJsonPath generates correctly (PostgreSQL)") + @DisplayName("byJsonPath generates correctly | PostgreSQL") fun byJsonPathPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "UPDATE $tbl SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)", - PatchQuery.byJsonPath(tbl), "Patch query not constructed correctly" + "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)") + @DisplayName("byJsonPath fails | SQLite") fun byJsonPathSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { PatchQuery.byJsonPath(tbl) } + ForceDialect.sqlite() + assertThrows { PatchQuery.byJsonPath(TEST_TABLE) } } } diff --git a/src/jvm/src/test/kotlin/query/QueryTest.kt b/src/jvm/src/test/kotlin/query/QueryTest.kt index c16b31a..1d3ec1d 100644 --- a/src/jvm/src/test/kotlin/query/QueryTest.kt +++ b/src/jvm/src/test/kotlin/query/QueryTest.kt @@ -3,10 +3,10 @@ package solutions.bitbadger.documents.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.support.ForceDialect import kotlin.test.assertEquals /** @@ -20,7 +20,7 @@ class QueryTest { */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test @@ -29,30 +29,30 @@ class QueryTest { assertEquals("x WHERE y", statementWhere("x", "y"), "Statements not combined correctly") @Test - @DisplayName("byId generates a numeric ID query (PostgreSQL)") + @DisplayName("byId generates a numeric ID query | PostgreSQL") fun byIdNumericPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("test WHERE (data->>'id')::numeric = :id", byId("test", 9)) } @Test - @DisplayName("byId generates an alphanumeric ID query (PostgreSQL)") + @DisplayName("byId generates an alphanumeric ID query | PostgreSQL") fun byIdAlphaPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("unit WHERE data->>'id' = :id", byId("unit", "18")) } @Test - @DisplayName("byId generates ID query (SQLite)") + @DisplayName("byId generates ID query | SQLite") fun byIdSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals("yo WHERE data->>'id' = :id", byId("yo", 27)) } @Test - @DisplayName("byFields generates default field query (PostgreSQL)") + @DisplayName("byFields generates default field query | PostgreSQL") fun byFieldsMultipleDefaultPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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"))) @@ -60,9 +60,9 @@ class QueryTest { } @Test - @DisplayName("byFields generates default field query (SQLite)") + @DisplayName("byFields generates default field query | SQLite") fun byFieldsMultipleDefaultSQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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"))) @@ -70,9 +70,9 @@ class QueryTest { } @Test - @DisplayName("byFields generates ANY field query (PostgreSQL)") + @DisplayName("byFields generates ANY field query | PostgreSQL") fun byFieldsMultipleAnyPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( "that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value", byFields( @@ -83,9 +83,9 @@ class QueryTest { } @Test - @DisplayName("byFields generates ANY field query (SQLite)") + @DisplayName("byFields generates ANY field query | SQLite") fun byFieldsMultipleAnySQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( "that WHERE data->>'a' = :the_a OR data->>'b' = :b_value", byFields( @@ -103,7 +103,7 @@ class QueryTest { } @Test - @DisplayName("orderBy generates single, no direction for PostgreSQL") + @DisplayName("orderBy generates single, no direction | PostgreSQL") fun orderBySinglePostgres() = assertEquals( " ORDER BY data->>'TestField'", @@ -111,7 +111,7 @@ class QueryTest { ) @Test - @DisplayName("orderBy generates single, no direction for SQLite") + @DisplayName("orderBy generates single, no direction | SQLite") fun orderBySingleSQLite() = assertEquals( " ORDER BY data->>'TestField'", orderBy(listOf(Field.named("TestField")), Dialect.SQLITE), @@ -119,7 +119,7 @@ class QueryTest { ) @Test - @DisplayName("orderBy generates multiple with direction for PostgreSQL") + @DisplayName("orderBy generates multiple with direction | PostgreSQL") fun orderByMultiplePostgres() = assertEquals( " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", @@ -131,7 +131,7 @@ class QueryTest { ) @Test - @DisplayName("orderBy generates multiple with direction for SQLite") + @DisplayName("orderBy generates multiple with direction | SQLite") fun orderByMultipleSQLite() = assertEquals( " ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", @@ -143,7 +143,7 @@ class QueryTest { ) @Test - @DisplayName("orderBy generates numeric ordering PostgreSQL") + @DisplayName("orderBy generates numeric ordering | PostgreSQL") fun orderByNumericPostgres() = assertEquals( " ORDER BY (data->>'Test')::numeric", @@ -151,7 +151,7 @@ class QueryTest { ) @Test - @DisplayName("orderBy generates numeric ordering for SQLite") + @DisplayName("orderBy generates numeric ordering | SQLite") fun orderByNumericSQLite() = assertEquals( " ORDER BY data->>'Test'", orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE), @@ -159,7 +159,7 @@ class QueryTest { ) @Test - @DisplayName("orderBy generates case-insensitive ordering for PostgreSQL") + @DisplayName("orderBy generates case-insensitive ordering | PostgreSQL") fun orderByCIPostgres() = assertEquals( " ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", @@ -168,7 +168,7 @@ class QueryTest { ) @Test - @DisplayName("orderBy generates case-insensitive ordering for SQLite") + @DisplayName("orderBy generates case-insensitive ordering | SQLite") fun orderByCISQLite() = assertEquals( " ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", diff --git a/src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt b/src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt index d042f17..06a2523 100644 --- a/src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt +++ b/src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt @@ -5,6 +5,8 @@ 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.support.ForceDialect +import solutions.bitbadger.documents.support.TEST_TABLE import kotlin.test.assertEquals /** @@ -13,36 +15,33 @@ import kotlin.test.assertEquals @DisplayName("JVM | Kotlin | Query | RemoveFieldsQuery") class RemoveFieldsQueryTest { - /** Test table name */ - private val tbl = "test_table" - /** * Reset the dialect */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test - @DisplayName("byId generates correctly (PostgreSQL)") + @DisplayName("byId generates correctly | PostgreSQL") fun byIdPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "UPDATE $tbl SET data = data - :name::text[] WHERE data->>'id' = :id", - RemoveFieldsQuery.byId(tbl, listOf(Parameter(":name", ParameterType.STRING, "{a,z}"))), + "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)") + @DisplayName("byId generates correctly | SQLite") fun byIdSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "UPDATE $tbl SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id", + "UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id", RemoveFieldsQuery.byId( - tbl, + TEST_TABLE, listOf( Parameter(":name0", ParameterType.STRING, "a"), Parameter(":name1", ParameterType.STRING, "z") @@ -53,13 +52,13 @@ class RemoveFieldsQueryTest { } @Test - @DisplayName("byFields generates correctly (PostgreSQL)") + @DisplayName("byFields generates correctly | PostgreSQL") fun byFieldsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "UPDATE $tbl SET data = data - :name::text[] WHERE data->>'f' > :g", + "UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data->>'f' > :g", RemoveFieldsQuery.byFields( - tbl, + TEST_TABLE, listOf(Parameter(":name", ParameterType.STRING, "{b,c}")), listOf(Field.greater("f", "", ":g")) ), @@ -68,13 +67,13 @@ class RemoveFieldsQueryTest { } @Test - @DisplayName("byFields generates correctly (SQLite)") + @DisplayName("byFields generates correctly | SQLite") fun byFieldsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( - "UPDATE $tbl SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g", + "UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g", RemoveFieldsQuery.byFields( - tbl, + TEST_TABLE, listOf(Parameter(":name0", ParameterType.STRING, "b"), Parameter(":name1", ParameterType.STRING, "c")), listOf(Field.greater("f", "", ":g")) ), @@ -83,38 +82,38 @@ class RemoveFieldsQueryTest { } @Test - @DisplayName("byContains generates correctly (PostgreSQL)") + @DisplayName("byContains generates correctly | PostgreSQL") fun byContainsPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "UPDATE $tbl SET data = data - :name::text[] WHERE data @> :criteria", - RemoveFieldsQuery.byContains(tbl, listOf(Parameter(":name", ParameterType.STRING, "{m,n}"))), + "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)") + @DisplayName("byContains fails | SQLite") fun byContainsSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { RemoveFieldsQuery.byContains(tbl, listOf()) } + ForceDialect.sqlite() + assertThrows { RemoveFieldsQuery.byContains(TEST_TABLE, listOf()) } } @Test - @DisplayName("byJsonPath generates correctly (PostgreSQL)") + @DisplayName("byJsonPath generates correctly | PostgreSQL") fun byJsonPathPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( - "UPDATE $tbl SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)", - RemoveFieldsQuery.byJsonPath(tbl, listOf(Parameter(":name", ParameterType.STRING, "{o,p}"))), + "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)") + @DisplayName("byJsonPath fails | SQLite") fun byJsonPathSQLite() { - Configuration.dialectValue = Dialect.SQLITE - assertThrows { RemoveFieldsQuery.byJsonPath(tbl, listOf()) } + ForceDialect.sqlite() + assertThrows { RemoveFieldsQuery.byJsonPath(TEST_TABLE, listOf()) } } -} \ No newline at end of file +} -- 2.47.2 From ca8c8e79ded360e2d8aff6ade34aef1835825ba0 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 16 Mar 2025 14:01:04 -0400 Subject: [PATCH 49/88] Finish Java query tests; WIP on DocEx declarations --- src/jvm/src/main/kotlin/Configuration.kt | 7 +- src/jvm/src/main/kotlin/Dialect.kt | 7 +- src/jvm/src/main/kotlin/DocumentException.kt | 2 +- src/jvm/src/main/kotlin/Field.kt | 1 - src/jvm/src/main/kotlin/FieldFormat.kt | 1 + src/jvm/src/main/kotlin/FieldMatch.kt | 1 + src/jvm/src/main/kotlin/Op.kt | 10 + src/jvm/src/main/kotlin/Parameter.kt | 5 +- .../src/main/kotlin/extensions/Connection.kt | 15 +- src/jvm/src/main/kotlin/jvm/Count.kt | 17 +- src/jvm/src/main/kotlin/jvm/Custom.kt | 19 ++ src/jvm/src/main/kotlin/jvm/Definition.kt | 11 +- .../integration/common/CountFunctions.java | 6 +- .../jvm/integration/postgresql/CountIT.java | 6 +- .../java/jvm/integration/sqlite/CountIT.java | 6 +- .../documents/java/query/WhereTest.java | 172 ++++++++++++++++++ src/jvm/src/test/kotlin/FieldTest.kt | 87 ++++----- src/jvm/src/test/kotlin/query/WhereTest.kt | 75 ++++---- 18 files changed, 348 insertions(+), 100 deletions(-) create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/query/WhereTest.java diff --git a/src/jvm/src/main/kotlin/Configuration.kt b/src/jvm/src/main/kotlin/Configuration.kt index e89793a..d40f653 100644 --- a/src/jvm/src/main/kotlin/Configuration.kt +++ b/src/jvm/src/main/kotlin/Configuration.kt @@ -21,7 +21,7 @@ object Configuration { var idStringLength = 16 /** The derived dialect value from the connection string */ - internal var dialectValue: Dialect? = null + private var dialectValue: Dialect? = null /** The connection string for the JDBC connection */ @JvmStatic @@ -35,12 +35,13 @@ object Configuration { * Retrieve a new connection to the configured database * * @return A new connection to the configured database - * @throws IllegalArgumentException If the connection string is not set before calling this + * @throws DocumentException If the connection string is not set before calling this */ + @Throws(DocumentException::class) @JvmStatic fun dbConn(): Connection { if (connectionString == null) { - throw IllegalArgumentException("Please provide a connection string before attempting data access") + throw DocumentException("Please provide a connection string before attempting data access") } return DriverManager.getConnection(connectionString) } diff --git a/src/jvm/src/main/kotlin/Dialect.kt b/src/jvm/src/main/kotlin/Dialect.kt index 986dddb..1d0c32b 100644 --- a/src/jvm/src/main/kotlin/Dialect.kt +++ b/src/jvm/src/main/kotlin/Dialect.kt @@ -1,11 +1,14 @@ package solutions.bitbadger.documents +import kotlin.jvm.Throws + /** * The SQL dialect to use when building queries */ enum class Dialect { /** PostgreSQL */ POSTGRESQL, + /** SQLite */ SQLITE; @@ -18,9 +21,11 @@ enum class Dialect { * @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(":sqlite:") -> SQLITE connectionString.contains(":postgresql:") -> POSTGRESQL else -> throw DocumentException("Cannot determine dialect from [$connectionString]") } diff --git a/src/jvm/src/main/kotlin/DocumentException.kt b/src/jvm/src/main/kotlin/DocumentException.kt index 4a292e6..d415801 100644 --- a/src/jvm/src/main/kotlin/DocumentException.kt +++ b/src/jvm/src/main/kotlin/DocumentException.kt @@ -6,4 +6,4 @@ package solutions.bitbadger.documents * @param message The message for the exception * @param cause The underlying exception (optional) */ -class DocumentException(message: String, cause: Throwable? = null) : Exception(message, cause) \ No newline at end of file +class DocumentException @JvmOverloads constructor(message: String, cause: Throwable? = null) : Exception(message, cause) diff --git a/src/jvm/src/main/kotlin/Field.kt b/src/jvm/src/main/kotlin/Field.kt index 9a4c899..c4d017a 100644 --- a/src/jvm/src/main/kotlin/Field.kt +++ b/src/jvm/src/main/kotlin/Field.kt @@ -118,7 +118,6 @@ class Field private constructor( is ComparisonInArray<*> -> { val mkString = Configuration.dialect("append parameters for InArray") == Dialect.POSTGRESQL - // TODO: I think this is actually Pair> comparison.value.second.forEachIndexed { index, item -> if (mkString) { existing.add(Parameter("${parameterName}_$index", ParameterType.STRING, "$item")) diff --git a/src/jvm/src/main/kotlin/FieldFormat.kt b/src/jvm/src/main/kotlin/FieldFormat.kt index c804a87..d323696 100644 --- a/src/jvm/src/main/kotlin/FieldFormat.kt +++ b/src/jvm/src/main/kotlin/FieldFormat.kt @@ -6,6 +6,7 @@ package solutions.bitbadger.documents 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/jvm/src/main/kotlin/FieldMatch.kt b/src/jvm/src/main/kotlin/FieldMatch.kt index 3b1d3ff..3af7230 100644 --- a/src/jvm/src/main/kotlin/FieldMatch.kt +++ b/src/jvm/src/main/kotlin/FieldMatch.kt @@ -6,6 +6,7 @@ package solutions.bitbadger.documents 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/jvm/src/main/kotlin/Op.kt b/src/jvm/src/main/kotlin/Op.kt index 61723c9..45004f4 100644 --- a/src/jvm/src/main/kotlin/Op.kt +++ b/src/jvm/src/main/kotlin/Op.kt @@ -6,24 +6,34 @@ package solutions.bitbadger.documents 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/jvm/src/main/kotlin/Parameter.kt b/src/jvm/src/main/kotlin/Parameter.kt index 1bd707b..3e9c9a7 100644 --- a/src/jvm/src/main/kotlin/Parameter.kt +++ b/src/jvm/src/main/kotlin/Parameter.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents import java.sql.PreparedStatement import java.sql.Types +import kotlin.jvm.Throws /** * A parameter to use for a query @@ -22,7 +23,9 @@ class Parameter(val name: String, val type: ParameterType, val value: T) { * * @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 -> { @@ -33,7 +36,7 @@ class Parameter(val name: String, val type: ParameterType, val value: T) { 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})" + "Number parameter must be Byte, Short, Int, or Long (${value::class.simpleName})" ) } } diff --git a/src/jvm/src/main/kotlin/extensions/Connection.kt b/src/jvm/src/main/kotlin/extensions/Connection.kt index 8c4267a..bdc56ff 100644 --- a/src/jvm/src/main/kotlin/extensions/Connection.kt +++ b/src/jvm/src/main/kotlin/extensions/Connection.kt @@ -6,6 +6,7 @@ import solutions.bitbadger.documents.* import solutions.bitbadger.documents.jvm.* import java.sql.Connection import java.sql.ResultSet +import kotlin.jvm.Throws // ~~~ CUSTOM QUERIES ~~~ @@ -17,7 +18,9 @@ import java.sql.ResultSet * @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(), @@ -34,7 +37,9 @@ fun Connection.customList( * @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(), @@ -48,7 +53,9 @@ fun Connection.customSingle( * * @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) fun Connection.customNonQuery(query: String, parameters: Collection> = listOf()) = Custom.nonQuery(query, parameters, this) @@ -60,7 +67,9 @@ fun Connection.customNonQuery(query: String, parameters: Collection * @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(), @@ -109,7 +118,7 @@ fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) * @param document The document to be inserted */ fun Connection.insert(tableName: String, document: TDoc) = - Document.insert(tableName, document, this) + Document.insert(tableName, document, this) /** * Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert") @@ -137,7 +146,9 @@ fun Connection.update(tableName: String, docId: TKey, document: TDo * * @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) @@ -148,7 +159,9 @@ fun Connection.countAll(tableName: String) = * @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) diff --git a/src/jvm/src/main/kotlin/jvm/Count.kt b/src/jvm/src/main/kotlin/jvm/Count.kt index 1127d80..25f87e3 100644 --- a/src/jvm/src/main/kotlin/jvm/Count.kt +++ b/src/jvm/src/main/kotlin/jvm/Count.kt @@ -4,6 +4,7 @@ import solutions.bitbadger.documents.* import solutions.bitbadger.documents.query.CountQuery import solutions.bitbadger.documents.extensions.customScalar import java.sql.Connection +import kotlin.jvm.Throws /** * Functions to count documents @@ -16,7 +17,9 @@ object Count { * @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) = conn.customScalar(CountQuery.all(tableName), listOf(), Long::class.java, Results::toCount) @@ -26,7 +29,9 @@ object Count { * * @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) } @@ -39,7 +44,9 @@ object Count { * @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( @@ -64,7 +71,9 @@ object Count { * @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) = @@ -79,6 +88,7 @@ object Count { * @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) = conn.customScalar( @@ -94,8 +104,9 @@ object Count { * @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 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) } @@ -109,6 +120,7 @@ object Count { * @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) = conn.customScalar( @@ -124,8 +136,9 @@ object Count { * @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 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/jvm/src/main/kotlin/jvm/Custom.kt b/src/jvm/src/main/kotlin/jvm/Custom.kt index b4f5d54..d438b64 100644 --- a/src/jvm/src/main/kotlin/jvm/Custom.kt +++ b/src/jvm/src/main/kotlin/jvm/Custom.kt @@ -1,9 +1,11 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Parameter import java.sql.Connection import java.sql.ResultSet +import kotlin.jvm.Throws object Custom { @@ -16,7 +18,9 @@ object Custom { * @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, @@ -34,7 +38,9 @@ object Custom { * @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, @@ -52,7 +58,9 @@ object Custom { * @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 + * @throws DocumentException If parameters are invalid */ + @Throws(DocumentException::class) @JvmStatic fun single( query: String, @@ -70,7 +78,9 @@ object Custom { * @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, @@ -85,7 +95,9 @@ object Custom { * @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 */ + @Throws(DocumentException::class) @JvmStatic fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) { Parameters.apply(conn, query, parameters).use { it.executeUpdate() } @@ -96,7 +108,9 @@ object Custom { * * @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, or if parameters are invalid */ + @Throws(DocumentException::class) @JvmStatic @JvmOverloads fun nonQuery(query: String, parameters: Collection> = listOf()) = @@ -110,7 +124,9 @@ object Custom { * @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 */ + @Throws(DocumentException::class) @JvmStatic fun scalar( query: String, @@ -132,7 +148,10 @@ object Custom { * @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(), diff --git a/src/jvm/src/main/kotlin/jvm/Definition.kt b/src/jvm/src/main/kotlin/jvm/Definition.kt index 8e18818..585e7d9 100644 --- a/src/jvm/src/main/kotlin/jvm/Definition.kt +++ b/src/jvm/src/main/kotlin/jvm/Definition.kt @@ -6,6 +6,7 @@ import solutions.bitbadger.documents.DocumentIndex import solutions.bitbadger.documents.extensions.customNonQuery import solutions.bitbadger.documents.query.DefinitionQuery import java.sql.Connection +import kotlin.jvm.Throws /** * Functions to define tables and indexes @@ -17,7 +18,9 @@ object Definition { * * @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 { @@ -29,7 +32,9 @@ object Definition { * 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) } @@ -41,7 +46,9 @@ object Definition { * @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) = conn.customNonQuery(DefinitionQuery.ensureIndexOn(tableName, indexName, fields)) @@ -52,7 +59,9 @@ object Definition { * @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) } @@ -75,7 +84,7 @@ object Definition { * * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java index c32598d..a502241 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java @@ -17,18 +17,18 @@ import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; */ final public class CountFunctions { - public static void all(ThrowawayDatabase db) { + 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) { + 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) { + 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"); diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java index 97c1d3f..def860b 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java @@ -14,7 +14,7 @@ public class CountIT { @Test @DisplayName("all counts all documents") - public void all() { + public void all() throws DocumentException { try (PgDB db = new PgDB()) { CountFunctions.all(db); } @@ -22,7 +22,7 @@ public class CountIT { @Test @DisplayName("byFields counts documents by a numeric value") - public void byFieldsNumeric() { + public void byFieldsNumeric() throws DocumentException { try (PgDB db = new PgDB()) { CountFunctions.byFieldsNumeric(db); } @@ -30,7 +30,7 @@ public class CountIT { @Test @DisplayName("byFields counts documents by a alphanumeric value") - public void byFieldsAlpha() { + public void byFieldsAlpha() throws DocumentException { try (PgDB db = new PgDB()) { CountFunctions.byFieldsAlpha(db); } diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java index 38fda0b..1d9078b 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java @@ -16,7 +16,7 @@ public class CountIT { @Test @DisplayName("all counts all documents") - public void all() { + public void all() throws DocumentException { try (SQLiteDB db = new SQLiteDB()) { CountFunctions.all(db); } @@ -24,7 +24,7 @@ public class CountIT { @Test @DisplayName("byFields counts documents by a numeric value") - public void byFieldsNumeric() { + public void byFieldsNumeric() throws DocumentException { try (SQLiteDB db = new SQLiteDB()) { CountFunctions.byFieldsNumeric(db); } @@ -32,7 +32,7 @@ public class CountIT { @Test @DisplayName("byFields counts documents by a alphanumeric value") - public void byFieldsAlpha() { + public void byFieldsAlpha() throws DocumentException { try (SQLiteDB db = new SQLiteDB()) { CountFunctions.byFieldsAlpha(db); } diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/WhereTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/WhereTest.java new file mode 100644 index 0000000..13de7d9 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/WhereTest.java @@ -0,0 +1,172 @@ +package solutions.bitbadger.documents.java.query; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.Field; +import solutions.bitbadger.documents.FieldMatch; +import solutions.bitbadger.documents.query.Where; +import solutions.bitbadger.documents.support.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("JVM | 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/jvm/src/test/kotlin/FieldTest.kt b/src/jvm/src/test/kotlin/FieldTest.kt index 12b71e5..4544d20 100644 --- a/src/jvm/src/test/kotlin/FieldTest.kt +++ b/src/jvm/src/test/kotlin/FieldTest.kt @@ -4,6 +4,7 @@ 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.support.ForceDialect import kotlin.test.assertEquals import kotlin.test.assertNotSame import kotlin.test.assertNull @@ -19,7 +20,7 @@ class FieldTest { */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } // ~~~ INSTANCE METHODS ~~~ @@ -120,178 +121,178 @@ class FieldTest { "Path not correct") @Test - @DisplayName("toWhere generates for exists w/o qualifier (PostgreSQL)") + @DisplayName("toWhere generates for exists w/o qualifier | PostgreSQL") fun toWhereExistsNoQualPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for exists w/o qualifier | SQLite") fun toWhereExistsNoQualSQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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)") + @DisplayName("toWhere generates for not-exists w/o qualifier | PostgreSQL") fun toWhereNotExistsNoQualPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for not-exists w/o qualifier | SQLite") fun toWhereNotExistsNoQualSQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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)") + @DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range | PostgreSQL") fun toWhereBetweenNoQualNumericPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range | PostgreSQL") fun toWhereBetweenNoQualAlphaPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for BETWEEN w/o qualifier | SQLite") fun toWhereBetweenNoQualSQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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)") + @DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range | PostgreSQL") fun toWhereBetweenQualNumericPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range | PostgreSQL") fun toWhereBetweenQualAlphaPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for BETWEEN w/ qualifier | SQLite") fun toWhereBetweenQualSQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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)") + @DisplayName("toWhere generates for IN/any, numeric values | PostgreSQL") fun toWhereAnyNumericPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for IN/any, alphanumeric values | PostgreSQL") fun toWhereAnyAlphaPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for IN/any | SQLite") fun toWhereAnySQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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)") + @DisplayName("toWhere generates for inArray | PostgreSQL") fun toWhereInArrayPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for inArray | SQLite") fun toWhereInArraySQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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)") + @DisplayName("toWhere generates for others w/o qualifier | PostgreSQL") fun toWhereOtherNoQualPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates for others w/o qualifier | SQLite") fun toWhereOtherNoQualSQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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)") + @DisplayName("toWhere generates no-parameter w/ qualifier | PostgreSQL") fun toWhereNoParamWithQualPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates no-parameter w/ qualifier | SQLite") fun toWhereNoParamWithQualSQLite() { - Configuration.dialectValue = Dialect.SQLITE + 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)") + @DisplayName("toWhere generates parameter w/ qualifier | PostgreSQL") fun toWhereParamWithQualPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + 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)") + @DisplayName("toWhere generates parameter w/ qualifier | SQLite") fun toWhereParamWithQualSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals("q.data->>'le_field' <= :it", Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(), "Field WHERE clause not generated correctly") diff --git a/src/jvm/src/test/kotlin/query/WhereTest.kt b/src/jvm/src/test/kotlin/query/WhereTest.kt index 95d68d8..c436ec8 100644 --- a/src/jvm/src/test/kotlin/query/WhereTest.kt +++ b/src/jvm/src/test/kotlin/query/WhereTest.kt @@ -5,6 +5,7 @@ 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.support.ForceDialect import kotlin.test.assertEquals /** @@ -18,7 +19,7 @@ class WhereTest { */ @AfterEach fun cleanUp() { - Configuration.dialectValue = null + ForceDialect.none() } @Test @@ -27,30 +28,30 @@ class WhereTest { assertEquals("", Where.byFields(listOf())) @Test - @DisplayName("byFields generates one numeric field (PostgreSQL)") + @DisplayName("byFields generates one numeric field | PostgreSQL") fun byFieldsOneFieldPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("(data->>'it')::numeric = :that", Where.byFields(listOf(Field.equal("it", 9, ":that")))) } @Test - @DisplayName("byFields generates one alphanumeric field (PostgreSQL)") + @DisplayName("byFields generates one alphanumeric field | PostgreSQL") fun byFieldsOneAlphaFieldPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("data->>'it' = :that", Where.byFields(listOf(Field.equal("it", "", ":that")))) } @Test - @DisplayName("byFields generates one field (SQLite)") + @DisplayName("byFields generates one field | SQLite") fun byFieldsOneFieldSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals("data->>'it' = :that", Where.byFields(listOf(Field.equal("it", "", ":that")))) } @Test - @DisplayName("byFields generates multiple fields w/ default match (PostgreSQL)") + @DisplayName("byFields generates multiple fields w/ default match | PostgreSQL") fun byFieldsMultipleDefaultPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( "data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three", Where.byFields( @@ -60,9 +61,9 @@ class WhereTest { } @Test - @DisplayName("byFields generates multiple fields w/ default match (SQLite)") + @DisplayName("byFields generates multiple fields w/ default match | SQLite") fun byFieldsMultipleDefaultSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( "data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three", Where.byFields( @@ -72,9 +73,9 @@ class WhereTest { } @Test - @DisplayName("byFields generates multiple fields w/ ANY match (PostgreSQL)") + @DisplayName("byFields generates multiple fields w/ ANY match | PostgreSQL") fun byFieldsMultipleAnyPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals( "data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three", Where.byFields( @@ -85,9 +86,9 @@ class WhereTest { } @Test - @DisplayName("byFields generates multiple fields w/ ANY match (SQLite)") + @DisplayName("byFields generates multiple fields w/ ANY match | SQLite") fun byFieldsMultipleAnySQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals( "data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three", Where.byFields( @@ -98,79 +99,79 @@ class WhereTest { } @Test - @DisplayName("byId generates defaults for alphanumeric key (PostgreSQL)") + @DisplayName("byId generates defaults for alphanumeric key | PostgreSQL") fun byIdDefaultAlphaPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("data->>'id' = :id", Where.byId(docId = "")) } @Test - @DisplayName("byId generates defaults for numeric key (PostgreSQL)") + @DisplayName("byId generates defaults for numeric key | PostgreSQL") fun byIdDefaultNumericPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("(data->>'id')::numeric = :id", Where.byId(docId = 5)) } @Test - @DisplayName("byId generates defaults (SQLite)") + @DisplayName("byId generates defaults | SQLite") fun byIdDefaultSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals("data->>'id' = :id", Where.byId(docId = "")) } @Test - @DisplayName("byId generates named ID (PostgreSQL)") + @DisplayName("byId generates named ID | PostgreSQL") fun byIdDefaultNamedPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("data->>'id' = :key", Where.byId(":key")) } @Test - @DisplayName("byId generates named ID (SQLite)") + @DisplayName("byId generates named ID | SQLite") fun byIdDefaultNamedSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertEquals("data->>'id' = :key", Where.byId(":key")) } @Test - @DisplayName("jsonContains generates defaults (PostgreSQL)") + @DisplayName("jsonContains generates defaults | PostgreSQL") fun jsonContainsDefaultPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("data @> :criteria", Where.jsonContains()) } @Test - @DisplayName("jsonContains generates named parameter (PostgreSQL)") + @DisplayName("jsonContains generates named parameter | PostgreSQL") fun jsonContainsNamedPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("data @> :it", Where.jsonContains(":it")) } @Test - @DisplayName("jsonContains fails (SQLite)") + @DisplayName("jsonContains fails | SQLite") fun jsonContainsFailsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertThrows { Where.jsonContains() } } @Test - @DisplayName("jsonPathMatches generates defaults (PostgreSQL)") + @DisplayName("jsonPathMatches generates defaults | PostgreSQL") fun jsonPathMatchDefaultPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("jsonb_path_exists(data, :path::jsonpath)", Where.jsonPathMatches()) } @Test - @DisplayName("jsonPathMatches generates named parameter (PostgreSQL)") + @DisplayName("jsonPathMatches generates named parameter | PostgreSQL") fun jsonPathMatchNamedPostgres() { - Configuration.dialectValue = Dialect.POSTGRESQL + ForceDialect.postgres() assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Where.jsonPathMatches(":jp")) } @Test - @DisplayName("jsonPathMatches fails (SQLite)") + @DisplayName("jsonPathMatches fails | SQLite") fun jsonPathFailsSQLite() { - Configuration.dialectValue = Dialect.SQLITE + ForceDialect.sqlite() assertThrows { Where.jsonPathMatches() } } } -- 2.47.2 From 7e91a7b73ffbbfbb2bc5ea0ed6d8e48225b1ee22 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 16 Mar 2025 17:29:52 -0400 Subject: [PATCH 50/88] Finish DocEx declarations --- .../src/main/kotlin/extensions/Connection.kt | 34 +++++++++++++ src/jvm/src/main/kotlin/jvm/Delete.kt | 13 ++++- src/jvm/src/main/kotlin/jvm/Document.kt | 15 ++++++ src/jvm/src/main/kotlin/jvm/Exists.kt | 13 ++++- src/jvm/src/main/kotlin/jvm/Find.kt | 31 ++++++++++-- src/jvm/src/main/kotlin/jvm/Patch.kt | 13 ++++- src/jvm/src/main/kotlin/jvm/RemoveFields.kt | 13 ++++- .../documents/java/ConfigurationTest.java | 48 +++++++++++++++++++ .../documents/java/support/JsonDocument.java | 10 +++- 9 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java diff --git a/src/jvm/src/main/kotlin/extensions/Connection.kt b/src/jvm/src/main/kotlin/extensions/Connection.kt index bdc56ff..9bdf51e 100644 --- a/src/jvm/src/main/kotlin/extensions/Connection.kt +++ b/src/jvm/src/main/kotlin/extensions/Connection.kt @@ -84,7 +84,9 @@ fun Connection.customScalar( * 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) @@ -94,7 +96,9 @@ fun Connection.ensureTable(tableName: String) = * @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) @@ -116,7 +120,9 @@ fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) * * @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) @@ -125,7 +131,9 @@ fun Connection.insert(tableName: String, document: TDoc) = * * @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) @@ -135,7 +143,9 @@ fun Connection.save(tableName: String, document: TDoc) = * @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) @@ -198,7 +208,9 @@ fun Connection.countByJsonPath(tableName: String, path: String) = * @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) @@ -209,7 +221,9 @@ fun Connection.existsById(tableName: String, docId: TKey) = * @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) @@ -247,7 +261,9 @@ fun Connection.existsByJsonPath(tableName: String, path: String) = * @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) @@ -259,7 +275,9 @@ fun Connection.findAll(tableName: String, clazz: Class, orderBy: Co * @param docId The ID of the document to retrieve * @param clazz The class of the document to be returned * @return The document if it is found, `null` otherwise + * @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) @@ -272,7 +290,9 @@ fun Connection.findById(tableName: String, docId: TKey, clazz: Clas * @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, @@ -332,6 +352,7 @@ fun Connection.findByJsonPath( * @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 + * @throws DocumentException If no dialect has been configured, or if parameters are invalid */ @Throws(DocumentException::class) @JvmOverloads @@ -392,7 +413,9 @@ fun Connection.findFirstByJsonPath( * @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) @@ -403,7 +426,9 @@ fun Connection.patchById(tableName: String, docId: TKey, patch: T * @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) fun Connection.patchByFields( tableName: String, fields: Collection>, @@ -448,7 +473,9 @@ fun Connection.patchByJsonPath(tableName: String, path: String, patch: * @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) @@ -459,7 +486,9 @@ fun Connection.removeFieldsById(tableName: String, docId: TKey, toRemove: * @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) fun Connection.removeFieldsByFields( tableName: String, fields: Collection>, @@ -503,7 +532,9 @@ fun Connection.removeFieldsByJsonPath(tableName: String, path: String, toRemove: * * @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) @@ -513,7 +544,9 @@ fun Connection.deleteById(tableName: String, docId: TKey) = * @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) @@ -536,5 +569,6 @@ fun Connection.deleteByContains(tableName: String, criteria: TContai * @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/jvm/src/main/kotlin/jvm/Delete.kt b/src/jvm/src/main/kotlin/jvm/Delete.kt index d442d7f..7cba82a 100644 --- a/src/jvm/src/main/kotlin/jvm/Delete.kt +++ b/src/jvm/src/main/kotlin/jvm/Delete.kt @@ -4,6 +4,7 @@ import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customNonQuery import solutions.bitbadger.documents.query.DeleteQuery import java.sql.Connection +import kotlin.jvm.Throws /** * Functions to delete documents @@ -16,7 +17,9 @@ object Delete { * @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) = conn.customNonQuery( @@ -29,7 +32,9 @@ object Delete { * * @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) } @@ -41,7 +46,9 @@ object Delete { * @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) { @@ -55,7 +62,9 @@ object Delete { * @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) = @@ -79,7 +88,7 @@ object Delete { * * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic @@ -104,7 +113,7 @@ object Delete { * * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic diff --git a/src/jvm/src/main/kotlin/jvm/Document.kt b/src/jvm/src/main/kotlin/jvm/Document.kt index 7b2e0eb..69c4e64 100644 --- a/src/jvm/src/main/kotlin/jvm/Document.kt +++ b/src/jvm/src/main/kotlin/jvm/Document.kt @@ -2,12 +2,14 @@ package solutions.bitbadger.documents.jvm import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.extensions.customNonQuery 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 @@ -20,7 +22,9 @@ object 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 @@ -37,7 +41,10 @@ object 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) } @@ -48,7 +55,9 @@ object Document { * @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) = conn.customNonQuery(DocumentQuery.save(tableName), listOf(Parameters.json(":data", document))) @@ -58,7 +67,9 @@ object Document { * * @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) } @@ -70,7 +81,9 @@ object Document { * @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) = conn.customNonQuery( @@ -87,7 +100,9 @@ object Document { * @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/jvm/src/main/kotlin/jvm/Exists.kt b/src/jvm/src/main/kotlin/jvm/Exists.kt index 96d3cfb..990129e 100644 --- a/src/jvm/src/main/kotlin/jvm/Exists.kt +++ b/src/jvm/src/main/kotlin/jvm/Exists.kt @@ -4,6 +4,7 @@ import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customScalar import solutions.bitbadger.documents.query.ExistsQuery import java.sql.Connection +import kotlin.jvm.Throws /** * Functions to determine whether documents exist @@ -17,7 +18,9 @@ object Exists { * @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) = conn.customScalar( @@ -33,7 +36,9 @@ object Exists { * @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) } @@ -46,7 +51,9 @@ object Exists { * @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( @@ -71,7 +78,9 @@ object Exists { * @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) = @@ -102,7 +111,7 @@ object Exists { * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic @@ -134,7 +143,7 @@ object Exists { * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic diff --git a/src/jvm/src/main/kotlin/jvm/Find.kt b/src/jvm/src/main/kotlin/jvm/Find.kt index 59948cc..b63d52f 100644 --- a/src/jvm/src/main/kotlin/jvm/Find.kt +++ b/src/jvm/src/main/kotlin/jvm/Find.kt @@ -6,6 +6,7 @@ import solutions.bitbadger.documents.extensions.customSingle import solutions.bitbadger.documents.query.FindQuery import solutions.bitbadger.documents.query.orderBy import java.sql.Connection +import kotlin.jvm.Throws /** * Functions to find and retrieve documents @@ -20,7 +21,9 @@ object Find { * @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) = conn.customList(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), clazz, Results::fromData) @@ -32,7 +35,9 @@ object Find { * @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) = @@ -45,7 +50,9 @@ object Find { * @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) @@ -58,7 +65,9 @@ object Find { * @param clazz The class of the document to be returned * @param conn The connection over which documents should be retrieved * @return The document if it is found, `null` otherwise + * @throws DocumentException If no dialect has been configured */ + @Throws(DocumentException::class) @JvmStatic fun byId(tableName: String, docId: TKey, clazz: Class, conn: Connection) = conn.customSingle( @@ -75,7 +84,9 @@ object Find { * @param docId The ID of the document to retrieve * @param clazz The class of the document to be returned * @return The document if it is found, `null` otherwise + * @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) } @@ -90,7 +101,9 @@ object Find { * @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, @@ -118,7 +131,9 @@ object Find { * @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( @@ -139,7 +154,9 @@ object Find { * @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, @@ -185,7 +202,7 @@ object Find { * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic @@ -248,7 +265,7 @@ object Find { * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic @@ -281,7 +298,9 @@ object Find { * @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 + * @throws DocumentException If no dialect has been configured, or if parameters are invalid */ + @Throws(DocumentException::class) @JvmStatic fun firstByFields( tableName: String, @@ -309,7 +328,9 @@ object Find { * @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 + * @throws DocumentException If no connection string has been set, or if parameters are invalid */ + @Throws(DocumentException::class) @JvmStatic @JvmOverloads fun firstByFields( @@ -330,7 +351,9 @@ object Find { * @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 + * @throws DocumentException If no dialect has been configured, or if parameters are invalid */ + @Throws(DocumentException::class) @JvmStatic fun firstByFields( tableName: String, @@ -395,7 +418,7 @@ object Find { * @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 The first document matching the JSON containment query, or `null` if no matches are found - * @throws DocumentException If called on a SQLite connection + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic @@ -458,7 +481,7 @@ object Find { * @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 The first document matching the JSON Path match query, or `null` if no matches are found - * @throws DocumentException If called on a SQLite connection + * @throws DocumentException If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic diff --git a/src/jvm/src/main/kotlin/jvm/Patch.kt b/src/jvm/src/main/kotlin/jvm/Patch.kt index 1b09991..1097d9c 100644 --- a/src/jvm/src/main/kotlin/jvm/Patch.kt +++ b/src/jvm/src/main/kotlin/jvm/Patch.kt @@ -4,6 +4,7 @@ import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customNonQuery import solutions.bitbadger.documents.query.PatchQuery import java.sql.Connection +import kotlin.jvm.Throws /** * Functions to patch (partially update) documents @@ -17,7 +18,9 @@ object Patch { * @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) = conn.customNonQuery( @@ -34,7 +37,9 @@ object Patch { * @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) } @@ -47,7 +52,9 @@ object Patch { * @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, @@ -72,7 +79,9 @@ object Patch { * @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( @@ -106,7 +115,7 @@ object Patch { * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic @@ -136,7 +145,7 @@ object Patch { * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic diff --git a/src/jvm/src/main/kotlin/jvm/RemoveFields.kt b/src/jvm/src/main/kotlin/jvm/RemoveFields.kt index 0484b1e..b69fbf1 100644 --- a/src/jvm/src/main/kotlin/jvm/RemoveFields.kt +++ b/src/jvm/src/main/kotlin/jvm/RemoveFields.kt @@ -4,6 +4,7 @@ import solutions.bitbadger.documents.* import solutions.bitbadger.documents.extensions.customNonQuery import solutions.bitbadger.documents.query.RemoveFieldsQuery import java.sql.Connection +import kotlin.jvm.Throws /** * Functions to remove fields from documents @@ -31,7 +32,9 @@ object RemoveFields { * @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) @@ -50,7 +53,9 @@ object RemoveFields { * @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) } @@ -63,7 +68,9 @@ object RemoveFields { * @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, @@ -87,7 +94,9 @@ object RemoveFields { * @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( @@ -128,7 +137,7 @@ object RemoveFields { * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic @@ -160,7 +169,7 @@ object RemoveFields { * @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 If no connection string has been set, or if called on a SQLite connection */ @Throws(DocumentException::class) @JvmStatic diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java new file mode 100644 index 0000000..2a7ef00 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java @@ -0,0 +1,48 @@ +package solutions.bitbadger.documents.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("JVM | 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/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java index 4d5b085..8b118cc 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java @@ -1,10 +1,12 @@ package solutions.bitbadger.documents.java.support; +import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.jvm.Document; import solutions.bitbadger.documents.support.ThrowawayDatabase; import java.util.List; +import static org.junit.jupiter.api.Assertions.fail; import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; public class JsonDocument { @@ -69,8 +71,12 @@ public class JsonDocument { new JsonDocument("five", "purple", 18)); public static void load(ThrowawayDatabase db, String tableName) { - for (JsonDocument doc : testDocuments) { - Document.insert(tableName, doc, db.getConn()); + try { + for (JsonDocument doc : testDocuments) { + Document.insert(tableName, doc, db.getConn()); + } + } catch (DocumentException ex) { + fail("Could not load test documents", ex); } } -- 2.47.2 From a4b956cace1f353bcc31c788592923290a7169ed Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 16 Mar 2025 23:13:47 -0400 Subject: [PATCH 51/88] WIP on Scala and Groovy test structure --- .idea/encodings.xml | 1 + .idea/modules.xml | 8 + pom.xml | 2 +- src/jvm/jvm.iml | 8 + src/jvm/pom.xml | 67 +++++++ .../documents/groovy/AutoIdTest.groovy | 166 ++++++++++++++++++ .../documents/groovy/FieldMatchTest.groovy | 26 +++ .../groovy/support/ByteIdClass.groovy | 9 + .../groovy/support/IntIdClass.groovy | 9 + .../groovy/support/LongIdClass.groovy | 9 + .../groovy/support/ShortIdClass.groovy | 9 + .../groovy/support/StringIdClass.groovy | 9 + .../documents/scala/FieldMatchTest.scala | 23 +++ 13 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 .idea/modules.xml create mode 100644 src/jvm/jvm.iml create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ByteIdClass.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/IntIdClass.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/LongIdClass.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ShortIdClass.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/StringIdClass.groovy create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 2afc1f9..3a6db4a 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -5,6 +5,7 @@ + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..46a3f3f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6871788..b32d087 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ solutions.bitbadger documents 4.0.0-alpha1-SNAPSHOT - jar + pom ${project.groupId}:${project.artifactId} Expose a document store interface for PostgreSQL and SQLite diff --git a/src/jvm/jvm.iml b/src/jvm/jvm.iml new file mode 100644 index 0000000..3eef5d9 --- /dev/null +++ b/src/jvm/jvm.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/jvm/pom.xml b/src/jvm/pom.xml index 33c4d65..af846aa 100644 --- a/src/jvm/pom.xml +++ b/src/jvm/pom.xml @@ -36,6 +36,18 @@ ${jackson.version} test + + org.scalatest + scalatest_3 + 3.2.9 + test + + + org.codehaus.groovy + groovy-test-junit5 + 3.0.24 + test + @@ -68,15 +80,69 @@ ${project.basedir}/src/test/kotlin ${project.basedir}/src/test/java + ${project.basedir}/src/test/scala + + net.alchim31.maven + scala-maven-plugin + 4.9.2 + + + + testCompile + + + + + ${java.version} + ${java.version} + + + + org.codehaus.gmaven + gmaven-plugin + 1.5 + + ${java.version} + + + + + 2.0 + + + generateTestStubs + testCompile + + + + maven-surefire-plugin 2.22.2 + + org.scalatest + scalatest-maven-plugin + 2.2.0 + + ${project.build.directory}/surefire-reports + . + WDF TestSuite.txt + + + + test + + test + + + + maven-failsafe-plugin 2.22.2 @@ -92,6 +158,7 @@ org.apache.maven.plugins maven-compiler-plugin + 3.13.0 ${java.version} ${java.version} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy new file mode 100644 index 0000000..ee64e9d --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy @@ -0,0 +1,166 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.groovy.support.* + +import static org.junit.jupiter.api.Assertions.* + +/** + * Unit tests for the `AutoId` enum + */ +@DisplayName('JVM | 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.class) { AutoId.needsAutoId(AutoId.DISABLED, null, 'id') } + } + + @Test + @DisplayName('needsAutoId fails for missing ID property') + void needsAutoIdFailsForMissingId() { + assertThrows(DocumentException.class) { 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.class) { 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.class) { 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.class) { + AutoId.needsAutoId(AutoId.RANDOM_STRING, new ShortIdClass((short) 55), 'id') + } + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy new file mode 100644 index 0000000..1573341 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.FieldMatch + +import static org.junit.jupiter.api.Assertions.assertEquals + +/** + * Unit tests for the `FieldMatch` enum + */ +@DisplayName("JVM | 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ByteIdClass.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ByteIdClass.groovy new file mode 100644 index 0000000..62442f6 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ByteIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.support + +class ByteIdClass { + byte id + + ByteIdClass(byte id) { + this.id = id + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/IntIdClass.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/IntIdClass.groovy new file mode 100644 index 0000000..63d7f75 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/IntIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.support + +class IntIdClass { + int id + + IntIdClass(int id) { + this.id = id + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/LongIdClass.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/LongIdClass.groovy new file mode 100644 index 0000000..377fa1d --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/LongIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.support + +class LongIdClass { + long id + + LongIdClass(long id) { + this.id = id + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ShortIdClass.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ShortIdClass.groovy new file mode 100644 index 0000000..fca6ea0 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ShortIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.support + +class ShortIdClass { + short id + + ShortIdClass(short id) { + this.id = id + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/StringIdClass.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/StringIdClass.groovy new file mode 100644 index 0000000..90e68e9 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/StringIdClass.groovy @@ -0,0 +1,9 @@ +package solutions.bitbadger.documents.groovy.support + +class StringIdClass { + String id + + StringIdClass(String id) { + this.id = id + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala new file mode 100644 index 0000000..214158f --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala @@ -0,0 +1,23 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import solutions.bitbadger.documents.FieldMatch + +/** + * Unit tests for the `FieldMatch` enum + */ +class FieldMatchTest extends AnyFunSpec { + + describe("sql") { + describe("ANY") { + it("should use OR") { + assert("OR" == FieldMatch.ANY.getSql) + } + } + describe("ALL") { + it("should use AND") { + assert("AND" == FieldMatch.ALL.getSql) + } + } + } +} -- 2.47.2 From 57c6176997222b8784f9702e5197da8f73f69906 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Mar 2025 10:54:34 -0400 Subject: [PATCH 52/88] WIP on Scala, Groovy unit tests --- .idea/codeStyles/Project.xml | 7 + .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/libraries/Maven__scala_sdk_3_0_0.xml | 26 ++++ .idea/scala_compiler.xml | 7 + .idea/scala_settings.xml | 6 + src/jvm/jvm.iml | 1 + src/jvm/pom.xml | 9 ++ .../documents/groovy/AutoIdTest.groovy | 121 +++++++++--------- .../documents/groovy/ConfigurationTest.groovy | 48 +++++++ .../documents/groovy/DialectTest.groovy | 42 ++++++ .../documents/groovy/FieldMatchTest.groovy | 12 +- .../bitbadger/documents/java/DialectTest.java | 43 +++++++ src/jvm/src/test/kotlin/DialectTest.kt | 4 +- .../documents/scala/AutoIdSpec.scala | 85 ++++++++++++ .../documents/scala/ConfigurationSpec.scala | 37 ++++++ .../documents/scala/DialectSpec.scala | 26 ++++ ...ldMatchTest.scala => FieldMatchSpec.scala} | 2 +- .../documents/scala/support/ByteIdClass.scala | 3 + .../documents/scala/support/IntIdClass.scala | 3 + .../documents/scala/support/LongIdClass.scala | 3 + .../scala/support/ShortIdClass.scala | 3 + .../scala/support/StringIdClass.scala | 3 + 22 files changed, 430 insertions(+), 66 deletions(-) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/libraries/Maven__scala_sdk_3_0_0.xml create mode 100644 .idea/scala_compiler.xml create mode 100644 .idea/scala_settings.xml create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/DialectTest.java create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala rename src/jvm/src/test/scala/solutions/bitbadger/documents/scala/{FieldMatchTest.scala => FieldMatchSpec.scala} (91%) create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ByteIdClass.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/IntIdClass.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/LongIdClass.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ShortIdClass.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/StringIdClass.scala 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/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/scala_compiler.xml b/.idea/scala_compiler.xml new file mode 100644 index 0000000..6cdd90a --- /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/src/jvm/jvm.iml b/src/jvm/jvm.iml index 3eef5d9..d6f3c7a 100644 --- a/src/jvm/jvm.iml +++ b/src/jvm/jvm.iml @@ -2,6 +2,7 @@ + diff --git a/src/jvm/pom.xml b/src/jvm/pom.xml index af846aa..674731b 100644 --- a/src/jvm/pom.xml +++ b/src/jvm/pom.xml @@ -42,6 +42,12 @@ 3.2.9 test + + org.codehaus.groovy + groovy-test + 3.0.24 + test + org.codehaus.groovy groovy-test-junit5 @@ -124,6 +130,9 @@ maven-surefire-plugin 2.22.2 + + --add-opens java.base/java.lang=ALL-UNNAMED + org.scalatest diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy index ee64e9d..52c79e2 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy @@ -6,7 +6,7 @@ import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.groovy.support.* -import static org.junit.jupiter.api.Assertions.* +import static groovy.test.GroovyAssert.* /** * Unit tests for the `AutoId` enum @@ -17,21 +17,21 @@ class AutoIdTest { @Test @DisplayName('Generates a UUID string') void generateUUID() { - assertEquals(32, AutoId.generateUUID().length(), 'The UUID should have been a 32-character string') + assertEquals('The UUID should have been a 32-character string', 32, AutoId.generateUUID().length()) } @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") + assertEquals("There should have been 8 characters in $result", 8, result.length()) } @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") + assertEquals("There should have been 11 characters in $result", 11, result.length()) } @Test @@ -39,128 +39,133 @@ class AutoIdTest { void generateRandomStringIsRandom() { def result1 = AutoId.generateRandomString 16 def result2 = AutoId.generateRandomString 16 - assertNotEquals(result1, result2, 'There should have been 2 different strings generated') + assertNotEquals('There should have been 2 different strings generated', result1, result2) } - @Test - @DisplayName('needsAutoId fails for null document') - void needsAutoIdFailsForNullDocument() { - assertThrows(DocumentException.class) { AutoId.needsAutoId(AutoId.DISABLED, null, 'id') } - } - - @Test - @DisplayName('needsAutoId fails for missing ID property') - void needsAutoIdFailsForMissingId() { - assertThrows(DocumentException.class) { AutoId.needsAutoId(AutoId.UUID, new IntIdClass(0), 'Id') } - } +// TODO: resolve java.base open issue +// @Test +// @DisplayName('needsAutoId fails for null document') +// void needsAutoIdFailsForNullDocument() { +// assertThrows(DocumentException) { AutoId.needsAutoId(AutoId.DISABLED, null, 'id') } +// } +// +// TODO: resolve java.base open issue +// @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') + assertFalse('Disabled Auto ID should always return false', AutoId.needsAutoId(AutoId.DISABLED, '', '')) } @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') + assertTrue('Number Auto ID with 0 should return true', + AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 0), 'id')) } @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') + assertFalse('Number Auto ID with 77 should return false', + AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 77), 'id')) } @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') + assertTrue('Number Auto ID with 0 should return true', + AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 0), 'id')) } @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') + assertFalse('Number Auto ID with 31 should return false', + AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 31), 'id')) } @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') + assertTrue('Number Auto ID with 0 should return true', + AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(0), 'id')) } @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') + assertFalse('Number Auto ID with 6 should return false', + AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(6), 'id')) } @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') + assertTrue('Number Auto ID with 0 should return true', + AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(0L), 'id')) } @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') + assertFalse('Number Auto ID with 2 should return false', + AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(2L), 'id')) } - @Test - @DisplayName('needsAutoId fails for Number strategy and non-number ID') - void needsAutoIdFailsForNumberWithStringId() { - assertThrows(DocumentException.class) { AutoId.needsAutoId(AutoId.NUMBER, new StringIdClass(''), 'id') } - } +// TODO: resolve java.base open issue +// @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') + assertTrue('UUID Auto ID with blank should return true', + AutoId.needsAutoId(AutoId.UUID, new StringIdClass(''), 'id')) } @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') + assertFalse('UUID Auto ID with non-blank should return false', + AutoId.needsAutoId(AutoId.UUID, new StringIdClass('howdy'), 'id')) } - @Test - @DisplayName('needsAutoId fails for UUID strategy and non-string ID') - void needsAutoIdFailsForUUIDNonString() { - assertThrows(DocumentException.class) { AutoId.needsAutoId(AutoId.UUID, new IntIdClass(5), 'id') } - } +// TODO: resolve java.base open issue +// @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') + assertTrue('Random String Auto ID with blank should return true', + AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass(''), 'id')) } @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') + assertFalse('Random String Auto ID with non-blank should return false', + AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass('full'), 'id')) } - @Test - @DisplayName('needsAutoId fails for Random String strategy and non-string ID') - void needsAutoIdFailsForRandomNonString() { - assertThrows(DocumentException.class) { - AutoId.needsAutoId(AutoId.RANDOM_STRING, new ShortIdClass((short) 55), 'id') - } - } +// TODO: resolve java.base open issue +// @Test +// @DisplayName('needsAutoId fails for Random String strategy and non-string ID') +// void needsAutoIdFailsForRandomNonString() { +// assertThrows(DocumentException.class) { +// AutoId.needsAutoId(AutoId.RANDOM_STRING, new ShortIdClass((short) 55), 'id') +// } +// } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy new file mode 100644 index 0000000..d276c7a --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy @@ -0,0 +1,48 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect +//import solutions.bitbadger.documents.DocumentException + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Configuration` object + */ +@DisplayName('JVM | Groovy | Configuration') +class ConfigurationTest { + + @Test + @DisplayName('Default ID field is "id"') + void defaultIdField() { + assertEquals('Default ID field incorrect', 'id', Configuration.idField) + } + + @Test + @DisplayName('Default Auto ID strategy is DISABLED') + void defaultAutoId() { + assertEquals('Default Auto ID strategy should be DISABLED', AutoId.DISABLED, Configuration.autoIdStrategy) + } + + @Test + @DisplayName('Default ID string length should be 16') + void defaultIdStringLength() { + assertEquals('Default ID string length should be 16', 16, Configuration.idStringLength) + } + + @Test + @DisplayName('Dialect is derived from connection string') + void dialectIsDerived() { + try { + // TODO: uncomment once java.base open issue resolved + //assertThrows(DocumentException) { Configuration.dialect() } + Configuration.connectionString = 'jdbc:postgresql:db' + assertEquals(Dialect.POSTGRESQL, Configuration.dialect()) + } finally { + Configuration.connectionString = null + } + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy new file mode 100644 index 0000000..5d95b99 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy @@ -0,0 +1,42 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.DocumentException + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Dialect` enum + */ +@DisplayName('JVM | Groovy | Dialect') +class DialectTest { + + @Test + @DisplayName('deriveFromConnectionString derives PostgreSQL correctly') + void derivesPostgres() { + assertEquals('Dialect should have been PostgreSQL', Dialect.POSTGRESQL, + Dialect.deriveFromConnectionString('jdbc:postgresql:db')) + } + + @Test + @DisplayName('deriveFromConnectionString derives SQLite correctly') + void derivesSQLite() { + assertEquals('Dialect should have been SQLite', Dialect.SQLITE, + Dialect.deriveFromConnectionString('jdbc:sqlite:memory')) + } + + @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 'The exception message should not have been null', ex.message + assertTrue('The connection string should have been in the exception message', + ex.message.contains('[SQL Server]')) + } + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy index 1573341..6474dbb 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy @@ -4,23 +4,23 @@ 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 +import static groovy.test.GroovyAssert.assertEquals /** * Unit tests for the `FieldMatch` enum */ -@DisplayName("JVM | Groovy | FieldMatch") +@DisplayName('JVM | Groovy | FieldMatch') class FieldMatchTest { @Test - @DisplayName("ANY uses proper SQL") + @DisplayName('ANY uses proper SQL') void anySQL() { - assertEquals("OR", FieldMatch.ANY.sql, "ANY should use OR") + assertEquals('ANY should use OR', 'OR', FieldMatch.ANY.sql) } @Test - @DisplayName("ALL uses proper SQL") + @DisplayName('ALL uses proper SQL') void allSQL() { - assertEquals("AND", FieldMatch.ALL.sql, "ALL should use AND") + assertEquals('ALL should use AND', 'AND', FieldMatch.ALL.sql) } } diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/DialectTest.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/DialectTest.java new file mode 100644 index 0000000..91fbb2f --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/DialectTest.java @@ -0,0 +1,43 @@ +package solutions.bitbadger.documents.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("JVM | 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/jvm/src/test/kotlin/DialectTest.kt b/src/jvm/src/test/kotlin/DialectTest.kt index f6b4025..eb97769 100644 --- a/src/jvm/src/test/kotlin/DialectTest.kt +++ b/src/jvm/src/test/kotlin/DialectTest.kt @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue +import kotlin.test.fail /** * Unit tests for the `Dialect` enum @@ -20,7 +21,7 @@ class DialectTest { "Dialect should have been PostgreSQL") @Test - @DisplayName("deriveFromConnectionString derives PostgreSQL correctly") + @DisplayName("deriveFromConnectionString derives SQLite correctly") fun derivesSQLite() = assertEquals( Dialect.SQLITE, Dialect.deriveFromConnectionString("jdbc:sqlite:memory"), @@ -31,6 +32,7 @@ class DialectTest { 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]"), diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala new file mode 100644 index 0000000..ecfa9a2 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala @@ -0,0 +1,85 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import solutions.bitbadger.documents.scala.support.{ByteIdClass, IntIdClass, LongIdClass, ShortIdClass, StringIdClass} +import solutions.bitbadger.documents.{AutoId, DocumentException} + +class AutoIdSpec extends AnyFunSpec { + + describe("generateUUID") { + it("generates a UUID string") { + assert(32 == AutoId.generateUUID().length) + } + } + + describe("generateRandomString") { + it("generates a random hex character string of an even length") { + assert(8 == AutoId.generateRandomString(8).length) + } + it("generates a random hex character string of an odd length") { + assert(11 == AutoId.generateRandomString(11).length) + } + it("generates different random hex character strings") { + val result1 = AutoId.generateRandomString(16) + val result2 = AutoId.generateRandomString(16) + assert(result1 != result2) + } + } + + describe("needsAutoId") { + it("fails for null document") { + assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.DISABLED, null, "id") } + } + it("fails for missing ID property") { + assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } + } + it("returns false if disabled") { + assert(!AutoId.needsAutoId(AutoId.DISABLED, "", "")) + } + it("returns true for Number strategy and byte ID of 0") { + assert(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id")) + } + it("returns false for Number strategy and byte ID of non-0") { + assert(!AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id")) + } + it("returns true for Number strategy and short ID of 0") { + assert(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id")) + } + it("returns false for Number strategy and short ID of non-0") { + assert(!AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id")) + } + it("returns true for Number strategy and int ID of 0") { + assert(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id")) + } + it("returns false for Number strategy and int ID of non-0") { + assert(!AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id")) + } + it("returns true for Number strategy and long ID of 0") { + assert(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id")) + } + it("returns false for Number strategy and long ID of non-0") { + assert(!AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id")) + } + it("fails for Number strategy and non-number ID") { + assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } + } + it("returns true for UUID strategy and blank ID") { + assert(AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id")) + } + it("returns false for UUID strategy and non-blank ID") { + assert(!AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id")) + } + it("fails for UUID strategy and non-string ID") { + assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } + } + it("returns true for Random String strategy and blank ID") { + assert(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id")) + } + it("returns false for Random String strategy and non-blank ID") { + assert(!AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id")) + } + it("fails for Random String strategy and non-string ID") { + assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala new file mode 100644 index 0000000..ae08e39 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala @@ -0,0 +1,37 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import solutions.bitbadger.documents.{AutoId, Configuration, Dialect, DocumentException} + +class ConfigurationSpec extends AnyFunSpec { + + describe("idField") { + it("defaults to `id`") { + assert("id" == Configuration.idField) + } + } + + describe("autoIdStrategy") { + it("defaults to `DISABLED`") { + assert(AutoId.DISABLED == Configuration.autoIdStrategy) + } + } + + describe("idStringLength") { + it("defaults to 16") { + assert(16 == Configuration.idStringLength) + } + } + + describe("dialect") { + it("is derived from connection string") { + try { + assertThrows[DocumentException] { Configuration.dialect() } + Configuration.setConnectionString("jdbc:postgresql:db") + assert(Dialect.POSTGRESQL == Configuration.dialect()) + } finally { + Configuration.setConnectionString(null) + } + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala new file mode 100644 index 0000000..1e56812 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import solutions.bitbadger.documents.{Dialect, DocumentException} + +class DialectSpec extends AnyFunSpec { + + describe("deriveFromConnectionString") { + it("derives PostgreSQL correctly") { + assert(Dialect.POSTGRESQL == Dialect.deriveFromConnectionString("jdbc:postgresql:db")) + } + it("derives SQLite correctly") { + assert(Dialect.SQLITE == Dialect.deriveFromConnectionString("jdbc:sqlite:memory")) + } + it("fails when the connection string is unknown") { + try { + Dialect.deriveFromConnectionString("SQL Server") + fail("Dialect derivation should have failed") + } catch { + case ex: DocumentException => + assert(ex.getMessage != null) + assert(ex.getMessage.contains("[SQL Server]")) + } + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala similarity index 91% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala rename to src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala index 214158f..25d5625 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala @@ -6,7 +6,7 @@ import solutions.bitbadger.documents.FieldMatch /** * Unit tests for the `FieldMatch` enum */ -class FieldMatchTest extends AnyFunSpec { +class FieldMatchSpec extends AnyFunSpec { describe("sql") { describe("ANY") { diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ByteIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ByteIdClass.scala new file mode 100644 index 0000000..611fd75 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ByteIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.support + +class ByteIdClass(var id: Byte) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/IntIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/IntIdClass.scala new file mode 100644 index 0000000..2c4e6f1 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/IntIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.support + +class IntIdClass(var id: Int) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/LongIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/LongIdClass.scala new file mode 100644 index 0000000..a66d738 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/LongIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.support + +class LongIdClass(var id: Long) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ShortIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ShortIdClass.scala new file mode 100644 index 0000000..a81dfc4 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ShortIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.support + +class ShortIdClass(var id: Short) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/StringIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/StringIdClass.scala new file mode 100644 index 0000000..11976a4 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/StringIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.support + +class StringIdClass(var id: String) -- 2.47.2 From 2b86864f82fe5f4119140b6f4d33c17b042f7ad5 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Mar 2025 17:16:45 -0400 Subject: [PATCH 53/88] Finish root ns Groovy/Scala tests --- .../documents/groovy/DocumentIndexTest.groovy | 26 + .../documents/groovy/FieldTest.groovy | 600 ++++++++++++++++++ .../bitbadger/documents/groovy/OpTest.groovy | 80 +++ .../documents/groovy/ParameterNameTest.groovy | 30 + .../documents/groovy/ParameterTest.groovy | 41 ++ src/jvm/src/test/kotlin/FieldTest.kt | 2 +- .../documents/scala/AutoIdSpec.scala | 47 +- .../documents/scala/ClearConfiguration.scala | 11 + .../documents/scala/ConfigurationSpec.scala | 13 +- .../documents/scala/DialectSpec.scala | 11 +- .../documents/scala/DocumentIndexSpec.scala | 17 + .../documents/scala/FieldMatchSpec.scala | 15 +- .../bitbadger/documents/scala/FieldSpec.scala | 428 +++++++++++++ .../bitbadger/documents/scala/OpSpec.scala | 44 ++ .../documents/scala/ParameterNameSpec.scala | 23 + .../documents/scala/ParameterSpec.scala | 26 + 16 files changed, 1370 insertions(+), 44 deletions(-) create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy new file mode 100644 index 0000000..03eb50d --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentIndex + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `DocumentIndex` enum + */ +@DisplayName('JVM | Groovy | DocumentIndex') +class DocumentIndexTest { + + @Test + @DisplayName('FULL uses proper SQL') + void fullSQL() { + assertEquals('The SQL for Full is incorrect', '', DocumentIndex.FULL.sql) + } + + @Test + @DisplayName('OPTIMIZED uses proper SQL') + void optimizedSQL() { + assertEquals('The SQL for Optimized is incorrect', ' jsonb_path_ops', DocumentIndex.OPTIMIZED.sql) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy new file mode 100644 index 0000000..e03e0e9 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy @@ -0,0 +1,600 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Dialect +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldFormat +import solutions.bitbadger.documents.Op +import solutions.bitbadger.documents.support.ForceDialect + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Field` class + */ +@DisplayName('JVM | Groovy | Field') +class FieldTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + // ~~~ INSTANCE METHODS ~~~ + +// TODO: fix java.base open issue +// @Test +// @DisplayName('withParameterName fails for invalid name') +// void withParamNameFails() { +// assertThrows(DocumentException) { Field.equal('it', '').withParameterName('2424') } +// } + + @Test + @DisplayName('withParameterName works with colon prefix') + void withParamNameColon() { + def field = Field.equal('abc', '22').withQualifier('me') + def withParam = field.withParameterName(':test') + assertNotSame('A new Field instance should have been created', field, withParam) + assertEquals('Name should have been preserved', field.name, withParam.name) + assertEquals('Comparison should have been preserved', field.comparison, withParam.comparison) + assertEquals('Parameter name not set correctly', ':test', withParam.parameterName) + assertEquals('Qualifier should have been preserved', field.qualifier, withParam.qualifier) + } + + @Test + @DisplayName('withParameterName works with at-sign prefix') + void withParamNameAtSign() { + def field = Field.equal('def', '44') + def withParam = field.withParameterName('@unit') + assertNotSame('A new Field instance should have been created', field, withParam) + assertEquals('Name should have been preserved', field.name, withParam.name) + assertEquals('Comparison should have been preserved', field.comparison, withParam.comparison) + assertEquals('Parameter name not set correctly', '@unit', withParam.parameterName) + assertEquals('Qualifier should have been preserved', field.qualifier, withParam.qualifier) + } + + @Test + @DisplayName('withQualifier sets qualifier correctly') + void withQualifier() { + def field = Field.equal('j', 'k') + def withQual = field.withQualifier('test') + assertNotSame('A new Field instance should have been created', field, withQual) + assertEquals('Name should have been preserved', field.name, withQual.name) + assertEquals('Comparison should have been preserved', field.comparison, withQual.comparison) + assertEquals('Parameter Name should have been preserved', field.parameterName, withQual.parameterName) + assertEquals('Qualifier not set correctly', 'test', withQual.qualifier) + } + + @Test + @DisplayName('path generates for simple unqualified PostgreSQL field') + void pathPostgresSimpleUnqualified() { + assertEquals('Path not correct', "data->>'SomethingCool'", + Field.greaterOrEqual('SomethingCool', 18).path(Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for simple qualified PostgreSQL field') + void pathPostgresSimpleQualified() { + assertEquals('Path not correct', "this.data->>'SomethingElse'", + Field.less('SomethingElse', 9).withQualifier('this').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for nested unqualified PostgreSQL field') + void pathPostgresNestedUnqualified() { + assertEquals('Path not correct', "data#>>'{My,Nested,Field}'", + Field.equal('My.Nested.Field', 'howdy').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for nested qualified PostgreSQL field') + void pathPostgresNestedQualified() { + assertEquals('Path not correct', "bird.data#>>'{Nest,Away}'", + Field.equal('Nest.Away', 'doc').withQualifier('bird').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for simple unqualified SQLite field') + void pathSQLiteSimpleUnqualified() { + assertEquals('Path not correct', "data->>'SomethingCool'", + Field.greaterOrEqual('SomethingCool', 18).path(Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for simple qualified SQLite field') + void pathSQLiteSimpleQualified() { + assertEquals('Path not correct', "this.data->>'SomethingElse'", + Field.less('SomethingElse', 9).withQualifier('this').path(Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for nested unqualified SQLite field') + void pathSQLiteNestedUnqualified() { + assertEquals('Path not correct', "data->'My'->'Nested'->>'Field'", + Field.equal('My.Nested.Field', 'howdy').path(Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('path generates for nested qualified SQLite field') + void pathSQLiteNestedQualified() { + assertEquals('Path not correct', "bird.data->'Nest'->>'Away'", + Field.equal('Nest.Away', 'doc').withQualifier('bird').path(Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('toWhere generates for exists w/o qualifier | PostgreSQL') + void toWhereExistsNoQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'that_field' IS NOT NULL", + Field.exists('that_field').toWhere()) + } + + @Test + @DisplayName('toWhere generates for exists w/o qualifier | SQLite') + void toWhereExistsNoQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'that_field' IS NOT NULL", + Field.exists('that_field').toWhere()) + } + + @Test + @DisplayName('toWhere generates for not-exists w/o qualifier | PostgreSQL') + void toWhereNotExistsNoQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'a_field' IS NULL", + Field.notExists('a_field').toWhere()) + } + + @Test + @DisplayName('toWhere generates for not-exists w/o qualifier | SQLite') + void toWhereNotExistsNoQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'a_field' IS NULL", + Field.notExists('a_field').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier, numeric range | PostgreSQL') + void toWhereBetweenNoQualNumericPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', + "(data->>'age')::numeric BETWEEN @agemin AND @agemax", Field.between('age', 13, 17, '@age').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier, alphanumeric range | PostgreSQL') + void toWhereBetweenNoQualAlphaPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'city' BETWEEN :citymin AND :citymax", + Field.between('city', 'Atlanta', 'Chicago', ':city').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/o qualifier | SQLite') + void toWhereBetweenNoQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'age' BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier, numeric range | PostgreSQL') + void toWhereBetweenQualNumericPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', + "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').withQualifier('test').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier, alphanumeric range | PostgreSQL') + void toWhereBetweenQualAlphaPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "unit.data->>'city' BETWEEN :citymin AND :citymax", + Field.between('city', 'Atlanta', 'Chicago', ':city').withQualifier('unit').toWhere()) + } + + @Test + @DisplayName('toWhere generates for BETWEEN w/ qualifier | SQLite') + void toWhereBetweenQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "my.data->>'age' BETWEEN @agemin AND @agemax", + Field.between('age', 13, 17, '@age').withQualifier('my').toWhere()) + } + + @Test + @DisplayName('toWhere generates for IN/any, numeric values | PostgreSQL') + void toWhereAnyNumericPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', + "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", + Field.any('even', List.of(2, 4, 6), ':nbr').toWhere()) + } + + @Test + @DisplayName('toWhere generates for IN/any, alphanumeric values | PostgreSQL') + void toWhereAnyAlphaPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'test' IN (:city_0, :city_1)", + Field.any('test', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + } + + @Test + @DisplayName('toWhere generates for IN/any | SQLite') + void toWhereAnySQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'test' IN (:city_0, :city_1)", + Field.any('test', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + } + + @Test + @DisplayName('toWhere generates for inArray | PostgreSQL') + void toWhereInArrayPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]", + Field.inArray('even', 'tbl', List.of(2, 4, 6, 8), ':it').toWhere()) + } + + @Test + @DisplayName('toWhere generates for inArray | SQLite') + void toWhereInArraySQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', + "EXISTS (SELECT 1 FROM json_each(tbl.data, '\$.test') WHERE value IN (:city_0, :city_1))", + Field.inArray('test', 'tbl', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + } + + @Test + @DisplayName('toWhere generates for others w/o qualifier | PostgreSQL') + void toWhereOtherNoQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "data->>'some_field' = :value", + Field.equal('some_field', '', ':value').toWhere()) + } + + @Test + @DisplayName('toWhere generates for others w/o qualifier | SQLite') + void toWhereOtherNoQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "data->>'some_field' = :value", + Field.equal('some_field', '', ':value').toWhere()) + } + + @Test + @DisplayName('toWhere generates no-parameter w/ qualifier | PostgreSQL') + void toWhereNoParamWithQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "test.data->>'no_field' IS NOT NULL", + Field.exists('no_field').withQualifier('test').toWhere()) + } + + @Test + @DisplayName('toWhere generates no-parameter w/ qualifier | SQLite') + void toWhereNoParamWithQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "test.data->>'no_field' IS NOT NULL", + Field.exists('no_field').withQualifier('test').toWhere()) + } + + @Test + @DisplayName('toWhere generates parameter w/ qualifier | PostgreSQL') + void toWhereParamWithQualPostgres() { + ForceDialect.postgres() + assertEquals('Field WHERE clause not generated correctly', "(q.data->>'le_field')::numeric <= :it", + Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere()) + } + + @Test + @DisplayName('toWhere generates parameter w/ qualifier | SQLite') + void toWhereParamWithQualSQLite() { + ForceDialect.sqlite() + assertEquals('Field WHERE clause not generated correctly', "q.data->>'le_field' <= :it", + Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere()) + } + + // ~~~ STATIC CONSTRUCTOR TESTS ~~~ + + @Test + @DisplayName('equal constructs a field w/o parameter name') + void equalCtor() { + def field = Field.equal('Test', 14) + assertEquals('Field name not filled correctly', 'Test', field.name) + assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 14, field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('equal constructs a field w/ parameter name') + void equalParameterCtor() { + def field = Field.equal('Test', 14, ':w') + assertEquals('Field name not filled correctly', 'Test', field.name) + assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 14, field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':w', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('greater constructs a field w/o parameter name') + void greaterCtor() { + def field = Field.greater('Great', 'night') + assertEquals('Field name not filled correctly', 'Great', field.name) + assertEquals('Field comparison operation not filled correctly', Op.GREATER, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'night', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('greater constructs a field w/ parameter name') + void greaterParameterCtor() { + def field = Field.greater('Great', 'night', ':yeah') + assertEquals('Field name not filled correctly', 'Great', field.name) + assertEquals('Field comparison operation not filled correctly', Op.GREATER, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'night', field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':yeah', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('greaterOrEqual constructs a field w/o parameter name') + void greaterOrEqualCtor() { + def field = Field.greaterOrEqual('Nice', 88L) + assertEquals('Field name not filled correctly', 'Nice', field.name) + assertEquals('Field comparison operation not filled correctly', Op.GREATER_OR_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 88L, field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('greaterOrEqual constructs a field w/ parameter name') + void greaterOrEqualParameterCtor() { + def field = Field.greaterOrEqual('Nice', 88L, ':nice') + assertEquals('Field name not filled correctly', 'Nice', field.name) + assertEquals('Field comparison operation not filled correctly', Op.GREATER_OR_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 88L, field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':nice', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('less constructs a field w/o parameter name') + void lessCtor() { + def field = Field.less('Lesser', 'seven') + assertEquals('Field name not filled correctly', 'Lesser', field.name) + assertEquals('Field comparison operation not filled correctly', Op.LESS, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'seven', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('less constructs a field w/ parameter name') + void lessParameterCtor() { + def field = Field.less('Lesser', 'seven', ':max') + assertEquals('Field name not filled correctly', 'Lesser', field.name) + assertEquals('Field comparison operation not filled correctly', Op.LESS, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'seven', field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':max', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('lessOrEqual constructs a field w/o parameter name') + void lessOrEqualCtor() { + def field = Field.lessOrEqual('Nobody', 'KNOWS') + assertEquals('Field name not filled correctly', 'Nobody', field.name) + assertEquals('Field comparison operation not filled correctly', Op.LESS_OR_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'KNOWS', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('lessOrEqual constructs a field w/ parameter name') + void lessOrEqualParameterCtor() { + def field = Field.lessOrEqual('Nobody', 'KNOWS', ':nope') + assertEquals('Field name not filled correctly', 'Nobody', field.name) + assertEquals('Field comparison operation not filled correctly', Op.LESS_OR_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'KNOWS', field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':nope', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('notEqual constructs a field w/o parameter name') + void notEqualCtor() { + def field = Field.notEqual('Park', 'here') + assertEquals('Field name not filled correctly', 'Park', field.name) + assertEquals('Field comparison operation not filled correctly', Op.NOT_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'here', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('notEqual constructs a field w/ parameter name') + void notEqualParameterCtor() { + def field = Field.notEqual('Park', 'here', ':now') + assertEquals('Field name not filled correctly', 'Park', field.name) + assertEquals('Field comparison operation not filled correctly', Op.NOT_EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', 'here', field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':now', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('between constructs a field w/o parameter name') + void betweenCtor() { + def field = Field.between('Age', 18, 49) + assertEquals('Field name not filled correctly', 'Age', field.name) + assertEquals('Field comparison operation not filled correctly', Op.BETWEEN, field.comparison.op) + assertEquals('Field comparison min value not filled correctly', 18, field.comparison.value.first) + assertEquals('Field comparison max value not filled correctly', 49, field.comparison.value.second, ) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('between constructs a field w/ parameter name') + void betweenParameterCtor() { + def field = Field.between('Age', 18, 49, ':limit') + assertEquals('Field name not filled correctly', 'Age', field.name) + assertEquals('Field comparison operation not filled correctly', Op.BETWEEN, field.comparison.op) + assertEquals('Field comparison min value not filled correctly', 18, field.comparison.value.first) + assertEquals('Field comparison max value not filled correctly', 49, field.comparison.value.second) + assertEquals('Field parameter name not filled correctly', ':limit', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('any constructs a field w/o parameter name') + void anyCtor() { + def field = Field.any('Here', List.of(8, 16, 32)) + assertEquals('Field name not filled correctly', 'Here', field.name) + assertEquals('Field comparison operation not filled correctly', Op.IN, field.comparison.op) + assertEquals('Field comparison value not filled correctly', List.of(8, 16, 32), field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('any constructs a field w/ parameter name') + void anyParameterCtor() { + def field = Field.any('Here', List.of(8, 16, 32), ':list') + assertEquals('Field name not filled correctly', 'Here', field.name) + assertEquals('Field comparison operation not filled correctly', Op.IN, field.comparison.op) + assertEquals('Field comparison value not filled correctly', List.of(8, 16, 32), field.comparison.value) + assertEquals('Field parameter name not filled correctly', ':list', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('inArray constructs a field w/o parameter name') + void inArrayCtor() { + def field = Field.inArray('ArrayField', 'table', List.of('z')) + assertEquals('Field name not filled correctly', 'ArrayField', field.name) + assertEquals('Field comparison operation not filled correctly', Op.IN_ARRAY, field.comparison.op) + assertEquals('Field comparison table not filled correctly', 'table', field.comparison.value.first) + assertEquals('Field comparison values not filled correctly', List.of('z'), field.comparison.value.second) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('inArray constructs a field w/ parameter name') + void inArrayParameterCtor() { + def field = Field.inArray('ArrayField', 'table', List.of('z'), ':a') + assertEquals('Field name not filled correctly', 'ArrayField', field.name) + assertEquals('Field comparison operation not filled correctly', Op.IN_ARRAY, field.comparison.op) + assertEquals('Field comparison table not filled correctly', 'table', field.comparison.value.first) + assertEquals('Field comparison values not filled correctly', List.of('z'), field.comparison.value.second) + assertEquals('Field parameter name not filled correctly', ':a', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('exists constructs a field') + void existsCtor() { + def field = Field.exists 'Groovy' + assertEquals('Field name not filled correctly', 'Groovy', field.name) + assertEquals('Field comparison operation not filled correctly', Op.EXISTS, field.comparison.op) + assertEquals('Field comparison value not filled correctly', '', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('notExists constructs a field') + void notExistsCtor() { + def field = Field.notExists 'Groovy' + assertEquals('Field name not filled correctly', 'Groovy', field.name) + assertEquals('Field comparison operation not filled correctly', Op.NOT_EXISTS, field.comparison.op) + assertEquals('Field comparison value not filled correctly', '', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + + @Test + @DisplayName('named constructs a field') + void namedCtor() { + def field = Field.named('Tacos') + assertEquals('Field name not filled correctly', 'Tacos', field.name) + assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) + assertEquals('Field comparison value not filled correctly', '', field.comparison.value) + assertNull('The parameter name should have been null', field.parameterName) + assertNull('The qualifier should have been null', field.qualifier) + } + +// TODO: fix java.base open issue +// @Test +// @DisplayName('static constructors fail for invalid parameter name') +// void staticCtorsFailOnParamName() { +// assertThrows(DocumentException) { Field.equal('a', 'b', "that ain't it, Jack...") } +// } + + @Test + @DisplayName('nameToPath creates a simple PostgreSQL SQL name') + void nameToPathPostgresSimpleSQL() { + assertEquals('Path not constructed correctly', "data->>'Simple'", + Field.nameToPath('Simple', Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('nameToPath creates a simple SQLite SQL name') + void nameToPathSQLiteSimpleSQL() { + assertEquals('Path not constructed correctly', "data->>'Simple'", + Field.nameToPath('Simple', Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('nameToPath creates a nested PostgreSQL SQL name') + void nameToPathPostgresNestedSQL() { + assertEquals('Path not constructed correctly', "data#>>'{A,Long,Path,to,the,Property}'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.POSTGRESQL, FieldFormat.SQL)) + } + + @Test + @DisplayName('nameToPath creates a nested SQLite SQL name') + void nameToPathSQLiteNestedSQL() { + assertEquals('Path not constructed correctly', "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.SQLITE, FieldFormat.SQL)) + } + + @Test + @DisplayName('nameToPath creates a simple PostgreSQL JSON name') + void nameToPathPostgresSimpleJSON() { + assertEquals('Path not constructed correctly', "data->'Simple'", + Field.nameToPath('Simple', Dialect.POSTGRESQL, FieldFormat.JSON)) + } + + @Test + @DisplayName('nameToPath creates a simple SQLite JSON name') + void nameToPathSQLiteSimpleJSON() { + assertEquals('Path not constructed correctly', "data->'Simple'", + Field.nameToPath('Simple', Dialect.SQLITE, FieldFormat.JSON)) + } + + @Test + @DisplayName('nameToPath creates a nested PostgreSQL JSON name') + void nameToPathPostgresNestedJSON() { + assertEquals('Path not constructed correctly', "data#>'{A,Long,Path,to,the,Property}'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.POSTGRESQL, FieldFormat.JSON)) + } + + @Test + @DisplayName('nameToPath creates a nested SQLite JSON name') + void nameToPathSQLiteNestedJSON() { + assertEquals('Path not constructed correctly', "data->'A'->'Long'->'Path'->'to'->'the'->'Property'", + Field.nameToPath('A.Long.Path.to.the.Property', Dialect.SQLITE, FieldFormat.JSON)) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy new file mode 100644 index 0000000..47118f0 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy @@ -0,0 +1,80 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Op + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Op` enum + */ +@DisplayName('JVM | Groovy | Op') +class OpTest { + + @Test + @DisplayName('EQUAL uses proper SQL') + void equalSQL() { + assertEquals('The SQL for equal is incorrect', '=', Op.EQUAL.sql) + } + + @Test + @DisplayName('GREATER uses proper SQL') + void greaterSQL() { + assertEquals('The SQL for greater is incorrect', '>', Op.GREATER.sql) + } + + @Test + @DisplayName('GREATER_OR_EQUAL uses proper SQL') + void greaterOrEqualSQL() { + assertEquals('The SQL for greater-or-equal is incorrect', '>=', Op.GREATER_OR_EQUAL.sql) + } + + @Test + @DisplayName('LESS uses proper SQL') + void lessSQL() { + assertEquals('The SQL for less is incorrect', '<', Op.LESS.sql) + } + + @Test + @DisplayName('LESS_OR_EQUAL uses proper SQL') + void lessOrEqualSQL() { + assertEquals('The SQL for less-or-equal is incorrect', '<=', Op.LESS_OR_EQUAL.sql) + } + + @Test + @DisplayName('NOT_EQUAL uses proper SQL') + void notEqualSQL() { + assertEquals('The SQL for not-equal is incorrect', '<>', Op.NOT_EQUAL.sql) + } + + @Test + @DisplayName('BETWEEN uses proper SQL') + void betweenSQL() { + assertEquals('The SQL for between is incorrect', 'BETWEEN', Op.BETWEEN.sql) + } + + @Test + @DisplayName('IN uses proper SQL') + void inSQL() { + assertEquals('The SQL for in is incorrect', 'IN', Op.IN.sql) + } + + @Test + @DisplayName('IN_ARRAY uses proper SQL') + void inArraySQL() { + assertEquals('The SQL for in-array is incorrect', '??|', Op.IN_ARRAY.sql) + } + + @Test + @DisplayName('EXISTS uses proper SQL') + void existsSQL() { + assertEquals('The SQL for exists is incorrect', 'IS NOT NULL', Op.EXISTS.sql) + } + + @Test + @DisplayName('NOT_EXISTS uses proper SQL') + void notExistsSQL() { + assertEquals('The SQL for not-exists is incorrect', 'IS NULL', Op.NOT_EXISTS.sql) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy new file mode 100644 index 0000000..8beb54b --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy @@ -0,0 +1,30 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.ParameterName +import static groovy.test.GroovyAssert.assertEquals +/** + * Unit tests for the `ParameterName` class + */ +@DisplayName('JVM | Groovy | ParameterName') +class ParameterNameTest { + + @Test + @DisplayName('derive works when given existing names') + void withExisting() { + def names = new ParameterName() + assertEquals('Name should have been :taco', ':taco', names.derive(':taco')) + assertEquals('Counter should not have advanced for named field', ':field0', names.derive(null)) + } + + @Test + @DisplayName('derive works when given all anonymous fields') + void allAnonymous() { + def names = new ParameterName() + assertEquals('Anonymous field name should have been returned', ':field0', names.derive(null)) + assertEquals('Counter should have advanced from previous call', ':field1', names.derive(null)) + assertEquals('Counter should have advanced from previous call', ':field2', names.derive(null)) + assertEquals('Counter should have advanced from previous call', ':field3', names.derive(null)) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy new file mode 100644 index 0000000..1cd06ab --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy @@ -0,0 +1,41 @@ +package solutions.bitbadger.documents.groovy + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Parameter` class + */ +@DisplayName('JVM | Groovy | Parameter') +class ParameterTest { + + @Test + @DisplayName("Construction with colon-prefixed name") + void ctorWithColon() { + def p = new Parameter(":test", ParameterType.STRING, "ABC") + assertEquals("Parameter name was incorrect", ":test", p.name) + assertEquals("Parameter type was incorrect", ParameterType.STRING, p.type) + assertEquals("Parameter value was incorrect", "ABC", p.value) + } + + @Test + @DisplayName("Construction with at-sign-prefixed name") + void ctorWithAtSign() { + def p = new Parameter("@yo", ParameterType.NUMBER, null) + assertEquals("Parameter name was incorrect", "@yo", p.name) + assertEquals("Parameter type was incorrect", ParameterType.NUMBER, p.type) + assertNull("Parameter value was incorrect", p.value) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName("Construction fails with incorrect prefix") +// void ctorFailsForPrefix() { +// assertThrows(DocumentException) { new Parameter("it", ParameterType.JSON, "") } +// } +} diff --git a/src/jvm/src/test/kotlin/FieldTest.kt b/src/jvm/src/test/kotlin/FieldTest.kt index 4544d20..d7a616a 100644 --- a/src/jvm/src/test/kotlin/FieldTest.kt +++ b/src/jvm/src/test/kotlin/FieldTest.kt @@ -452,7 +452,7 @@ class FieldTest { assertEquals(Op.BETWEEN, field.comparison.op, "Field comparison operation not filled correctly") assertEquals(18, field.comparison.value.first, "Field comparison min value not filled correctly") assertEquals(49, field.comparison.value.second, "Field comparison max value not filled correctly") - assertEquals(":limit", field.parameterName, "The parameter name should have been null") + assertEquals(":limit", field.parameterName, "Field parameter name not filled correctly") assertNull(field.qualifier, "The qualifier should have been null") } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala index ecfa9a2..63ca4d9 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala @@ -1,85 +1,86 @@ package solutions.bitbadger.documents.scala import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import solutions.bitbadger.documents.scala.support.{ByteIdClass, IntIdClass, LongIdClass, ShortIdClass, StringIdClass} import solutions.bitbadger.documents.{AutoId, DocumentException} -class AutoIdSpec extends AnyFunSpec { +class AutoIdSpec extends AnyFunSpec with Matchers { describe("generateUUID") { it("generates a UUID string") { - assert(32 == AutoId.generateUUID().length) + AutoId.generateUUID().length shouldEqual 32 } } describe("generateRandomString") { it("generates a random hex character string of an even length") { - assert(8 == AutoId.generateRandomString(8).length) + AutoId.generateRandomString(8).length shouldEqual 8 } it("generates a random hex character string of an odd length") { - assert(11 == AutoId.generateRandomString(11).length) + AutoId.generateRandomString(11).length shouldEqual 11 } it("generates different random hex character strings") { val result1 = AutoId.generateRandomString(16) val result2 = AutoId.generateRandomString(16) - assert(result1 != result2) + result1 should not be theSameInstanceAs (result2) } } describe("needsAutoId") { it("fails for null document") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.DISABLED, null, "id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.DISABLED, null, "id") } it("fails for missing ID property") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } it("returns false if disabled") { - assert(!AutoId.needsAutoId(AutoId.DISABLED, "", "")) + AutoId.needsAutoId(AutoId.DISABLED, "", "") shouldBe false } it("returns true for Number strategy and byte ID of 0") { - assert(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id")) + AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id") shouldBe true } it("returns false for Number strategy and byte ID of non-0") { - assert(!AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id")) + AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id") shouldBe false } it("returns true for Number strategy and short ID of 0") { - assert(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id")) + AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id") shouldBe true } it("returns false for Number strategy and short ID of non-0") { - assert(!AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id")) + AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id") shouldBe false } it("returns true for Number strategy and int ID of 0") { - assert(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id")) + AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id") shouldBe true } it("returns false for Number strategy and int ID of non-0") { - assert(!AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id")) + AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id") shouldBe false } it("returns true for Number strategy and long ID of 0") { - assert(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id")) + AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id") shouldBe true } it("returns false for Number strategy and long ID of non-0") { - assert(!AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id")) + AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id") shouldBe false } it("fails for Number strategy and non-number ID") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } it("returns true for UUID strategy and blank ID") { - assert(AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id")) + AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id") shouldBe true } it("returns false for UUID strategy and non-blank ID") { - assert(!AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id")) + AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id") shouldBe false } it("fails for UUID strategy and non-string ID") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } it("returns true for Random String strategy and blank ID") { - assert(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id")) + AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id") shouldBe true } it("returns false for Random String strategy and non-blank ID") { - assert(!AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id")) + AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id") shouldBe false } it("fails for Random String strategy and non-string ID") { - assertThrows[DocumentException] { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } + an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala new file mode 100644 index 0000000..b2b64cf --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.{BeforeAndAfterEach, Suite} +import solutions.bitbadger.documents.support.ForceDialect + +trait ClearConfiguration extends BeforeAndAfterEach { this: Suite => + + override def afterEach(): Unit = + try super.afterEach () + finally ForceDialect.none() +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala index ae08e39..c42e21f 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala @@ -1,34 +1,35 @@ package solutions.bitbadger.documents.scala import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import solutions.bitbadger.documents.{AutoId, Configuration, Dialect, DocumentException} -class ConfigurationSpec extends AnyFunSpec { +class ConfigurationSpec extends AnyFunSpec with Matchers { describe("idField") { it("defaults to `id`") { - assert("id" == Configuration.idField) + Configuration.idField shouldEqual "id" } } describe("autoIdStrategy") { it("defaults to `DISABLED`") { - assert(AutoId.DISABLED == Configuration.autoIdStrategy) + Configuration.autoIdStrategy shouldEqual AutoId.DISABLED } } describe("idStringLength") { it("defaults to 16") { - assert(16 == Configuration.idStringLength) + Configuration.idStringLength shouldEqual 16 } } describe("dialect") { it("is derived from connection string") { try { - assertThrows[DocumentException] { Configuration.dialect() } + an [DocumentException] should be thrownBy Configuration.dialect() Configuration.setConnectionString("jdbc:postgresql:db") - assert(Dialect.POSTGRESQL == Configuration.dialect()) + Configuration.dialect() shouldEqual Dialect.POSTGRESQL } finally { Configuration.setConnectionString(null) } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala index 1e56812..7321fe0 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala @@ -1,16 +1,17 @@ package solutions.bitbadger.documents.scala import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import solutions.bitbadger.documents.{Dialect, DocumentException} -class DialectSpec extends AnyFunSpec { +class DialectSpec extends AnyFunSpec with Matchers { describe("deriveFromConnectionString") { it("derives PostgreSQL correctly") { - assert(Dialect.POSTGRESQL == Dialect.deriveFromConnectionString("jdbc:postgresql:db")) + Dialect.deriveFromConnectionString("jdbc:postgresql:db") shouldEqual Dialect.POSTGRESQL } it("derives SQLite correctly") { - assert(Dialect.SQLITE == Dialect.deriveFromConnectionString("jdbc:sqlite:memory")) + Dialect.deriveFromConnectionString("jdbc:sqlite:memory") shouldEqual Dialect.SQLITE } it("fails when the connection string is unknown") { try { @@ -18,8 +19,8 @@ class DialectSpec extends AnyFunSpec { fail("Dialect derivation should have failed") } catch { case ex: DocumentException => - assert(ex.getMessage != null) - assert(ex.getMessage.contains("[SQL Server]")) + ex.getMessage should not be null + ex.getMessage should include ("[SQL Server]") } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala new file mode 100644 index 0000000..3e21f8e --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala @@ -0,0 +1,17 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.DocumentIndex + +class DocumentIndexSpec extends AnyFunSpec with Matchers { + + describe("sql") { + it("returns blank for FULL") { + DocumentIndex.FULL.getSql shouldEqual "" + } + it("returns jsonb_path_ops for OPTIMIZED") { + DocumentIndex.OPTIMIZED.getSql shouldEqual " jsonb_path_ops" + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala index 25d5625..07d0843 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala @@ -1,23 +1,20 @@ package solutions.bitbadger.documents.scala import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers import solutions.bitbadger.documents.FieldMatch /** * Unit tests for the `FieldMatch` enum */ -class FieldMatchSpec extends AnyFunSpec { +class FieldMatchSpec extends AnyFunSpec with Matchers { describe("sql") { - describe("ANY") { - it("should use OR") { - assert("OR" == FieldMatch.ANY.getSql) - } + it("returns OR for ANY") { + FieldMatch.ANY.getSql shouldEqual "OR" } - describe("ALL") { - it("should use AND") { - assert("AND" == FieldMatch.ALL.getSql) - } + it("returns AND for ALL") { + FieldMatch.ALL.getSql shouldEqual "AND" } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala new file mode 100644 index 0000000..a1123d1 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala @@ -0,0 +1,428 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.{Dialect, DocumentException, Field, FieldFormat, Op} + +import scala.jdk.CollectionConverters.* + +class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { + + // ~~~ INSTANCE METHODS ~~~ + + describe("withParameterName") { + it("fails for invalid name") { + an [DocumentException] should be thrownBy Field.equal("it", "").withParameterName("2424") + } + it("works with colon prefix") { + val field = Field.equal("abc", "22").withQualifier("me") + val withParam = field.withParameterName(":test") + withParam should not be theSameInstanceAs (field) + withParam.getName shouldEqual field.getName + withParam.getComparison shouldEqual field.getComparison + withParam.getParameterName shouldEqual ":test" + withParam.getQualifier shouldEqual field.getQualifier + } + it("works with at-sign prefix") { + val field = Field.equal("def", "44") + val withParam = field.withParameterName("@unit") + withParam should not be theSameInstanceAs (field) + withParam.getName shouldEqual field.getName + withParam.getComparison shouldEqual field.getComparison + withParam.getParameterName shouldEqual "@unit" + withParam.getQualifier shouldEqual field.getQualifier + } + } + + describe("withQualifier") { + it("sets qualifier correctly") { + val field = Field.equal("j", "k") + val withQual = field.withQualifier("test") + withQual should not be theSameInstanceAs (field) + withQual.getName shouldEqual field.getName + withQual.getComparison shouldEqual field.getComparison + withQual.getParameterName shouldEqual field.getParameterName + withQual.getQualifier shouldEqual "test" + } + } + + describe("path") { + it("generates simple unqualified PostgreSQL field") { + Field.greaterOrEqual("SomethingCool", 18) + .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data->>'SomethingCool'" + } + it("generates simple qualified PostgreSQL field") { + Field.less("SomethingElse", 9).withQualifier("this") + .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "this.data->>'SomethingElse'" + } + it("generates nested unqualified PostgreSQL field") { + Field.equal("My.Nested.Field", "howdy") + .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data#>>'{My,Nested,Field}'" + } + it("generates nested qualified PostgreSQL field") { + Field.equal("Nest.Away", "doc").withQualifier("bird") + .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "bird.data#>>'{Nest,Away}'" + } + it("generates simple unqualified SQLite field") { + Field.greaterOrEqual("SomethingCool", 18) + .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->>'SomethingCool'" + } + it("generates simple qualified SQLite field") { + Field.less("SomethingElse", 9).withQualifier("this") + .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "this.data->>'SomethingElse'" + } + it("generates nested unqualified SQLite field") { + Field.equal("My.Nested.Field", "howdy") + .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->'My'->'Nested'->>'Field'" + } + it("generates nested qualified SQLite field") { + Field.equal("Nest.Away", "doc").withQualifier("bird") + .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "bird.data->'Nest'->>'Away'" + } + } + + describe("toWhere") { + it("generates exists w/o qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.exists("that_field").toWhere shouldEqual "data->>'that_field' IS NOT NULL" + } + it("generates exists w/o qualifier | SQLite") { + ForceDialect.sqlite() + Field.exists("that_field").toWhere shouldEqual "data->>'that_field' IS NOT NULL" + } + it("generates not-exists w/o qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.notExists("a_field").toWhere shouldEqual "data->>'a_field' IS NULL" + } + it("generates not-exists w/o qualifier | SQLite") { + ForceDialect.sqlite() + Field.notExists("a_field").toWhere shouldEqual "data->>'a_field' IS NULL" + } + it("generates BETWEEN w/o qualifier, numeric range | PostgreSQL") { + ForceDialect.postgres() + Field.between("age", 13, 17, "@age").toWhere shouldEqual "(data->>'age')::numeric BETWEEN @agemin AND @agemax" + } + it("generates BETWEEN w/o qualifier, alphanumeric range | PostgreSQL") { + ForceDialect.postgres() + Field.between("city", "Atlanta", "Chicago", ":city") + .toWhere shouldEqual "data->>'city' BETWEEN :citymin AND :citymax" + } + it("generates BETWEEN w/o qualifier | SQLite") { + ForceDialect.sqlite() + Field.between("age", 13, 17, "@age").toWhere shouldEqual "data->>'age' BETWEEN @agemin AND @agemax" + } + it("generates BETWEEN w/ qualifier, numeric range | PostgreSQL") { + ForceDialect.postgres() + Field.between("age", 13, 17, "@age").withQualifier("test") + .toWhere shouldEqual "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax" + } + it("generates BETWEEN w/ qualifier, alphanumeric range | PostgreSQL") { + ForceDialect.postgres() + Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit") + .toWhere shouldEqual "unit.data->>'city' BETWEEN :citymin AND :citymax" + } + it("generates BETWEEN w/ qualifier | SQLite") { + ForceDialect.sqlite() + Field.between("age", 13, 17, "@age").withQualifier("my") + .toWhere shouldEqual "my.data->>'age' BETWEEN @agemin AND @agemax" + } + it("generates IN/any, numeric values | PostgreSQL") { + ForceDialect.postgres() + Field.any("even", List(2, 4, 6).asJava, ":nbr") + .toWhere shouldEqual "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)" + } + it("generates IN/any, alphanumeric values | PostgreSQL") { + ForceDialect.postgres() + Field.any("test", List("Atlanta", "Chicago").asJava, ":city") + .toWhere shouldEqual "data->>'test' IN (:city_0, :city_1)" + } + it("generates IN/any | SQLite") { + ForceDialect.sqlite() + Field.any("test", List("Atlanta", "Chicago").asJava, ":city") + .toWhere shouldEqual "data->>'test' IN (:city_0, :city_1)" + } + it("generates inArray | PostgreSQL") { + ForceDialect.postgres() + Field.inArray("even", "tbl", List(2, 4, 6, 8).asJava, ":it") + .toWhere shouldEqual "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]" + } + it("generates inArray | SQLite") { + ForceDialect.sqlite() + Field.inArray("test", "tbl", List("Atlanta", "Chicago").asJava, ":city") + .toWhere shouldEqual "EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))" + } + it("generates others w/o qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.equal("some_field", "", ":value").toWhere shouldEqual "data->>'some_field' = :value" + } + it("generates others w/o qualifier | SQLite") { + ForceDialect.sqlite() + Field.equal("some_field", "", ":value").toWhere shouldEqual "data->>'some_field' = :value" + } + it("generates no-parameter w/ qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.exists("no_field").withQualifier("test").toWhere shouldEqual "test.data->>'no_field' IS NOT NULL" + } + it("generates no-parameter w/ qualifier | SQLite") { + ForceDialect.sqlite() + Field.exists("no_field").withQualifier("test").toWhere shouldEqual "test.data->>'no_field' IS NOT NULL" + } + it("generates parameter w/ qualifier | PostgreSQL") { + ForceDialect.postgres() + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q") + .toWhere shouldEqual "(q.data->>'le_field')::numeric <= :it" + } + it("generates parameter w/ qualifier | SQLite") { + ForceDialect.sqlite() + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere shouldEqual "q.data->>'le_field' <= :it" + } + } + + // ~~~ STATIC CONSTRUCTOR TESTS ~~~ + + describe("equal") { + it("constructs a field w/o parameter name") { + val field = Field.equal("Test", 14) + field.getName shouldEqual "Test" + field.getComparison.getOp shouldEqual Op.EQUAL + field.getComparison.getValue shouldEqual 14 + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.equal("Test", 14, ":w") + field.getName shouldEqual "Test" + field.getComparison.getOp shouldEqual Op.EQUAL + field.getComparison.getValue shouldEqual 14 + field.getParameterName shouldEqual ":w" + field.getQualifier should be (null) + } + } + + describe("greater") { + it("constructs a field w/o parameter name") { + val field = Field.greater("Great", "night") + field.getName shouldEqual "Great" + field.getComparison.getOp shouldEqual Op.GREATER + field.getComparison.getValue shouldEqual "night" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.greater("Great", "night", ":yeah") + field.getName shouldEqual "Great" + field.getComparison.getOp shouldEqual Op.GREATER + field.getComparison.getValue shouldEqual "night" + field.getParameterName shouldEqual ":yeah" + field.getQualifier should be (null) + } + } + + describe("greaterOrEqual") { + it("constructs a field w/o parameter name") { + val field = Field.greaterOrEqual("Nice", 88L) + field.getName shouldEqual "Nice" + field.getComparison.getOp shouldEqual Op.GREATER_OR_EQUAL + field.getComparison.getValue shouldEqual 88L + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.greaterOrEqual("Nice", 88L, ":nice") + field.getName shouldEqual "Nice" + field.getComparison.getOp shouldEqual Op.GREATER_OR_EQUAL + field.getComparison.getValue shouldEqual 88L + field.getParameterName shouldEqual ":nice" + field.getQualifier should be (null) + } + } + + describe("less") { + it("constructs a field w/o parameter name") { + val field = Field.less("Lesser", "seven") + field.getName shouldEqual "Lesser" + field.getComparison.getOp shouldEqual Op.LESS + field.getComparison.getValue shouldEqual "seven" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.less("Lesser", "seven", ":max") + field.getName shouldEqual "Lesser" + field.getComparison.getOp shouldEqual Op.LESS + field.getComparison.getValue shouldEqual "seven" + field.getParameterName shouldEqual ":max" + field.getQualifier should be (null) + } + } + + describe("lessOrEqual") { + it("constructs a field w/o parameter name") { + val field = Field.lessOrEqual("Nobody", "KNOWS") + field.getName shouldEqual "Nobody" + field.getComparison.getOp shouldEqual Op.LESS_OR_EQUAL + field.getComparison.getValue shouldEqual "KNOWS" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.lessOrEqual("Nobody", "KNOWS", ":nope") + field.getName shouldEqual "Nobody" + field.getComparison.getOp shouldEqual Op.LESS_OR_EQUAL + field.getComparison.getValue shouldEqual "KNOWS" + field.getParameterName shouldEqual ":nope" + field.getQualifier should be (null) + } + } + + describe("notEqual") { + it("constructs a field w/o parameter name") { + val field = Field.notEqual("Park", "here") + field.getName shouldEqual "Park" + field.getComparison.getOp shouldEqual Op.NOT_EQUAL + field.getComparison.getValue shouldEqual "here" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.notEqual("Park", "here", ":now") + field.getName shouldEqual "Park" + field.getComparison.getOp shouldEqual Op.NOT_EQUAL + field.getComparison.getValue shouldEqual "here" + field.getParameterName shouldEqual ":now" + field.getQualifier should be (null) + } + } + + describe("between") { + it("constructs a field w/o parameter name") { + val field = Field.between("Age", 18, 49) + field.getName shouldEqual "Age" + field.getComparison.getOp shouldEqual Op.BETWEEN + field.getComparison.getValue.getFirst shouldEqual 18 + field.getComparison.getValue.getSecond shouldEqual 49 + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.between("Age", 18, 49, ":limit") + field.getName shouldEqual "Age" + field.getComparison.getOp shouldEqual Op.BETWEEN + field.getComparison.getValue.getFirst shouldEqual 18 + field.getComparison.getValue.getSecond shouldEqual 49 + field.getParameterName shouldEqual ":limit" + field.getQualifier should be (null) + } + } + + describe("any") { + it("constructs a field w/o parameter name") { + val field = Field.any("Here", List(8, 16, 32).asJava) + field.getName shouldEqual "Here" + field.getComparison.getOp shouldEqual Op.IN + field.getComparison.getValue should be (List(8, 16, 32).asJava) + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.any("Here", List(8, 16, 32).asJava, ":list") + field.getName shouldEqual "Here" + field.getComparison.getOp shouldEqual Op.IN + field.getComparison.getValue should be (List(8, 16, 32).asJava) + field.getParameterName shouldEqual ":list" + field.getQualifier should be (null) + } + } + + describe("inArray") { + it("constructs a field w/o parameter name") { + val field = Field.inArray("ArrayField", "table", List("z").asJava) + field.getName shouldEqual "ArrayField" + field.getComparison.getOp shouldEqual Op.IN_ARRAY + field.getComparison.getValue.getFirst shouldEqual "table" + field.getComparison.getValue.getSecond should be (List("z").asJava) + field.getParameterName should be (null) + field.getQualifier should be (null) + } + it("constructs a field w/ parameter name") { + val field = Field.inArray("ArrayField", "table", List("z").asJava, ":a") + field.getName shouldEqual "ArrayField" + field.getComparison.getOp shouldEqual Op.IN_ARRAY + field.getComparison.getValue.getFirst shouldEqual "table" + field.getComparison.getValue.getSecond should be (List("z").asJava) + field.getParameterName shouldEqual ":a" + field.getQualifier should be (null) + } + } + + describe("exists") { + it("constructs a field") { + val field = Field.exists("Groovy") + field.getName shouldEqual "Groovy" + field.getComparison.getOp shouldEqual Op.EXISTS + field.getComparison.getValue shouldEqual "" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + } + + describe("notExists") { + it("constructs a field") { + val field = Field.notExists("Groovy") + field.getName shouldEqual "Groovy" + field.getComparison.getOp shouldEqual Op.NOT_EXISTS + field.getComparison.getValue shouldEqual "" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + } + + describe("named") { + it("named constructs a field") { + val field = Field.named("Tacos") + field.getName shouldEqual "Tacos" + field.getComparison.getOp shouldEqual Op.EQUAL + field.getComparison.getValue shouldEqual "" + field.getParameterName should be (null) + field.getQualifier should be (null) + } + } + + describe("static constructors") { + it("fail for invalid parameter name") { + an [DocumentException] should be thrownBy Field.equal("a", "b", "that ain't it, Jack...") + } + } + + describe("nameToPath") { + it("creates a simple PostgreSQL SQL name") { + Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data->>'Simple'" + } + it("creates a simple SQLite SQL name") { + Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->>'Simple'" + } + it("creates a nested PostgreSQL SQL name") { + (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL) + shouldEqual "data#>>'{A,Long,Path,to,the,Property}'") + } + it("creates a nested SQLite SQL name") { + (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL) + shouldEqual "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'") + } + it("creates a simple PostgreSQL JSON name") { + Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON) shouldEqual "data->'Simple'" + } + it("creates a simple SQLite JSON name") { + Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON) shouldEqual "data->'Simple'" + } + it("creates a nested PostgreSQL JSON name") { + (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON) + shouldEqual "data#>'{A,Long,Path,to,the,Property}'") + } + it("creates a nested SQLite JSON name") { + (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON) + shouldEqual "data->'A'->'Long'->'Path'->'to'->'the'->'Property'") + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala new file mode 100644 index 0000000..aea074f --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala @@ -0,0 +1,44 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.Op + +class OpSpec extends AnyFunSpec with Matchers { + + describe("sql") { + it("returns = for EQUAL") { + Op.EQUAL.getSql shouldEqual "=" + } + it("returns > for GREATER") { + Op.GREATER.getSql shouldEqual ">" + } + it("returns >= for GREATER_OR_EQUAL") { + Op.GREATER_OR_EQUAL.getSql shouldEqual ">=" + } + it("returns < for LESS") { + Op.LESS.getSql shouldEqual "<" + } + it("returns <= for LESS_OR_EQUAL") { + Op.LESS_OR_EQUAL.getSql shouldEqual "<=" + } + it("returns <> for NOT_EQUAL") { + Op.NOT_EQUAL.getSql shouldEqual "<>" + } + it("returns BETWEEN for BETWEEN") { + Op.BETWEEN.getSql shouldEqual "BETWEEN" + } + it("returns IN for IN") { + Op.IN.getSql shouldEqual "IN" + } + it("returns ??| for IN_ARRAY") { + Op.IN_ARRAY.getSql shouldEqual "??|" + } + it("returns IS NOT NULL for EXISTS") { + Op.EXISTS.getSql shouldEqual "IS NOT NULL" + } + it("returns IS NULL for NOT_EXISTS") { + Op.NOT_EXISTS.getSql shouldEqual "IS NULL" + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala new file mode 100644 index 0000000..35188e8 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala @@ -0,0 +1,23 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.ParameterName + +class ParameterNameSpec extends AnyFunSpec with Matchers { + + describe("derive") { + it("works when given existing names") { + val names = new ParameterName() + names.derive(":taco") shouldEqual ":taco" + names.derive(null) shouldEqual ":field0" + } + it("works when given all anonymous fields") { + val names = new ParameterName() + names.derive(null) shouldEqual ":field0" + names.derive(null) shouldEqual ":field1" + names.derive(null) shouldEqual ":field2" + names.derive(null) shouldEqual ":field3" + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala new file mode 100644 index 0000000..dbda937 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala @@ -0,0 +1,26 @@ +package solutions.bitbadger.documents.scala + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Parameter, ParameterType} + +class ParameterSpec extends AnyFunSpec with Matchers { + + describe("constructor") { + it("succeeds with colon-prefixed name") { + val p = new Parameter(":test", ParameterType.STRING, "ABC") + p.getName shouldEqual ":test" + p.getType shouldEqual ParameterType.STRING + p.getValue shouldEqual "ABC" + } + it("succeeds with at-sign-prefixed name") { + val p = Parameter("@yo", ParameterType.NUMBER, null) + p.getName shouldEqual "@yo" + p.getType shouldEqual ParameterType.NUMBER + p.getValue should be (null) + } + it("fails with incorrect prefix") { + an [DocumentException] should be thrownBy Parameter("it", ParameterType.JSON, "") + } + } +} -- 2.47.2 From 781df81ce2d17d6f4018f17dbabf7ed5bd1c7a29 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 17 Mar 2025 22:47:15 -0400 Subject: [PATCH 54/88] WIP on Scala/Groovy query tests --- .../documents/groovy/AutoIdTest.groovy | 2 +- .../groovy/query/CountQueryTest.groovy | 86 ++++++++++ .../groovy/query/DefinitionQueryTest.groovy | 128 ++++++++++++++ .../groovy/query/DeleteQueryTest.groovy | 94 +++++++++++ .../groovy/query/DocumentQueryTest.groovy | 135 +++++++++++++++ .../groovy/query/FindQueryTest.groovy | 101 +++++++++++ .../groovy/query/PatchQueryTest.groovy | 97 +++++++++++ .../groovy/query/QueryUtilsTest.groovy | 159 ++++++++++++++++++ .../documents/scala/AutoIdSpec.scala | 10 +- .../documents/scala/ConfigurationSpec.scala | 2 +- .../bitbadger/documents/scala/FieldSpec.scala | 92 +++++----- .../documents/scala/ParameterSpec.scala | 2 +- .../scala/query/CountQuerySpec.scala | 56 ++++++ .../scala/query/DefinitionQuerySpec.scala | 81 +++++++++ .../scala/query/DeleteQuerySpec.scala | 61 +++++++ .../scala/query/DocumentQuerySpec.scala | 85 ++++++++++ .../scala/query/ExistsQuerySpec.scala | 64 +++++++ .../documents/scala/query/FindQuerySpec.scala | 67 ++++++++ .../scala/query/PatchQuerySpec.scala | 63 +++++++ .../scala/query/QueryUtilsSpec.scala | 103 ++++++++++++ 20 files changed, 1434 insertions(+), 54 deletions(-) create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy index 52c79e2..6d7e0d9 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.groovy import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import solutions.bitbadger.documents.AutoId -import solutions.bitbadger.documents.DocumentException +//import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.groovy.support.* import static groovy.test.GroovyAssert.* diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy new file mode 100644 index 0000000..54d3d96 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy @@ -0,0 +1,86 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Count` object + */ +@DisplayName('JVM | Groovy | Query | CountQuery') +class CountQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('all generates correctly') + void all() { + assertEquals('Count query not constructed correctly', "SELECT COUNT(*) AS it FROM $TEST_TABLE".toString(), + CountQuery.all(TEST_TABLE)) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Count query not constructed correctly', + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0".toString(), + CountQuery.byFields(TEST_TABLE, List.of(Field.equal('test', '', ':field0')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Count query not constructed correctly', + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0".toString(), + CountQuery.byFields(TEST_TABLE, List.of(Field.equal('test', '', ':field0')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Count query not constructed correctly', + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria".toString(), + CountQuery.byContains(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byContains fails | SQLite') +// void byContainsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { CountQuery.byContains(TEST_TABLE) } +// } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals('Count query not constructed correctly', + "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + CountQuery.byJsonPath(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { CountQuery.byJsonPath(TEST_TABLE) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy new file mode 100644 index 0000000..bba2783 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy @@ -0,0 +1,128 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Dialect +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentIndex +import solutions.bitbadger.documents.query.DefinitionQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Definition` object + */ +@DisplayName('JVM | Groovy | Query | DefinitionQuery') +class DefinitionQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('ensureTableFor generates correctly') + void ensureTableFor() { + assertEquals('CREATE TABLE statement not constructed correctly', + 'CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)', + DefinitionQuery.ensureTableFor('my.table', 'JSONB')) + } + + @Test + @DisplayName('ensureTable generates correctly | PostgreSQL') + void ensureTablePostgres() { + ForceDialect.postgres() + assertEquals("CREATE TABLE IF NOT EXISTS $TEST_TABLE (data JSONB NOT NULL)".toString(), + DefinitionQuery.ensureTable(TEST_TABLE)) + } + + @Test + @DisplayName('ensureTable generates correctly | SQLite') + void ensureTableSQLite() { + ForceDialect.sqlite() + assertEquals("CREATE TABLE IF NOT EXISTS $TEST_TABLE (data TEXT NOT NULL)".toString(), + DefinitionQuery.ensureTable(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('ensureTable fails when no dialect is set') +// void ensureTableFailsUnknown() { +// assertThrows(DocumentException) { DefinitionQuery.ensureTable(TEST_TABLE) } +// } + + @Test + @DisplayName('ensureKey generates correctly with schema') + void ensureKeyWithSchema() { + assertEquals('CREATE INDEX for key statement with schema not constructed correctly', + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", + DefinitionQuery.ensureKey('test.table', Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('ensureKey generates correctly without schema') + void ensureKeyWithoutSchema() { + assertEquals('CREATE INDEX for key statement without schema not constructed correctly', + "CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))".toString(), + DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE)) + } + + @Test + @DisplayName('ensureIndexOn generates multiple fields and directions') + void ensureIndexOnMultipleFields() { + assertEquals('CREATE INDEX for multiple field statement not constructed correctly', + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" + .toString(), + DefinitionQuery.ensureIndexOn('test.table', 'gibberish', List.of('taco', 'guac DESC', 'salsa ASC'), + Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('ensureIndexOn generates nested field | PostgreSQL') + void ensureIndexOnNestedPostgres() { + assertEquals('CREATE INDEX for nested PostgreSQL field incorrect', + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data#>>'{a,b,c}'))".toString(), + DefinitionQuery.ensureIndexOn(TEST_TABLE, 'nest', List.of('a.b.c'), Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('ensureIndexOn generates nested field | SQLite') + void ensureIndexOnNestedSQLite() { + assertEquals('CREATE INDEX for nested SQLite field incorrect', + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data->'a'->'b'->>'c'))".toString(), + DefinitionQuery.ensureIndexOn(TEST_TABLE, 'nest', List.of('a.b.c'), Dialect.SQLITE)) + } + + @Test + @DisplayName('ensureDocumentIndexOn generates Full | PostgreSQL') + void ensureDocumentIndexOnFullPostgres() { + ForceDialect.postgres() + assertEquals('CREATE INDEX for full document index incorrect', + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)".toString(), + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL)) + } + + @Test + @DisplayName('ensureDocumentIndexOn generates Optimized | PostgreSQL') + void ensureDocumentIndexOnOptimizedPostgres() { + ForceDialect.postgres() + assertEquals('CREATE INDEX for optimized document index incorrect', + "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data jsonb_path_ops)" + .toString(), + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('ensureDocumentIndexOn fails | SQLite') +// void ensureDocumentIndexOnFailsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy new file mode 100644 index 0000000..06fe0b7 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy @@ -0,0 +1,94 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Delete` object + */ +@DisplayName('JVM | Groovy | Query | DeleteQuery') +class DeleteQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), DeleteQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), DeleteQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data->>'a' = :b".toString(), + DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data->>'a' = :b".toString(), + DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE data @> :criteria".toString(), DeleteQuery.byContains(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byContains fails | SQLite') +// void byContainsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { DeleteQuery.byContains(TEST_TABLE) } +// } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals('Delete query not constructed correctly', + "DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + DeleteQuery.byJsonPath(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { DeleteQuery.byJsonPath(TEST_TABLE) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy new file mode 100644 index 0000000..1d7cc15 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy @@ -0,0 +1,135 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.Configuration +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.query.DocumentQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Document` object + */ +@DisplayName('JVM | Groovy | Query | DocumentQuery') +class DocumentQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('insert generates no auto ID | PostgreSQL') + void insertNoAutoPostgres() { + ForceDialect.postgres() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data)".toString(), DocumentQuery.insert(TEST_TABLE)) + } + + @Test + @DisplayName('insert generates no auto ID | SQLite') + void insertNoAutoSQLite() { + ForceDialect.sqlite() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data)".toString(), DocumentQuery.insert(TEST_TABLE)) + } + + @Test + @DisplayName('insert generates auto number | PostgreSQL') + void insertAutoNumberPostgres() { + ForceDialect.postgres() + assertEquals("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || ('{\"id\":' || (SELECT ".toString() + + "COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $TEST_TABLE) || '}')::jsonb)".toString(), + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER)) + } + + @Test + @DisplayName('insert generates auto number | SQLite') + void insertAutoNumberSQLite() { + ForceDialect.sqlite() + assertEquals("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', ".toString() + + "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $TEST_TABLE)))".toString(), + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER)) + } + + @Test + @DisplayName('insert generates auto UUID | PostgreSQL') + void insertAutoUUIDPostgres() { + ForceDialect.postgres() + def query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + assertTrue("Query start not correct (actual: $query)", + query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"")) + assertTrue('Query end not correct', query.endsWith("\"}')")) + } + + @Test + @DisplayName('insert generates auto UUID | SQLite') + void insertAutoUUIDSQLite() { + ForceDialect.sqlite() + def query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + assertTrue("Query start not correct (actual: $query)", + query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '")) + assertTrue('Query end not correct', query.endsWith("'))")) + } + + @Test + @DisplayName('insert generates auto random string | PostgreSQL') + void insertAutoRandomPostgres() { + try { + ForceDialect.postgres() + Configuration.idStringLength = 8 + def query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + assertTrue("Query start not correct (actual: $query)", + query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"")) + assertTrue('Query end not correct', query.endsWith("\"}')")) + assertEquals('Random string length incorrect', 8, + query.replace("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", '') + .replace("\"}')", '').length()) + } finally { + Configuration.idStringLength = 16 + } + } + + @Test + @DisplayName('insert generates auto random string | SQLite') + void insertAutoRandomSQLite() { + ForceDialect.sqlite() + def query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + assertTrue("Query start not correct (actual: $query)", + query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '")) + assertTrue('Query end not correct', query.endsWith("'))")) + assertEquals('Random string length incorrect', Configuration.idStringLength, + query.replace("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '", '').replace("'))", '') + .length()) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('insert fails when no dialect is set') +// void insertFailsUnknown() { +// assertThrows(DocumentException) { DocumentQuery.insert(TEST_TABLE) } +// } + + @Test + @DisplayName('save generates correctly') + void save() { + ForceDialect.postgres() + assertEquals('INSERT ON CONFLICT UPDATE statement not constructed correctly', + "INSERT INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data" + .toString(), + DocumentQuery.save(TEST_TABLE)) + } + + @Test + @DisplayName('update generates successfully') + void update() { + assertEquals('Update query not constructed correctly', "UPDATE $TEST_TABLE SET data = :data".toString(), + DocumentQuery.update(TEST_TABLE)) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy new file mode 100644 index 0000000..4f3502f --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy @@ -0,0 +1,101 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Find` object + */ +@DisplayName('JVM | Groovy | Query | FindQuery') +class FindQueryTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('all generates correctly') + void all() { + assertEquals('Find query not constructed correctly', "SELECT data FROM $TEST_TABLE".toString(), + FindQuery.all(TEST_TABLE)) + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), FindQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), FindQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND (data->>'c')::numeric < :d".toString(), + FindQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b'), Field.less('c', 14, ':d')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND data->>'c' < :d".toString(), + FindQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b'), Field.less('c', 14, ':d')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE data @> :criteria".toString(), FindQuery.byContains(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byContains fails | SQLite') +// void byContainsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { FindQuery.byContains(TEST_TABLE) } +// } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals('Find query not constructed correctly', + "SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + FindQuery.byJsonPath(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { FindQuery.byJsonPath(TEST_TABLE) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy new file mode 100644 index 0000000..f34c7a7 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy @@ -0,0 +1,97 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.query.PatchQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Patch` object + */ +@DisplayName('JVM | Groovy | Query | PatchQuery') +class PatchQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id".toString(), + PatchQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id".toString(), + PatchQuery.byId(TEST_TABLE)) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y".toString(), + PatchQuery.byFields(TEST_TABLE, List.of(Field.equal('z', '', ':y')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y".toString(), + PatchQuery.byFields(TEST_TABLE, List.of(Field.equal('z', '', ':y')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria".toString(), + PatchQuery.byContains(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byContains fails | SQLite') +// void byContainsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { PatchQuery.byContains(TEST_TABLE) } +// } + + @Test + @DisplayName('byJsonPath generates correctly | PostgreSQL') + void byJsonPathPostgres() { + ForceDialect.postgres() + assertEquals('Patch query not constructed correctly', + "UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + PatchQuery.byJsonPath(TEST_TABLE)) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { PatchQuery.byJsonPath(TEST_TABLE) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy new file mode 100644 index 0000000..069bbec --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy @@ -0,0 +1,159 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.Dialect +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.query.QueryUtils +import solutions.bitbadger.documents.support.ForceDialect + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `QueryUtils` class + */ +@DisplayName('JVM | Groovy | Query | QueryUtils') +class QueryUtilsTest { + + /** + * Clear the connection string (resets Dialect) + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('statementWhere generates correctly') + void statementWhere() { + assertEquals('Statements not combined correctly', 'x WHERE y', QueryUtils.statementWhere('x', 'y')) + } + + @Test + @DisplayName('byId generates a numeric ID query | PostgreSQL') + void byIdNumericPostgres() { + ForceDialect.postgres() + assertEquals("test WHERE (data->>'id')::numeric = :id", QueryUtils.byId('test', 9)) + } + + @Test + @DisplayName('byId generates an alphanumeric ID query | PostgreSQL') + void byIdAlphaPostgres() { + ForceDialect.postgres() + assertEquals("unit WHERE data->>'id' = :id", QueryUtils.byId('unit', '18')) + } + + @Test + @DisplayName('byId generates ID query | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals("yo WHERE data->>'id' = :id", QueryUtils.byId('yo', 27)) + } + + @Test + @DisplayName('byFields generates default field query | PostgreSQL') + void byFieldsMultipleDefaultPostgres() { + ForceDialect.postgres() + assertEquals("this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value", + QueryUtils.byFields('this', List.of(Field.equal('a', '', ':the_a'), Field.equal('b', 0, ':b_value')))) + } + + @Test + @DisplayName('byFields generates default field query | SQLite') + void byFieldsMultipleDefaultSQLite() { + ForceDialect.sqlite() + assertEquals("this WHERE data->>'a' = :the_a AND data->>'b' = :b_value", + QueryUtils.byFields('this', List.of(Field.equal('a', '', ':the_a'), Field.equal('b', 0, ':b_value')))) + } + + @Test + @DisplayName('byFields generates ANY field query | PostgreSQL') + void byFieldsMultipleAnyPostgres() { + ForceDialect.postgres() + assertEquals("that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value", + QueryUtils.byFields('that', List.of(Field.equal('a', '', ':the_a'), Field.equal('b', 0, ':b_value')), + FieldMatch.ANY)) + } + + @Test + @DisplayName('byFields generates ANY field query | SQLite') + void byFieldsMultipleAnySQLite() { + ForceDialect.sqlite() + assertEquals("that WHERE data->>'a' = :the_a OR data->>'b' = :b_value", + QueryUtils.byFields('that', List.of(Field.equal('a', '', ':the_a'), Field.equal('b', 0, ':b_value')), + FieldMatch.ANY)) + } + + @Test + @DisplayName('orderBy generates for no fields') + void orderByNone() { + assertEquals('ORDER BY should have been blank (PostgreSQL)', '', QueryUtils.orderBy(List.of(), + Dialect.POSTGRESQL)) + assertEquals('ORDER BY should have been blank (SQLite)', '', QueryUtils.orderBy(List.of(), Dialect.SQLITE)) + } + + @Test + @DisplayName('orderBy generates single, no direction | PostgreSQL') + void orderBySinglePostgres() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY data->>'TestField'", + QueryUtils.orderBy(List.of(Field.named('TestField')), Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('orderBy generates single, no direction | SQLite') + void orderBySingleSQLite() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY data->>'TestField'", + QueryUtils.orderBy(List.of(Field.named('TestField')), Dialect.SQLITE)) + } + + @Test + @DisplayName('orderBy generates multiple with direction | PostgreSQL') + void orderByMultiplePostgres() { + assertEquals('ORDER BY not constructed correctly', + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + QueryUtils.orderBy(List.of(Field.named('Nested.Test.Field DESC'), Field.named('AnotherField'), + Field.named('It DESC')), + Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('orderBy generates multiple with direction | SQLite') + void orderByMultipleSQLite() { + assertEquals('ORDER BY not constructed correctly', + " ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + QueryUtils.orderBy(List.of(Field.named('Nested.Test.Field DESC'), Field.named('AnotherField'), + Field.named('It DESC')), + Dialect.SQLITE)) + } + + @Test + @DisplayName('orderBy generates numeric ordering | PostgreSQL') + void orderByNumericPostgres() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY (data->>'Test')::numeric", + QueryUtils.orderBy(List.of(Field.named('n:Test')), Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('orderBy generates numeric ordering | SQLite') + void orderByNumericSQLite() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY data->>'Test'", + QueryUtils.orderBy(List.of(Field.named('n:Test')), Dialect.SQLITE)) + } + + @Test + @DisplayName('orderBy generates case-insensitive ordering | PostgreSQL') + void orderByCIPostgres() { + assertEquals('ORDER BY not constructed correctly', " ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", + QueryUtils.orderBy(List.of(Field.named('i:Test.Field DESC NULLS FIRST')), Dialect.POSTGRESQL)) + } + + @Test + @DisplayName('orderBy generates case-insensitive ordering | SQLite') + void orderByCISQLite() { + assertEquals('ORDER BY not constructed correctly', + " ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", + QueryUtils.orderBy(List.of(Field.named('i:Test.Field ASC NULLS LAST')), Dialect.SQLITE)) + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala index 63ca4d9..7653c91 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala @@ -29,10 +29,10 @@ class AutoIdSpec extends AnyFunSpec with Matchers { describe("needsAutoId") { it("fails for null document") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.DISABLED, null, "id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.DISABLED, null, "id") } it("fails for missing ID property") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") } it("returns false if disabled") { AutoId.needsAutoId(AutoId.DISABLED, "", "") shouldBe false @@ -62,7 +62,7 @@ class AutoIdSpec extends AnyFunSpec with Matchers { AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id") shouldBe false } it("fails for Number strategy and non-number ID") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") } it("returns true for UUID strategy and blank ID") { AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id") shouldBe true @@ -71,7 +71,7 @@ class AutoIdSpec extends AnyFunSpec with Matchers { AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id") shouldBe false } it("fails for UUID strategy and non-string ID") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") } it("returns true for Random String strategy and blank ID") { AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id") shouldBe true @@ -80,7 +80,7 @@ class AutoIdSpec extends AnyFunSpec with Matchers { AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id") shouldBe false } it("fails for Random String strategy and non-string ID") { - an [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") + a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala index c42e21f..183f4b6 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala @@ -27,7 +27,7 @@ class ConfigurationSpec extends AnyFunSpec with Matchers { describe("dialect") { it("is derived from connection string") { try { - an [DocumentException] should be thrownBy Configuration.dialect() + a [DocumentException] should be thrownBy Configuration.dialect() Configuration.setConnectionString("jdbc:postgresql:db") Configuration.dialect() shouldEqual Dialect.POSTGRESQL } finally { diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala index a1123d1..d54390b 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala @@ -13,7 +13,7 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { describe("withParameterName") { it("fails for invalid name") { - an [DocumentException] should be thrownBy Field.equal("it", "").withParameterName("2424") + a [DocumentException] should be thrownBy Field.equal("it", "").withParameterName("2424") } it("works with colon prefix") { val field = Field.equal("abc", "22").withQualifier("me") @@ -49,36 +49,36 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { describe("path") { it("generates simple unqualified PostgreSQL field") { - Field.greaterOrEqual("SomethingCool", 18) - .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data->>'SomethingCool'" + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "data->>'SomethingCool'" } it("generates simple qualified PostgreSQL field") { - Field.less("SomethingElse", 9).withQualifier("this") - .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "this.data->>'SomethingElse'" + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "this.data->>'SomethingElse'" } it("generates nested unqualified PostgreSQL field") { - Field.equal("My.Nested.Field", "howdy") - .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data#>>'{My,Nested,Field}'" + Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "data#>>'{My,Nested,Field}'" } it("generates nested qualified PostgreSQL field") { - Field.equal("Nest.Away", "doc").withQualifier("bird") - .path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "bird.data#>>'{Nest,Away}'" + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "bird.data#>>'{Nest,Away}'" } it("generates simple unqualified SQLite field") { - Field.greaterOrEqual("SomethingCool", 18) - .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->>'SomethingCool'" + Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "data->>'SomethingCool'" } it("generates simple qualified SQLite field") { - Field.less("SomethingElse", 9).withQualifier("this") - .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "this.data->>'SomethingElse'" + Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "this.data->>'SomethingElse'" } it("generates nested unqualified SQLite field") { - Field.equal("My.Nested.Field", "howdy") - .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->'My'->'Nested'->>'Field'" + Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "data->'My'->'Nested'->>'Field'" } it("generates nested qualified SQLite field") { - Field.equal("Nest.Away", "doc").withQualifier("bird") - .path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual "bird.data->'Nest'->>'Away'" + Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "bird.data->'Nest'->>'Away'" } } @@ -105,8 +105,8 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { } it("generates BETWEEN w/o qualifier, alphanumeric range | PostgreSQL") { ForceDialect.postgres() - Field.between("city", "Atlanta", "Chicago", ":city") - .toWhere shouldEqual "data->>'city' BETWEEN :citymin AND :citymax" + Field.between("city", "Atlanta", "Chicago", ":city").toWhere shouldEqual + "data->>'city' BETWEEN :citymin AND :citymax" } it("generates BETWEEN w/o qualifier | SQLite") { ForceDialect.sqlite() @@ -114,43 +114,43 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { } it("generates BETWEEN w/ qualifier, numeric range | PostgreSQL") { ForceDialect.postgres() - Field.between("age", 13, 17, "@age").withQualifier("test") - .toWhere shouldEqual "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax" + Field.between("age", 13, 17, "@age").withQualifier("test").toWhere shouldEqual + "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax" } it("generates BETWEEN w/ qualifier, alphanumeric range | PostgreSQL") { ForceDialect.postgres() - Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit") - .toWhere shouldEqual "unit.data->>'city' BETWEEN :citymin AND :citymax" + Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere shouldEqual + "unit.data->>'city' BETWEEN :citymin AND :citymax" } it("generates BETWEEN w/ qualifier | SQLite") { ForceDialect.sqlite() - Field.between("age", 13, 17, "@age").withQualifier("my") - .toWhere shouldEqual "my.data->>'age' BETWEEN @agemin AND @agemax" + Field.between("age", 13, 17, "@age").withQualifier("my").toWhere shouldEqual + "my.data->>'age' BETWEEN @agemin AND @agemax" } it("generates IN/any, numeric values | PostgreSQL") { ForceDialect.postgres() - Field.any("even", List(2, 4, 6).asJava, ":nbr") - .toWhere shouldEqual "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)" + Field.any("even", List(2, 4, 6).asJava, ":nbr").toWhere shouldEqual + "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)" } it("generates IN/any, alphanumeric values | PostgreSQL") { ForceDialect.postgres() - Field.any("test", List("Atlanta", "Chicago").asJava, ":city") - .toWhere shouldEqual "data->>'test' IN (:city_0, :city_1)" + Field.any("test", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual + "data->>'test' IN (:city_0, :city_1)" } it("generates IN/any | SQLite") { ForceDialect.sqlite() - Field.any("test", List("Atlanta", "Chicago").asJava, ":city") - .toWhere shouldEqual "data->>'test' IN (:city_0, :city_1)" + Field.any("test", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual + "data->>'test' IN (:city_0, :city_1)" } it("generates inArray | PostgreSQL") { ForceDialect.postgres() - Field.inArray("even", "tbl", List(2, 4, 6, 8).asJava, ":it") - .toWhere shouldEqual "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]" + Field.inArray("even", "tbl", List(2, 4, 6, 8).asJava, ":it").toWhere shouldEqual + "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]" } it("generates inArray | SQLite") { ForceDialect.sqlite() - Field.inArray("test", "tbl", List("Atlanta", "Chicago").asJava, ":city") - .toWhere shouldEqual "EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))" + Field.inArray("test", "tbl", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual + "EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))" } it("generates others w/o qualifier | PostgreSQL") { ForceDialect.postgres() @@ -170,8 +170,8 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { } it("generates parameter w/ qualifier | PostgreSQL") { ForceDialect.postgres() - Field.lessOrEqual("le_field", 18, ":it").withQualifier("q") - .toWhere shouldEqual "(q.data->>'le_field')::numeric <= :it" + Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere shouldEqual + "(q.data->>'le_field')::numeric <= :it" } it("generates parameter w/ qualifier | SQLite") { ForceDialect.sqlite() @@ -391,7 +391,7 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { describe("static constructors") { it("fail for invalid parameter name") { - an [DocumentException] should be thrownBy Field.equal("a", "b", "that ain't it, Jack...") + a [DocumentException] should be thrownBy Field.equal("a", "b", "that ain't it, Jack...") } } @@ -403,12 +403,12 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->>'Simple'" } it("creates a nested PostgreSQL SQL name") { - (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL) - shouldEqual "data#>>'{A,Long,Path,to,the,Property}'") + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual + "data#>>'{A,Long,Path,to,the,Property}'" } it("creates a nested SQLite SQL name") { - (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL) - shouldEqual "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'") + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL) shouldEqual + "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'" } it("creates a simple PostgreSQL JSON name") { Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON) shouldEqual "data->'Simple'" @@ -417,12 +417,12 @@ class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON) shouldEqual "data->'Simple'" } it("creates a nested PostgreSQL JSON name") { - (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON) - shouldEqual "data#>'{A,Long,Path,to,the,Property}'") + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON) shouldEqual + "data#>'{A,Long,Path,to,the,Property}'" } it("creates a nested SQLite JSON name") { - (Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON) - shouldEqual "data->'A'->'Long'->'Path'->'to'->'the'->'Property'") + Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON) shouldEqual + "data->'A'->'Long'->'Path'->'to'->'the'->'Property'" } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala index dbda937..3e393b7 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala @@ -20,7 +20,7 @@ class ParameterSpec extends AnyFunSpec with Matchers { p.getValue should be (null) } it("fails with incorrect prefix") { - an [DocumentException] should be thrownBy Parameter("it", ParameterType.JSON, "") + a [DocumentException] should be thrownBy Parameter("it", ParameterType.JSON, "") } } } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala new file mode 100644 index 0000000..6a24e4c --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala @@ -0,0 +1,56 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class CountQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("all") { + it("generates correctly") { + CountQuery.all(TEST_TABLE) shouldEqual s"SELECT COUNT(*) AS it FROM $TEST_TABLE" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + CountQuery.byFields(TEST_TABLE, List(Field.equal("test", "", ":field0")).asJava) shouldEqual + s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + CountQuery.byFields(TEST_TABLE, List(Field.equal("test", "", ":field0")).asJava) shouldEqual + s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + CountQuery.byContains(TEST_TABLE) shouldEqual s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy CountQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + CountQuery.byJsonPath(TEST_TABLE) shouldEqual + s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy CountQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala new file mode 100644 index 0000000..dbf6aad --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala @@ -0,0 +1,81 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{Dialect, DocumentException, DocumentIndex} +import solutions.bitbadger.documents.query.DefinitionQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class DefinitionQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("ensureTableFor") { + it("generates correctly") { + DefinitionQuery.ensureTableFor("my.table", "JSONB") shouldEqual + "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)" + } + } + + describe("ensureTable") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DefinitionQuery.ensureTable(TEST_TABLE) shouldEqual + s"CREATE TABLE IF NOT EXISTS $TEST_TABLE (data JSONB NOT NULL)" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + DefinitionQuery.ensureTable(TEST_TABLE) shouldEqual + s"CREATE TABLE IF NOT EXISTS $TEST_TABLE (data TEXT NOT NULL)" + } + it("fails when no dialect is set") { + a [DocumentException] should be thrownBy DefinitionQuery.ensureTable(TEST_TABLE) + } + } + + describe("ensureKey") { + it("generates correctly with schema") { + DefinitionQuery.ensureKey("test.table", Dialect.POSTGRESQL) shouldEqual + "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))" + } + it("generates correctly without schema") { + DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE) shouldEqual + s"CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))" + } + } + + describe("ensureIndexOn") { + it("generates multiple fields and directions") { + DefinitionQuery.ensureIndexOn("test.table", "gibberish", List("taco", "guac DESC", "salsa ASC").asJava, + Dialect.POSTGRESQL) shouldEqual + "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" + } + it("generates nested field | PostgreSQL") { + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List("a.b.c").asJava, Dialect.POSTGRESQL) shouldEqual + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data#>>'{a,b,c}'))" + } + it("generates nested field | SQLite") { + DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List("a.b.c").asJava, Dialect.SQLITE) shouldEqual + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data->'a'->'b'->>'c'))" + } + } + + describe("ensureDocumentOn") { + it("generates Full | PostgreSQL") { + ForceDialect.postgres() + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) shouldEqual + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)" + } + it("generates Optimized | PostgreSQL") { + ForceDialect.postgres() + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED) shouldEqual + s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data jsonb_path_ops)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala new file mode 100644 index 0000000..728d1cd --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala @@ -0,0 +1,61 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class DeleteQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("byId") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DeleteQuery.byId(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + DeleteQuery.byId(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DeleteQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b")).asJava) shouldEqual + s"DELETE FROM $TEST_TABLE WHERE data->>'a' = :b" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + DeleteQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b")).asJava) shouldEqual + s"DELETE FROM $TEST_TABLE WHERE data->>'a' = :b" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DeleteQuery.byContains(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data @> :criteria" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy DeleteQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + DeleteQuery.byJsonPath(TEST_TABLE) shouldEqual + s"DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy DeleteQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala new file mode 100644 index 0000000..adbef90 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala @@ -0,0 +1,85 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{AutoId, Configuration, DocumentException} +import solutions.bitbadger.documents.query.DocumentQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +class DocumentQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("insert") { + it("generates no auto ID | PostgreSQL") { + ForceDialect.postgres() + DocumentQuery.insert(TEST_TABLE) shouldEqual s"INSERT INTO $TEST_TABLE VALUES (:data)" + } + it("generates no auto ID | SQLite") { + ForceDialect.sqlite() + DocumentQuery.insert(TEST_TABLE) shouldEqual s"INSERT INTO $TEST_TABLE VALUES (:data)" + } + it("generates auto number | PostgreSQL") { + ForceDialect.postgres() + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER) shouldEqual + s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || ('{\"id\":' " + + s"|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $TEST_TABLE) || '}')::jsonb)" + } + it("generates auto number | SQLite") { + ForceDialect.sqlite() + DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER) shouldEqual + s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', " + + s"(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $TEST_TABLE)))" + } + it("generates auto UUID | PostgreSQL") { + ForceDialect.postgres() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + query should startWith (s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"") + query should endWith ("\"}')") + } + it("generates auto UUID | SQLite") { + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) + query should startWith (s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '") + query should endWith ("'))") + } + it("generates auto random string | PostgreSQL") { + try { + ForceDialect.postgres() + Configuration.idStringLength = 8 + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + query should startWith (s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"") + query should endWith ("\"}')") + query.replace(s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "") + .length shouldEqual 8 + } finally { + Configuration.idStringLength = 16 + } + } + it("insert generates auto random string | SQLite") { + ForceDialect.sqlite() + val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) + query should startWith (s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '") + query should endWith ("'))") + query.replace(s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '", "").replace("'))", "") + .length shouldEqual Configuration.idStringLength + } + it("fails when no dialect is set") { + a [DocumentException] should be thrownBy DocumentQuery.insert(TEST_TABLE) + } + } + + describe("save") { + it("generates correctly") { + ForceDialect.postgres() + DocumentQuery.save(TEST_TABLE) shouldEqual + s"INSERT INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data" + } + } + + describe("update") { + it("generates successfully") { + DocumentQuery.update(TEST_TABLE) shouldEqual s"UPDATE $TEST_TABLE SET data = :data" + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala new file mode 100644 index 0000000..d539543 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala @@ -0,0 +1,64 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.ExistsQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class ExistsQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("byId") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + ExistsQuery.byId(TEST_TABLE) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + ExistsQuery.byId(TEST_TABLE) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + ExistsQuery.byFields(TEST_TABLE, List(Field.equal("it", 7, ":test")).asJava) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE (data->>'it')::numeric = :test) AS it" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + ExistsQuery.byFields(TEST_TABLE, List(Field.equal("it", 7, ":test")).asJava) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'it' = :test) AS it" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + ExistsQuery.byContains(TEST_TABLE) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data @> :criteria) AS it" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy ExistsQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + ExistsQuery.byJsonPath(TEST_TABLE) shouldEqual + s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)) AS it" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy ExistsQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala new file mode 100644 index 0000000..5d8a585 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala @@ -0,0 +1,67 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class FindQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("all") { + it("generates correctly") { + FindQuery.all(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE" + } + } + + describe("byId") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + FindQuery.byId(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + FindQuery.byId(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + FindQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b"), Field.less("c", 14, ":d")).asJava) shouldEqual + s"SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND (data->>'c')::numeric < :d" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + FindQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b"), Field.less("c", 14, ":d")).asJava) shouldEqual + s"SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND data->>'c' < :d" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + FindQuery.byContains(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data @> :criteria" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy FindQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + FindQuery.byJsonPath(TEST_TABLE) shouldEqual + s"SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy FindQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala new file mode 100644 index 0000000..a37a8b3 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala @@ -0,0 +1,63 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.PatchQuery +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +class PatchQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("byId") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + PatchQuery.byId(TEST_TABLE) shouldEqual s"UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + PatchQuery.byId(TEST_TABLE) shouldEqual + s"UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id" + } + } + + describe("byFields") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + PatchQuery.byFields(TEST_TABLE, List(Field.equal("z", "", ":y")).asJava) shouldEqual + s"UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y" + } + it("generates correctly | SQLite") { + ForceDialect.sqlite() + PatchQuery.byFields(TEST_TABLE, List(Field.equal("z", "", ":y")).asJava) shouldEqual + s"UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y" + } + } + + describe("byContains") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + PatchQuery.byContains(TEST_TABLE) shouldEqual + s"UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy PatchQuery.byContains(TEST_TABLE) + } + } + + describe("byJsonPath") { + it("generates correctly | PostgreSQL") { + ForceDialect.postgres() + PatchQuery.byJsonPath(TEST_TABLE) shouldEqual + s"UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)" + } + it("fails | SQLite") { + ForceDialect.sqlite() + a [DocumentException] should be thrownBy PatchQuery.byJsonPath(TEST_TABLE) + } + } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala new file mode 100644 index 0000000..0b58169 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala @@ -0,0 +1,103 @@ +package solutions.bitbadger.documents.scala.query + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import solutions.bitbadger.documents.{Dialect, Field, FieldMatch} +import solutions.bitbadger.documents.query.QueryUtils +import solutions.bitbadger.documents.scala.ClearConfiguration +import solutions.bitbadger.documents.support.ForceDialect + +import scala.jdk.CollectionConverters.* + +class QueryUtilsSpec extends AnyFunSpec with ClearConfiguration with Matchers { + + describe("statementWhere") { + it("generates correctly") { + QueryUtils.statementWhere("x", "y") shouldEqual "x WHERE y" + } + } + + describe("byId") { + it("generates a numeric ID query | PostgreSQL") { + ForceDialect.postgres() + QueryUtils.byId("test", 9) shouldEqual "test WHERE (data->>'id')::numeric = :id" + } + it("generates an alphanumeric ID query | PostgreSQL") { + ForceDialect.postgres() + QueryUtils.byId("unit", "18") shouldEqual "unit WHERE data->>'id' = :id" + } + it("generates ID query | SQLite") { + ForceDialect.sqlite() + QueryUtils.byId("yo", 27) shouldEqual "yo WHERE data->>'id' = :id" + } + } + + describe("byFields") { + it("generates default field query | PostgreSQL") { + ForceDialect.postgres() + QueryUtils.byFields("this", + List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava) shouldEqual + "this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value" + } + it("generates default field query | SQLite") { + ForceDialect.sqlite() + QueryUtils.byFields("this", + List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava) shouldEqual + "this WHERE data->>'a' = :the_a AND data->>'b' = :b_value" + } + it("generates ANY field query | PostgreSQL") { + ForceDialect.postgres() + QueryUtils.byFields("that", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava, + FieldMatch.ANY) shouldEqual + "that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value" + } + it("generates ANY field query | SQLite") { + ForceDialect.sqlite() + QueryUtils.byFields("that", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava, + FieldMatch.ANY) shouldEqual + "that WHERE data->>'a' = :the_a OR data->>'b' = :b_value" + } + } + + describe("orderBy") { + it("generates for no fields") { + QueryUtils.orderBy(List().asJava, Dialect.POSTGRESQL) shouldEqual "" + QueryUtils.orderBy(List().asJava, Dialect.SQLITE) shouldEqual "" + } + it("generates single, no direction | PostgreSQL") { + QueryUtils.orderBy(List(Field.named("TestField")).asJava, Dialect.POSTGRESQL) shouldEqual + " ORDER BY data->>'TestField'" + } + it("generates single, no direction | SQLite") { + QueryUtils.orderBy(List(Field.named("TestField")).asJava, Dialect.SQLITE) shouldEqual + " ORDER BY data->>'TestField'" + } + it("generates multiple with direction | PostgreSQL") { + QueryUtils.orderBy( + List(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")).asJava, + Dialect.POSTGRESQL) shouldEqual + " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC" + } + it("generates multiple with direction | SQLite") { + QueryUtils.orderBy( + List(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")).asJava, + Dialect.SQLITE) shouldEqual + " ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC" + } + it("generates numeric ordering | PostgreSQL") { + QueryUtils.orderBy(List(Field.named("n:Test")).asJava, Dialect.POSTGRESQL) shouldEqual + " ORDER BY (data->>'Test')::numeric" + } + it("generates numeric ordering | SQLite") { + QueryUtils.orderBy(List(Field.named("n:Test")).asJava, Dialect.SQLITE) shouldEqual " ORDER BY data->>'Test'" + } + it("generates case-insensitive ordering | PostgreSQL") { + QueryUtils.orderBy(List(Field.named("i:Test.Field DESC NULLS FIRST")).asJava, Dialect.POSTGRESQL) shouldEqual + " ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST" + } + it("generates case-insensitive ordering | SQLite") { + QueryUtils.orderBy(List(Field.named("i:Test.Field ASC NULLS LAST")).asJava, Dialect.SQLITE) shouldEqual + " ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST" + } + } +} -- 2.47.2 From ac1a3940b1c38ce6c26ec153326e229f7d31e4be Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 18 Mar 2025 17:02:30 -0400 Subject: [PATCH 55/88] Move Scala tests to JUnit; begin work onintegration tests --- .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 + src/jvm/pom.xml | 24 +- .../groovy/query/RemoveFieldsQueryTest.groovy | 107 ++++ .../documents/groovy/query/WhereTest.groovy | 175 ++++++ .../documents/scala/AutoIdSpec.scala | 86 --- .../documents/scala/AutoIdTest.scala | 128 +++++ .../documents/scala/ClearConfiguration.scala | 11 - .../documents/scala/ConfigurationSpec.scala | 38 -- .../documents/scala/ConfigurationTest.scala | 35 ++ .../documents/scala/DialectSpec.scala | 27 - .../documents/scala/DialectTest.scala | 34 ++ .../documents/scala/DocumentIndexSpec.scala | 17 - .../documents/scala/DocumentIndexTest.scala | 19 + .../documents/scala/FieldMatchSpec.scala | 20 - .../documents/scala/FieldMatchTest.scala | 22 + .../bitbadger/documents/scala/FieldSpec.scala | 428 -------------- .../bitbadger/documents/scala/FieldTest.scala | 539 ++++++++++++++++++ .../bitbadger/documents/scala/OpSpec.scala | 44 -- .../bitbadger/documents/scala/OpTest.scala | 64 +++ .../documents/scala/ParameterNameSpec.scala | 23 - .../documents/scala/ParameterNameTest.scala | 25 + .../documents/scala/ParameterSpec.scala | 26 - .../documents/scala/ParameterTest.scala | 30 + .../scala/query/CountQuerySpec.scala | 56 -- .../scala/query/CountQueryTest.scala | 69 +++ .../scala/query/DefinitionQuerySpec.scala | 81 --- .../scala/query/DefinitionQueryTest.scala | 107 ++++ .../scala/query/DeleteQuerySpec.scala | 61 -- .../scala/query/DeleteQueryTest.scala | 77 +++ .../scala/query/DocumentQuerySpec.scala | 85 --- .../scala/query/DocumentQueryTest.scala | 113 ++++ .../scala/query/ExistsQuerySpec.scala | 64 --- .../scala/query/ExistsQueryTest.scala | 77 +++ .../documents/scala/query/FindQuerySpec.scala | 67 --- .../documents/scala/query/FindQueryTest.scala | 82 +++ .../scala/query/PatchQuerySpec.scala | 63 -- .../scala/query/PatchQueryTest.scala | 75 +++ .../scala/query/QueryUtilsSpec.scala | 103 ---- .../scala/query/QueryUtilsTest.scala | 138 +++++ .../scala/query/RemoveFieldsQueryTest.scala | 85 +++ .../documents/scala/query/WhereTest.scala | 143 +++++ .../scala/support/JsonDocument.scala | 20 + .../documents/scala/support/SubDocument.scala | 3 + 45 files changed, 2247 insertions(+), 1321 deletions(-) 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 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQueryTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQueryTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQueryTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQueryTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQueryTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQueryTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQueryTest.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsTest.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/RemoveFieldsQueryTest.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/WhereTest.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/JsonDocument.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/SubDocument.scala 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/src/jvm/pom.xml b/src/jvm/pom.xml index 674731b..343f18b 100644 --- a/src/jvm/pom.xml +++ b/src/jvm/pom.xml @@ -37,9 +37,9 @@ test - org.scalatest - scalatest_3 - 3.2.9 + org.scala-lang + scala3-library_3 + 3.5.2 test @@ -134,24 +134,6 @@ --add-opens java.base/java.lang=ALL-UNNAMED - - org.scalatest - scalatest-maven-plugin - 2.2.0 - - ${project.build.directory}/surefire-reports - . - WDF TestSuite.txt - - - - test - - test - - - - maven-failsafe-plugin 2.22.2 diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy new file mode 100644 index 0000000..44acb89 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy @@ -0,0 +1,107 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.query.RemoveFieldsQuery +import solutions.bitbadger.documents.support.ForceDialect + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `RemoveFields` object + */ +@DisplayName('JVM | Groovy | Query | RemoveFieldsQuery') +class RemoveFieldsQueryTest { + + /** + * Reset the dialect + */ + @AfterEach + void cleanUp() { + ForceDialect.none() + } + + @Test + @DisplayName('byId generates correctly | PostgreSQL') + void byIdPostgres() { + ForceDialect.postgres() + assertEquals('Remove Fields query not constructed correctly', + "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}')))) + } + + @Test + @DisplayName('byId generates correctly | SQLite') + void byIdSQLite() { + ForceDialect.sqlite() + assertEquals('Remove Field query not constructed correctly', + "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')))) + } + + @Test + @DisplayName('byFields generates correctly | PostgreSQL') + void byFieldsPostgres() { + ForceDialect.postgres() + assertEquals('Remove Field query not constructed correctly', + "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')))) + } + + @Test + @DisplayName('byFields generates correctly | SQLite') + void byFieldsSQLite() { + ForceDialect.sqlite() + assertEquals('Remove Field query not constructed correctly', + "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')))) + } + + @Test + @DisplayName('byContains generates correctly | PostgreSQL') + void byContainsPostgres() { + ForceDialect.postgres() + assertEquals('Remove Field query not constructed correctly', + "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}')))) + } + +// TODO: resolve java.base open issue +// @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('Remove Field query not constructed correctly', + "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}')))) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('byJsonPath fails | SQLite') +// void byJsonPathSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { RemoveFieldsQuery.byJsonPath(TEST_TABLE, List.of()) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy new file mode 100644 index 0000000..d65a115 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy @@ -0,0 +1,175 @@ +package solutions.bitbadger.documents.groovy.query + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.FieldMatch +import solutions.bitbadger.documents.query.Where +import solutions.bitbadger.documents.support.ForceDialect + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Where` object + */ +@DisplayName('JVM | 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')) + } + +// TODO: resolve java.base open issue +// @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')) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('jsonPathMatches fails | SQLite') +// void jsonPathFailsSQLite() { +// ForceDialect.sqlite() +// assertThrows(DocumentException) { Where.jsonPathMatches() } +// } +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala deleted file mode 100644 index 7653c91..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdSpec.scala +++ /dev/null @@ -1,86 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.scala.support.{ByteIdClass, IntIdClass, LongIdClass, ShortIdClass, StringIdClass} -import solutions.bitbadger.documents.{AutoId, DocumentException} - -class AutoIdSpec extends AnyFunSpec with Matchers { - - describe("generateUUID") { - it("generates a UUID string") { - AutoId.generateUUID().length shouldEqual 32 - } - } - - describe("generateRandomString") { - it("generates a random hex character string of an even length") { - AutoId.generateRandomString(8).length shouldEqual 8 - } - it("generates a random hex character string of an odd length") { - AutoId.generateRandomString(11).length shouldEqual 11 - } - it("generates different random hex character strings") { - val result1 = AutoId.generateRandomString(16) - val result2 = AutoId.generateRandomString(16) - result1 should not be theSameInstanceAs (result2) - } - } - - describe("needsAutoId") { - it("fails for null document") { - a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.DISABLED, null, "id") - } - it("fails for missing ID property") { - a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") - } - it("returns false if disabled") { - AutoId.needsAutoId(AutoId.DISABLED, "", "") shouldBe false - } - it("returns true for Number strategy and byte ID of 0") { - AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id") shouldBe true - } - it("returns false for Number strategy and byte ID of non-0") { - AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id") shouldBe false - } - it("returns true for Number strategy and short ID of 0") { - AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id") shouldBe true - } - it("returns false for Number strategy and short ID of non-0") { - AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id") shouldBe false - } - it("returns true for Number strategy and int ID of 0") { - AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id") shouldBe true - } - it("returns false for Number strategy and int ID of non-0") { - AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id") shouldBe false - } - it("returns true for Number strategy and long ID of 0") { - AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id") shouldBe true - } - it("returns false for Number strategy and long ID of non-0") { - AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(2), "id") shouldBe false - } - it("fails for Number strategy and non-number ID") { - a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") - } - it("returns true for UUID strategy and blank ID") { - AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id") shouldBe true - } - it("returns false for UUID strategy and non-blank ID") { - AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id") shouldBe false - } - it("fails for UUID strategy and non-string ID") { - a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") - } - it("returns true for Random String strategy and blank ID") { - AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id") shouldBe true - } - it("returns false for Random String strategy and non-blank ID") { - AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id") shouldBe false - } - it("fails for Random String strategy and non-string ID") { - a [DocumentException] should be thrownBy AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdTest.scala new file mode 100644 index 0000000..6c41e8f --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdTest.scala @@ -0,0 +1,128 @@ +package solutions.bitbadger.documents.scala + +import org.junit.jupiter.api.Assertions._ +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.scala.support.{ByteIdClass, IntIdClass, LongIdClass, ShortIdClass, StringIdClass} +import solutions.bitbadger.documents.{AutoId, DocumentException} + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala deleted file mode 100644 index b2b64cf..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ClearConfiguration.scala +++ /dev/null @@ -1,11 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.{BeforeAndAfterEach, Suite} -import solutions.bitbadger.documents.support.ForceDialect - -trait ClearConfiguration extends BeforeAndAfterEach { this: Suite => - - override def afterEach(): Unit = - try super.afterEach () - finally ForceDialect.none() -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala deleted file mode 100644 index 183f4b6..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationSpec.scala +++ /dev/null @@ -1,38 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{AutoId, Configuration, Dialect, DocumentException} - -class ConfigurationSpec extends AnyFunSpec with Matchers { - - describe("idField") { - it("defaults to `id`") { - Configuration.idField shouldEqual "id" - } - } - - describe("autoIdStrategy") { - it("defaults to `DISABLED`") { - Configuration.autoIdStrategy shouldEqual AutoId.DISABLED - } - } - - describe("idStringLength") { - it("defaults to 16") { - Configuration.idStringLength shouldEqual 16 - } - } - - describe("dialect") { - it("is derived from connection string") { - try { - a [DocumentException] should be thrownBy Configuration.dialect() - Configuration.setConnectionString("jdbc:postgresql:db") - Configuration.dialect() shouldEqual Dialect.POSTGRESQL - } finally { - Configuration.setConnectionString(null) - } - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationTest.scala new file mode 100644 index 0000000..1864216 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationTest.scala @@ -0,0 +1,35 @@ +package solutions.bitbadger.documents.scala + +import org.junit.jupiter.api.{DisplayName, Test} +import org.junit.jupiter.api.Assertions._ +import solutions.bitbadger.documents.{AutoId, Configuration, Dialect, DocumentException} + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala deleted file mode 100644 index 7321fe0..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectSpec.scala +++ /dev/null @@ -1,27 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{Dialect, DocumentException} - -class DialectSpec extends AnyFunSpec with Matchers { - - describe("deriveFromConnectionString") { - it("derives PostgreSQL correctly") { - Dialect.deriveFromConnectionString("jdbc:postgresql:db") shouldEqual Dialect.POSTGRESQL - } - it("derives SQLite correctly") { - Dialect.deriveFromConnectionString("jdbc:sqlite:memory") shouldEqual Dialect.SQLITE - } - it("fails when the connection string is unknown") { - try { - Dialect.deriveFromConnectionString("SQL Server") - fail("Dialect derivation should have failed") - } catch { - case ex: DocumentException => - ex.getMessage should not be null - ex.getMessage should include ("[SQL Server]") - } - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectTest.scala new file mode 100644 index 0000000..5089914 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectTest.scala @@ -0,0 +1,34 @@ +package solutions.bitbadger.documents.scala + +import org.junit.jupiter.api.{DisplayName, Test} +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{Dialect, DocumentException} + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala deleted file mode 100644 index 3e21f8e..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexSpec.scala +++ /dev/null @@ -1,17 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.DocumentIndex - -class DocumentIndexSpec extends AnyFunSpec with Matchers { - - describe("sql") { - it("returns blank for FULL") { - DocumentIndex.FULL.getSql shouldEqual "" - } - it("returns jsonb_path_ops for OPTIMIZED") { - DocumentIndex.OPTIMIZED.getSql shouldEqual " jsonb_path_ops" - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexTest.scala new file mode 100644 index 0000000..fd372bf --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexTest.scala @@ -0,0 +1,19 @@ +package solutions.bitbadger.documents.scala + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.DocumentIndex + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala deleted file mode 100644 index 07d0843..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchSpec.scala +++ /dev/null @@ -1,20 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.FieldMatch - -/** - * Unit tests for the `FieldMatch` enum - */ -class FieldMatchSpec extends AnyFunSpec with Matchers { - - describe("sql") { - it("returns OR for ANY") { - FieldMatch.ANY.getSql shouldEqual "OR" - } - it("returns AND for ALL") { - FieldMatch.ALL.getSql shouldEqual "AND" - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala new file mode 100644 index 0000000..4b2440b --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala @@ -0,0 +1,22 @@ +package solutions.bitbadger.documents.scala + +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("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala deleted file mode 100644 index d54390b..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldSpec.scala +++ /dev/null @@ -1,428 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.{Dialect, DocumentException, Field, FieldFormat, Op} - -import scala.jdk.CollectionConverters.* - -class FieldSpec extends AnyFunSpec with ClearConfiguration with Matchers { - - // ~~~ INSTANCE METHODS ~~~ - - describe("withParameterName") { - it("fails for invalid name") { - a [DocumentException] should be thrownBy Field.equal("it", "").withParameterName("2424") - } - it("works with colon prefix") { - val field = Field.equal("abc", "22").withQualifier("me") - val withParam = field.withParameterName(":test") - withParam should not be theSameInstanceAs (field) - withParam.getName shouldEqual field.getName - withParam.getComparison shouldEqual field.getComparison - withParam.getParameterName shouldEqual ":test" - withParam.getQualifier shouldEqual field.getQualifier - } - it("works with at-sign prefix") { - val field = Field.equal("def", "44") - val withParam = field.withParameterName("@unit") - withParam should not be theSameInstanceAs (field) - withParam.getName shouldEqual field.getName - withParam.getComparison shouldEqual field.getComparison - withParam.getParameterName shouldEqual "@unit" - withParam.getQualifier shouldEqual field.getQualifier - } - } - - describe("withQualifier") { - it("sets qualifier correctly") { - val field = Field.equal("j", "k") - val withQual = field.withQualifier("test") - withQual should not be theSameInstanceAs (field) - withQual.getName shouldEqual field.getName - withQual.getComparison shouldEqual field.getComparison - withQual.getParameterName shouldEqual field.getParameterName - withQual.getQualifier shouldEqual "test" - } - } - - describe("path") { - it("generates simple unqualified PostgreSQL field") { - Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual - "data->>'SomethingCool'" - } - it("generates simple qualified PostgreSQL field") { - Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual - "this.data->>'SomethingElse'" - } - it("generates nested unqualified PostgreSQL field") { - Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual - "data#>>'{My,Nested,Field}'" - } - it("generates nested qualified PostgreSQL field") { - Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual - "bird.data#>>'{Nest,Away}'" - } - it("generates simple unqualified SQLite field") { - Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual - "data->>'SomethingCool'" - } - it("generates simple qualified SQLite field") { - Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual - "this.data->>'SomethingElse'" - } - it("generates nested unqualified SQLite field") { - Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual - "data->'My'->'Nested'->>'Field'" - } - it("generates nested qualified SQLite field") { - Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL) shouldEqual - "bird.data->'Nest'->>'Away'" - } - } - - describe("toWhere") { - it("generates exists w/o qualifier | PostgreSQL") { - ForceDialect.postgres() - Field.exists("that_field").toWhere shouldEqual "data->>'that_field' IS NOT NULL" - } - it("generates exists w/o qualifier | SQLite") { - ForceDialect.sqlite() - Field.exists("that_field").toWhere shouldEqual "data->>'that_field' IS NOT NULL" - } - it("generates not-exists w/o qualifier | PostgreSQL") { - ForceDialect.postgres() - Field.notExists("a_field").toWhere shouldEqual "data->>'a_field' IS NULL" - } - it("generates not-exists w/o qualifier | SQLite") { - ForceDialect.sqlite() - Field.notExists("a_field").toWhere shouldEqual "data->>'a_field' IS NULL" - } - it("generates BETWEEN w/o qualifier, numeric range | PostgreSQL") { - ForceDialect.postgres() - Field.between("age", 13, 17, "@age").toWhere shouldEqual "(data->>'age')::numeric BETWEEN @agemin AND @agemax" - } - it("generates BETWEEN w/o qualifier, alphanumeric range | PostgreSQL") { - ForceDialect.postgres() - Field.between("city", "Atlanta", "Chicago", ":city").toWhere shouldEqual - "data->>'city' BETWEEN :citymin AND :citymax" - } - it("generates BETWEEN w/o qualifier | SQLite") { - ForceDialect.sqlite() - Field.between("age", 13, 17, "@age").toWhere shouldEqual "data->>'age' BETWEEN @agemin AND @agemax" - } - it("generates BETWEEN w/ qualifier, numeric range | PostgreSQL") { - ForceDialect.postgres() - Field.between("age", 13, 17, "@age").withQualifier("test").toWhere shouldEqual - "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax" - } - it("generates BETWEEN w/ qualifier, alphanumeric range | PostgreSQL") { - ForceDialect.postgres() - Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere shouldEqual - "unit.data->>'city' BETWEEN :citymin AND :citymax" - } - it("generates BETWEEN w/ qualifier | SQLite") { - ForceDialect.sqlite() - Field.between("age", 13, 17, "@age").withQualifier("my").toWhere shouldEqual - "my.data->>'age' BETWEEN @agemin AND @agemax" - } - it("generates IN/any, numeric values | PostgreSQL") { - ForceDialect.postgres() - Field.any("even", List(2, 4, 6).asJava, ":nbr").toWhere shouldEqual - "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)" - } - it("generates IN/any, alphanumeric values | PostgreSQL") { - ForceDialect.postgres() - Field.any("test", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual - "data->>'test' IN (:city_0, :city_1)" - } - it("generates IN/any | SQLite") { - ForceDialect.sqlite() - Field.any("test", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual - "data->>'test' IN (:city_0, :city_1)" - } - it("generates inArray | PostgreSQL") { - ForceDialect.postgres() - Field.inArray("even", "tbl", List(2, 4, 6, 8).asJava, ":it").toWhere shouldEqual - "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]" - } - it("generates inArray | SQLite") { - ForceDialect.sqlite() - Field.inArray("test", "tbl", List("Atlanta", "Chicago").asJava, ":city").toWhere shouldEqual - "EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))" - } - it("generates others w/o qualifier | PostgreSQL") { - ForceDialect.postgres() - Field.equal("some_field", "", ":value").toWhere shouldEqual "data->>'some_field' = :value" - } - it("generates others w/o qualifier | SQLite") { - ForceDialect.sqlite() - Field.equal("some_field", "", ":value").toWhere shouldEqual "data->>'some_field' = :value" - } - it("generates no-parameter w/ qualifier | PostgreSQL") { - ForceDialect.postgres() - Field.exists("no_field").withQualifier("test").toWhere shouldEqual "test.data->>'no_field' IS NOT NULL" - } - it("generates no-parameter w/ qualifier | SQLite") { - ForceDialect.sqlite() - Field.exists("no_field").withQualifier("test").toWhere shouldEqual "test.data->>'no_field' IS NOT NULL" - } - it("generates parameter w/ qualifier | PostgreSQL") { - ForceDialect.postgres() - Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere shouldEqual - "(q.data->>'le_field')::numeric <= :it" - } - it("generates parameter w/ qualifier | SQLite") { - ForceDialect.sqlite() - Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere shouldEqual "q.data->>'le_field' <= :it" - } - } - - // ~~~ STATIC CONSTRUCTOR TESTS ~~~ - - describe("equal") { - it("constructs a field w/o parameter name") { - val field = Field.equal("Test", 14) - field.getName shouldEqual "Test" - field.getComparison.getOp shouldEqual Op.EQUAL - field.getComparison.getValue shouldEqual 14 - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.equal("Test", 14, ":w") - field.getName shouldEqual "Test" - field.getComparison.getOp shouldEqual Op.EQUAL - field.getComparison.getValue shouldEqual 14 - field.getParameterName shouldEqual ":w" - field.getQualifier should be (null) - } - } - - describe("greater") { - it("constructs a field w/o parameter name") { - val field = Field.greater("Great", "night") - field.getName shouldEqual "Great" - field.getComparison.getOp shouldEqual Op.GREATER - field.getComparison.getValue shouldEqual "night" - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.greater("Great", "night", ":yeah") - field.getName shouldEqual "Great" - field.getComparison.getOp shouldEqual Op.GREATER - field.getComparison.getValue shouldEqual "night" - field.getParameterName shouldEqual ":yeah" - field.getQualifier should be (null) - } - } - - describe("greaterOrEqual") { - it("constructs a field w/o parameter name") { - val field = Field.greaterOrEqual("Nice", 88L) - field.getName shouldEqual "Nice" - field.getComparison.getOp shouldEqual Op.GREATER_OR_EQUAL - field.getComparison.getValue shouldEqual 88L - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.greaterOrEqual("Nice", 88L, ":nice") - field.getName shouldEqual "Nice" - field.getComparison.getOp shouldEqual Op.GREATER_OR_EQUAL - field.getComparison.getValue shouldEqual 88L - field.getParameterName shouldEqual ":nice" - field.getQualifier should be (null) - } - } - - describe("less") { - it("constructs a field w/o parameter name") { - val field = Field.less("Lesser", "seven") - field.getName shouldEqual "Lesser" - field.getComparison.getOp shouldEqual Op.LESS - field.getComparison.getValue shouldEqual "seven" - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.less("Lesser", "seven", ":max") - field.getName shouldEqual "Lesser" - field.getComparison.getOp shouldEqual Op.LESS - field.getComparison.getValue shouldEqual "seven" - field.getParameterName shouldEqual ":max" - field.getQualifier should be (null) - } - } - - describe("lessOrEqual") { - it("constructs a field w/o parameter name") { - val field = Field.lessOrEqual("Nobody", "KNOWS") - field.getName shouldEqual "Nobody" - field.getComparison.getOp shouldEqual Op.LESS_OR_EQUAL - field.getComparison.getValue shouldEqual "KNOWS" - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.lessOrEqual("Nobody", "KNOWS", ":nope") - field.getName shouldEqual "Nobody" - field.getComparison.getOp shouldEqual Op.LESS_OR_EQUAL - field.getComparison.getValue shouldEqual "KNOWS" - field.getParameterName shouldEqual ":nope" - field.getQualifier should be (null) - } - } - - describe("notEqual") { - it("constructs a field w/o parameter name") { - val field = Field.notEqual("Park", "here") - field.getName shouldEqual "Park" - field.getComparison.getOp shouldEqual Op.NOT_EQUAL - field.getComparison.getValue shouldEqual "here" - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.notEqual("Park", "here", ":now") - field.getName shouldEqual "Park" - field.getComparison.getOp shouldEqual Op.NOT_EQUAL - field.getComparison.getValue shouldEqual "here" - field.getParameterName shouldEqual ":now" - field.getQualifier should be (null) - } - } - - describe("between") { - it("constructs a field w/o parameter name") { - val field = Field.between("Age", 18, 49) - field.getName shouldEqual "Age" - field.getComparison.getOp shouldEqual Op.BETWEEN - field.getComparison.getValue.getFirst shouldEqual 18 - field.getComparison.getValue.getSecond shouldEqual 49 - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.between("Age", 18, 49, ":limit") - field.getName shouldEqual "Age" - field.getComparison.getOp shouldEqual Op.BETWEEN - field.getComparison.getValue.getFirst shouldEqual 18 - field.getComparison.getValue.getSecond shouldEqual 49 - field.getParameterName shouldEqual ":limit" - field.getQualifier should be (null) - } - } - - describe("any") { - it("constructs a field w/o parameter name") { - val field = Field.any("Here", List(8, 16, 32).asJava) - field.getName shouldEqual "Here" - field.getComparison.getOp shouldEqual Op.IN - field.getComparison.getValue should be (List(8, 16, 32).asJava) - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.any("Here", List(8, 16, 32).asJava, ":list") - field.getName shouldEqual "Here" - field.getComparison.getOp shouldEqual Op.IN - field.getComparison.getValue should be (List(8, 16, 32).asJava) - field.getParameterName shouldEqual ":list" - field.getQualifier should be (null) - } - } - - describe("inArray") { - it("constructs a field w/o parameter name") { - val field = Field.inArray("ArrayField", "table", List("z").asJava) - field.getName shouldEqual "ArrayField" - field.getComparison.getOp shouldEqual Op.IN_ARRAY - field.getComparison.getValue.getFirst shouldEqual "table" - field.getComparison.getValue.getSecond should be (List("z").asJava) - field.getParameterName should be (null) - field.getQualifier should be (null) - } - it("constructs a field w/ parameter name") { - val field = Field.inArray("ArrayField", "table", List("z").asJava, ":a") - field.getName shouldEqual "ArrayField" - field.getComparison.getOp shouldEqual Op.IN_ARRAY - field.getComparison.getValue.getFirst shouldEqual "table" - field.getComparison.getValue.getSecond should be (List("z").asJava) - field.getParameterName shouldEqual ":a" - field.getQualifier should be (null) - } - } - - describe("exists") { - it("constructs a field") { - val field = Field.exists("Groovy") - field.getName shouldEqual "Groovy" - field.getComparison.getOp shouldEqual Op.EXISTS - field.getComparison.getValue shouldEqual "" - field.getParameterName should be (null) - field.getQualifier should be (null) - } - } - - describe("notExists") { - it("constructs a field") { - val field = Field.notExists("Groovy") - field.getName shouldEqual "Groovy" - field.getComparison.getOp shouldEqual Op.NOT_EXISTS - field.getComparison.getValue shouldEqual "" - field.getParameterName should be (null) - field.getQualifier should be (null) - } - } - - describe("named") { - it("named constructs a field") { - val field = Field.named("Tacos") - field.getName shouldEqual "Tacos" - field.getComparison.getOp shouldEqual Op.EQUAL - field.getComparison.getValue shouldEqual "" - field.getParameterName should be (null) - field.getQualifier should be (null) - } - } - - describe("static constructors") { - it("fail for invalid parameter name") { - a [DocumentException] should be thrownBy Field.equal("a", "b", "that ain't it, Jack...") - } - } - - describe("nameToPath") { - it("creates a simple PostgreSQL SQL name") { - Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual "data->>'Simple'" - } - it("creates a simple SQLite SQL name") { - Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL) shouldEqual "data->>'Simple'" - } - it("creates a nested PostgreSQL SQL name") { - Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL) shouldEqual - "data#>>'{A,Long,Path,to,the,Property}'" - } - it("creates a nested SQLite SQL name") { - Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL) shouldEqual - "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'" - } - it("creates a simple PostgreSQL JSON name") { - Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON) shouldEqual "data->'Simple'" - } - it("creates a simple SQLite JSON name") { - Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON) shouldEqual "data->'Simple'" - } - it("creates a nested PostgreSQL JSON name") { - Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON) shouldEqual - "data#>'{A,Long,Path,to,the,Property}'" - } - it("creates a nested SQLite JSON name") { - Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON) shouldEqual - "data->'A'->'Long'->'Path'->'to'->'the'->'Property'" - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldTest.scala new file mode 100644 index 0000000..3923561 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldTest.scala @@ -0,0 +1,539 @@ +package solutions.bitbadger.documents.scala + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.{Dialect, DocumentException, Field, FieldFormat, Op} + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala deleted file mode 100644 index aea074f..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpSpec.scala +++ /dev/null @@ -1,44 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.Op - -class OpSpec extends AnyFunSpec with Matchers { - - describe("sql") { - it("returns = for EQUAL") { - Op.EQUAL.getSql shouldEqual "=" - } - it("returns > for GREATER") { - Op.GREATER.getSql shouldEqual ">" - } - it("returns >= for GREATER_OR_EQUAL") { - Op.GREATER_OR_EQUAL.getSql shouldEqual ">=" - } - it("returns < for LESS") { - Op.LESS.getSql shouldEqual "<" - } - it("returns <= for LESS_OR_EQUAL") { - Op.LESS_OR_EQUAL.getSql shouldEqual "<=" - } - it("returns <> for NOT_EQUAL") { - Op.NOT_EQUAL.getSql shouldEqual "<>" - } - it("returns BETWEEN for BETWEEN") { - Op.BETWEEN.getSql shouldEqual "BETWEEN" - } - it("returns IN for IN") { - Op.IN.getSql shouldEqual "IN" - } - it("returns ??| for IN_ARRAY") { - Op.IN_ARRAY.getSql shouldEqual "??|" - } - it("returns IS NOT NULL for EXISTS") { - Op.EXISTS.getSql shouldEqual "IS NOT NULL" - } - it("returns IS NULL for NOT_EXISTS") { - Op.NOT_EXISTS.getSql shouldEqual "IS NULL" - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpTest.scala new file mode 100644 index 0000000..62cb683 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpTest.scala @@ -0,0 +1,64 @@ +package solutions.bitbadger.documents.scala + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.Op + +@DisplayName("JVM | Kotlin | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala deleted file mode 100644 index 35188e8..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameSpec.scala +++ /dev/null @@ -1,23 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.ParameterName - -class ParameterNameSpec extends AnyFunSpec with Matchers { - - describe("derive") { - it("works when given existing names") { - val names = new ParameterName() - names.derive(":taco") shouldEqual ":taco" - names.derive(null) shouldEqual ":field0" - } - it("works when given all anonymous fields") { - val names = new ParameterName() - names.derive(null) shouldEqual ":field0" - names.derive(null) shouldEqual ":field1" - names.derive(null) shouldEqual ":field2" - names.derive(null) shouldEqual ":field3" - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameTest.scala new file mode 100644 index 0000000..d58c4fa --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameTest.scala @@ -0,0 +1,25 @@ +package solutions.bitbadger.documents.scala + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.ParameterName + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala deleted file mode 100644 index 3e393b7..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterSpec.scala +++ /dev/null @@ -1,26 +0,0 @@ -package solutions.bitbadger.documents.scala - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{DocumentException, Parameter, ParameterType} - -class ParameterSpec extends AnyFunSpec with Matchers { - - describe("constructor") { - it("succeeds with colon-prefixed name") { - val p = new Parameter(":test", ParameterType.STRING, "ABC") - p.getName shouldEqual ":test" - p.getType shouldEqual ParameterType.STRING - p.getValue shouldEqual "ABC" - } - it("succeeds with at-sign-prefixed name") { - val p = Parameter("@yo", ParameterType.NUMBER, null) - p.getName shouldEqual "@yo" - p.getType shouldEqual ParameterType.NUMBER - p.getValue should be (null) - } - it("fails with incorrect prefix") { - a [DocumentException] should be thrownBy Parameter("it", ParameterType.JSON, "") - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterTest.scala new file mode 100644 index 0000000..0c3c689 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterTest.scala @@ -0,0 +1,30 @@ +package solutions.bitbadger.documents.scala + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.{DocumentException, Parameter, ParameterType} + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala deleted file mode 100644 index 6a24e4c..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQuerySpec.scala +++ /dev/null @@ -1,56 +0,0 @@ -package solutions.bitbadger.documents.scala.query - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{DocumentException, Field} -import solutions.bitbadger.documents.query.CountQuery -import solutions.bitbadger.documents.scala.ClearConfiguration -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -import scala.jdk.CollectionConverters.* - -class CountQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { - - describe("all") { - it("generates correctly") { - CountQuery.all(TEST_TABLE) shouldEqual s"SELECT COUNT(*) AS it FROM $TEST_TABLE" - } - } - - describe("byFields") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - CountQuery.byFields(TEST_TABLE, List(Field.equal("test", "", ":field0")).asJava) shouldEqual - s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - CountQuery.byFields(TEST_TABLE, List(Field.equal("test", "", ":field0")).asJava) shouldEqual - s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0" - } - } - - describe("byContains") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - CountQuery.byContains(TEST_TABLE) shouldEqual s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy CountQuery.byContains(TEST_TABLE) - } - } - - describe("byJsonPath") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - CountQuery.byJsonPath(TEST_TABLE) shouldEqual - s"SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy CountQuery.byJsonPath(TEST_TABLE) - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQueryTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQueryTest.scala new file mode 100644 index 0000000..1b601b0 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQueryTest.scala @@ -0,0 +1,69 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala deleted file mode 100644 index dbf6aad..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQuerySpec.scala +++ /dev/null @@ -1,81 +0,0 @@ -package solutions.bitbadger.documents.scala.query - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{Dialect, DocumentException, DocumentIndex} -import solutions.bitbadger.documents.query.DefinitionQuery -import solutions.bitbadger.documents.scala.ClearConfiguration -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -import scala.jdk.CollectionConverters.* - -class DefinitionQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { - - describe("ensureTableFor") { - it("generates correctly") { - DefinitionQuery.ensureTableFor("my.table", "JSONB") shouldEqual - "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)" - } - } - - describe("ensureTable") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - DefinitionQuery.ensureTable(TEST_TABLE) shouldEqual - s"CREATE TABLE IF NOT EXISTS $TEST_TABLE (data JSONB NOT NULL)" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - DefinitionQuery.ensureTable(TEST_TABLE) shouldEqual - s"CREATE TABLE IF NOT EXISTS $TEST_TABLE (data TEXT NOT NULL)" - } - it("fails when no dialect is set") { - a [DocumentException] should be thrownBy DefinitionQuery.ensureTable(TEST_TABLE) - } - } - - describe("ensureKey") { - it("generates correctly with schema") { - DefinitionQuery.ensureKey("test.table", Dialect.POSTGRESQL) shouldEqual - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))" - } - it("generates correctly without schema") { - DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE) shouldEqual - s"CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))" - } - } - - describe("ensureIndexOn") { - it("generates multiple fields and directions") { - DefinitionQuery.ensureIndexOn("test.table", "gibberish", List("taco", "guac DESC", "salsa ASC").asJava, - Dialect.POSTGRESQL) shouldEqual - "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" - } - it("generates nested field | PostgreSQL") { - DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List("a.b.c").asJava, Dialect.POSTGRESQL) shouldEqual - s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data#>>'{a,b,c}'))" - } - it("generates nested field | SQLite") { - DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List("a.b.c").asJava, Dialect.SQLITE) shouldEqual - s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data->'a'->'b'->>'c'))" - } - } - - describe("ensureDocumentOn") { - it("generates Full | PostgreSQL") { - ForceDialect.postgres() - DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) shouldEqual - s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)" - } - it("generates Optimized | PostgreSQL") { - ForceDialect.postgres() - DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED) shouldEqual - s"CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data jsonb_path_ops)" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQueryTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQueryTest.scala new file mode 100644 index 0000000..d963970 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQueryTest.scala @@ -0,0 +1,107 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{Dialect, DocumentException, DocumentIndex} +import solutions.bitbadger.documents.query.DefinitionQuery +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala deleted file mode 100644 index 728d1cd..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQuerySpec.scala +++ /dev/null @@ -1,61 +0,0 @@ -package solutions.bitbadger.documents.scala.query - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{DocumentException, Field} -import solutions.bitbadger.documents.query.DeleteQuery -import solutions.bitbadger.documents.scala.ClearConfiguration -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -import scala.jdk.CollectionConverters.* - -class DeleteQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { - - describe("byId") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - DeleteQuery.byId(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - DeleteQuery.byId(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id" - } - } - - describe("byFields") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - DeleteQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b")).asJava) shouldEqual - s"DELETE FROM $TEST_TABLE WHERE data->>'a' = :b" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - DeleteQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b")).asJava) shouldEqual - s"DELETE FROM $TEST_TABLE WHERE data->>'a' = :b" - } - } - - describe("byContains") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - DeleteQuery.byContains(TEST_TABLE) shouldEqual s"DELETE FROM $TEST_TABLE WHERE data @> :criteria" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy DeleteQuery.byContains(TEST_TABLE) - } - } - - describe("byJsonPath") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - DeleteQuery.byJsonPath(TEST_TABLE) shouldEqual - s"DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy DeleteQuery.byJsonPath(TEST_TABLE) - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQueryTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQueryTest.scala new file mode 100644 index 0000000..1106867 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQueryTest.scala @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala deleted file mode 100644 index adbef90..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQuerySpec.scala +++ /dev/null @@ -1,85 +0,0 @@ -package solutions.bitbadger.documents.scala.query - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{AutoId, Configuration, DocumentException} -import solutions.bitbadger.documents.query.DocumentQuery -import solutions.bitbadger.documents.scala.ClearConfiguration -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -class DocumentQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { - - describe("insert") { - it("generates no auto ID | PostgreSQL") { - ForceDialect.postgres() - DocumentQuery.insert(TEST_TABLE) shouldEqual s"INSERT INTO $TEST_TABLE VALUES (:data)" - } - it("generates no auto ID | SQLite") { - ForceDialect.sqlite() - DocumentQuery.insert(TEST_TABLE) shouldEqual s"INSERT INTO $TEST_TABLE VALUES (:data)" - } - it("generates auto number | PostgreSQL") { - ForceDialect.postgres() - DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER) shouldEqual - s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || ('{\"id\":' " + - s"|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM $TEST_TABLE) || '}')::jsonb)" - } - it("generates auto number | SQLite") { - ForceDialect.sqlite() - DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER) shouldEqual - s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', " + - s"(SELECT coalesce(max(data->>'id'), 0) + 1 FROM $TEST_TABLE)))" - } - it("generates auto UUID | PostgreSQL") { - ForceDialect.postgres() - val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) - query should startWith (s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"") - query should endWith ("\"}')") - } - it("generates auto UUID | SQLite") { - ForceDialect.sqlite() - val query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) - query should startWith (s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '") - query should endWith ("'))") - } - it("generates auto random string | PostgreSQL") { - try { - ForceDialect.postgres() - Configuration.idStringLength = 8 - val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) - query should startWith (s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"") - query should endWith ("\"}')") - query.replace(s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", "").replace("\"}')", "") - .length shouldEqual 8 - } finally { - Configuration.idStringLength = 16 - } - } - it("insert generates auto random string | SQLite") { - ForceDialect.sqlite() - val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) - query should startWith (s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '") - query should endWith ("'))") - query.replace(s"INSERT INTO $TEST_TABLE VALUES (json_set(:data, '$$.id', '", "").replace("'))", "") - .length shouldEqual Configuration.idStringLength - } - it("fails when no dialect is set") { - a [DocumentException] should be thrownBy DocumentQuery.insert(TEST_TABLE) - } - } - - describe("save") { - it("generates correctly") { - ForceDialect.postgres() - DocumentQuery.save(TEST_TABLE) shouldEqual - s"INSERT INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data" - } - } - - describe("update") { - it("generates successfully") { - DocumentQuery.update(TEST_TABLE) shouldEqual s"UPDATE $TEST_TABLE SET data = :data" - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQueryTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQueryTest.scala new file mode 100644 index 0000000..2410773 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQueryTest.scala @@ -0,0 +1,113 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions._ +import solutions.bitbadger.documents.{AutoId, Configuration, DocumentException} +import solutions.bitbadger.documents.query.DocumentQuery +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala deleted file mode 100644 index d539543..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQuerySpec.scala +++ /dev/null @@ -1,64 +0,0 @@ -package solutions.bitbadger.documents.scala.query - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{DocumentException, Field} -import solutions.bitbadger.documents.query.ExistsQuery -import solutions.bitbadger.documents.scala.ClearConfiguration -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -import scala.jdk.CollectionConverters.* - -class ExistsQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { - - describe("byId") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - ExistsQuery.byId(TEST_TABLE) shouldEqual - s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - ExistsQuery.byId(TEST_TABLE) shouldEqual - s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'id' = :id) AS it" - } - } - - describe("byFields") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - ExistsQuery.byFields(TEST_TABLE, List(Field.equal("it", 7, ":test")).asJava) shouldEqual - s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE (data->>'it')::numeric = :test) AS it" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - ExistsQuery.byFields(TEST_TABLE, List(Field.equal("it", 7, ":test")).asJava) shouldEqual - s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data->>'it' = :test) AS it" - } - } - - describe("byContains") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - ExistsQuery.byContains(TEST_TABLE) shouldEqual - s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE data @> :criteria) AS it" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy ExistsQuery.byContains(TEST_TABLE) - } - } - - describe("byJsonPath") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - ExistsQuery.byJsonPath(TEST_TABLE) shouldEqual - s"SELECT EXISTS (SELECT 1 FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)) AS it" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy ExistsQuery.byJsonPath(TEST_TABLE) - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQueryTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQueryTest.scala new file mode 100644 index 0000000..3ab3992 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQueryTest.scala @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions._ +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.ExistsQuery +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala deleted file mode 100644 index 5d8a585..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQuerySpec.scala +++ /dev/null @@ -1,67 +0,0 @@ -package solutions.bitbadger.documents.scala.query - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{DocumentException, Field} -import solutions.bitbadger.documents.query.FindQuery -import solutions.bitbadger.documents.scala.ClearConfiguration -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -import scala.jdk.CollectionConverters.* - -class FindQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { - - describe("all") { - it("generates correctly") { - FindQuery.all(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE" - } - } - - describe("byId") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - FindQuery.byId(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - FindQuery.byId(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id" - } - } - - describe("byFields") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - FindQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b"), Field.less("c", 14, ":d")).asJava) shouldEqual - s"SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND (data->>'c')::numeric < :d" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - FindQuery.byFields(TEST_TABLE, List(Field.equal("a", "", ":b"), Field.less("c", 14, ":d")).asJava) shouldEqual - s"SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND data->>'c' < :d" - } - } - - describe("byContains") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - FindQuery.byContains(TEST_TABLE) shouldEqual s"SELECT data FROM $TEST_TABLE WHERE data @> :criteria" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy FindQuery.byContains(TEST_TABLE) - } - } - - describe("byJsonPath") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - FindQuery.byJsonPath(TEST_TABLE) shouldEqual - s"SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy FindQuery.byJsonPath(TEST_TABLE) - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQueryTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQueryTest.scala new file mode 100644 index 0000000..06e1586 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQueryTest.scala @@ -0,0 +1,82 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala deleted file mode 100644 index a37a8b3..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQuerySpec.scala +++ /dev/null @@ -1,63 +0,0 @@ -package solutions.bitbadger.documents.scala.query - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{DocumentException, Field} -import solutions.bitbadger.documents.query.PatchQuery -import solutions.bitbadger.documents.scala.ClearConfiguration -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -import scala.jdk.CollectionConverters.* - -class PatchQuerySpec extends AnyFunSpec with ClearConfiguration with Matchers { - - describe("byId") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - PatchQuery.byId(TEST_TABLE) shouldEqual s"UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - PatchQuery.byId(TEST_TABLE) shouldEqual - s"UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id" - } - } - - describe("byFields") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - PatchQuery.byFields(TEST_TABLE, List(Field.equal("z", "", ":y")).asJava) shouldEqual - s"UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y" - } - it("generates correctly | SQLite") { - ForceDialect.sqlite() - PatchQuery.byFields(TEST_TABLE, List(Field.equal("z", "", ":y")).asJava) shouldEqual - s"UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y" - } - } - - describe("byContains") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - PatchQuery.byContains(TEST_TABLE) shouldEqual - s"UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy PatchQuery.byContains(TEST_TABLE) - } - } - - describe("byJsonPath") { - it("generates correctly | PostgreSQL") { - ForceDialect.postgres() - PatchQuery.byJsonPath(TEST_TABLE) shouldEqual - s"UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)" - } - it("fails | SQLite") { - ForceDialect.sqlite() - a [DocumentException] should be thrownBy PatchQuery.byJsonPath(TEST_TABLE) - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQueryTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQueryTest.scala new file mode 100644 index 0000000..2741f2d --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQueryTest.scala @@ -0,0 +1,75 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions._ +import solutions.bitbadger.documents.{DocumentException, Field} +import solutions.bitbadger.documents.query.PatchQuery +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala deleted file mode 100644 index 0b58169..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsSpec.scala +++ /dev/null @@ -1,103 +0,0 @@ -package solutions.bitbadger.documents.scala.query - -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers -import solutions.bitbadger.documents.{Dialect, Field, FieldMatch} -import solutions.bitbadger.documents.query.QueryUtils -import solutions.bitbadger.documents.scala.ClearConfiguration -import solutions.bitbadger.documents.support.ForceDialect - -import scala.jdk.CollectionConverters.* - -class QueryUtilsSpec extends AnyFunSpec with ClearConfiguration with Matchers { - - describe("statementWhere") { - it("generates correctly") { - QueryUtils.statementWhere("x", "y") shouldEqual "x WHERE y" - } - } - - describe("byId") { - it("generates a numeric ID query | PostgreSQL") { - ForceDialect.postgres() - QueryUtils.byId("test", 9) shouldEqual "test WHERE (data->>'id')::numeric = :id" - } - it("generates an alphanumeric ID query | PostgreSQL") { - ForceDialect.postgres() - QueryUtils.byId("unit", "18") shouldEqual "unit WHERE data->>'id' = :id" - } - it("generates ID query | SQLite") { - ForceDialect.sqlite() - QueryUtils.byId("yo", 27) shouldEqual "yo WHERE data->>'id' = :id" - } - } - - describe("byFields") { - it("generates default field query | PostgreSQL") { - ForceDialect.postgres() - QueryUtils.byFields("this", - List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava) shouldEqual - "this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value" - } - it("generates default field query | SQLite") { - ForceDialect.sqlite() - QueryUtils.byFields("this", - List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava) shouldEqual - "this WHERE data->>'a' = :the_a AND data->>'b' = :b_value" - } - it("generates ANY field query | PostgreSQL") { - ForceDialect.postgres() - QueryUtils.byFields("that", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava, - FieldMatch.ANY) shouldEqual - "that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value" - } - it("generates ANY field query | SQLite") { - ForceDialect.sqlite() - QueryUtils.byFields("that", List(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")).asJava, - FieldMatch.ANY) shouldEqual - "that WHERE data->>'a' = :the_a OR data->>'b' = :b_value" - } - } - - describe("orderBy") { - it("generates for no fields") { - QueryUtils.orderBy(List().asJava, Dialect.POSTGRESQL) shouldEqual "" - QueryUtils.orderBy(List().asJava, Dialect.SQLITE) shouldEqual "" - } - it("generates single, no direction | PostgreSQL") { - QueryUtils.orderBy(List(Field.named("TestField")).asJava, Dialect.POSTGRESQL) shouldEqual - " ORDER BY data->>'TestField'" - } - it("generates single, no direction | SQLite") { - QueryUtils.orderBy(List(Field.named("TestField")).asJava, Dialect.SQLITE) shouldEqual - " ORDER BY data->>'TestField'" - } - it("generates multiple with direction | PostgreSQL") { - QueryUtils.orderBy( - List(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")).asJava, - Dialect.POSTGRESQL) shouldEqual - " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC" - } - it("generates multiple with direction | SQLite") { - QueryUtils.orderBy( - List(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")).asJava, - Dialect.SQLITE) shouldEqual - " ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC" - } - it("generates numeric ordering | PostgreSQL") { - QueryUtils.orderBy(List(Field.named("n:Test")).asJava, Dialect.POSTGRESQL) shouldEqual - " ORDER BY (data->>'Test')::numeric" - } - it("generates numeric ordering | SQLite") { - QueryUtils.orderBy(List(Field.named("n:Test")).asJava, Dialect.SQLITE) shouldEqual " ORDER BY data->>'Test'" - } - it("generates case-insensitive ordering | PostgreSQL") { - QueryUtils.orderBy(List(Field.named("i:Test.Field DESC NULLS FIRST")).asJava, Dialect.POSTGRESQL) shouldEqual - " ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST" - } - it("generates case-insensitive ordering | SQLite") { - QueryUtils.orderBy(List(Field.named("i:Test.Field ASC NULLS LAST")).asJava, Dialect.SQLITE) shouldEqual - " ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST" - } - } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsTest.scala new file mode 100644 index 0000000..3076708 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsTest.scala @@ -0,0 +1,138 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions._ +import solutions.bitbadger.documents.{Dialect, Field, FieldMatch} +import solutions.bitbadger.documents.query.QueryUtils +import solutions.bitbadger.documents.support.ForceDialect + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/RemoveFieldsQueryTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/RemoveFieldsQueryTest.scala new file mode 100644 index 0000000..e5e93e0 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/RemoveFieldsQueryTest.scala @@ -0,0 +1,85 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions._ +import solutions.bitbadger.documents.{DocumentException, Field, Parameter, ParameterType} +import solutions.bitbadger.documents.query.RemoveFieldsQuery +import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/WhereTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/WhereTest.scala new file mode 100644 index 0000000..26acf2c --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/WhereTest.scala @@ -0,0 +1,143 @@ +package solutions.bitbadger.documents.scala.query + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions._ +import solutions.bitbadger.documents.{DocumentException, Field, FieldMatch} +import solutions.bitbadger.documents.query.Where +import solutions.bitbadger.documents.support.ForceDialect + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/JsonDocument.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/JsonDocument.scala new file mode 100644 index 0000000..861eeab --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/JsonDocument.scala @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents.scala.support + +import solutions.bitbadger.documents.jvm.Document +import solutions.bitbadger.documents.support.ThrowawayDatabase +import solutions.bitbadger.documents.support.TypesKt.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 => Document.insert(tableName, it, db.getConn) } diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/SubDocument.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/SubDocument.scala new file mode 100644 index 0000000..9ec17d5 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/SubDocument.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.support + +class SubDocument(val foo: String = "", val bar: String = "") -- 2.47.2 From 0ca5e28c30436b9671f724379558c2f003e1f8e5 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 18 Mar 2025 17:22:35 -0400 Subject: [PATCH 56/88] Bikeshed POMs --- src/jvm/pom.xml | 6 +++--- src/pom.xml | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/jvm/pom.xml b/src/jvm/pom.xml index 343f18b..fda3830 100644 --- a/src/jvm/pom.xml +++ b/src/jvm/pom.xml @@ -39,19 +39,19 @@ org.scala-lang scala3-library_3 - 3.5.2 + ${scala.version} test org.codehaus.groovy groovy-test - 3.0.24 + ${groovy.version} test org.codehaus.groovy groovy-test-junit5 - 3.0.24 + ${groovy.version} test diff --git a/src/pom.xml b/src/pom.xml index 27748e6..0d195b4 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -42,6 +42,10 @@ ${java.version} 2.1.10 1.8.0 + 3.5.2 + 3.0.24 + 3.46.1.2 + 42.7.5 @@ -80,13 +84,13 @@ org.xerial sqlite-jdbc - 3.46.1.2 + ${sqlite.version} test org.postgresql postgresql - 42.7.5 + ${postgresql.version} test -- 2.47.2 From ecf71de1c4c5881a001e83366214f7fed25e5e94 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 18 Mar 2025 21:38:18 -0400 Subject: [PATCH 57/88] WIP on Scala/Groovy db tests; WIP on Groovy test upgrades --- src/jvm/pom.xml | 9 +- .../documents/groovy/AutoIdTest.groovy | 62 +-- .../documents/groovy/ConfigurationTest.groovy | 10 +- .../documents/groovy/DialectTest.groovy | 20 +- .../documents/groovy/DocumentIndexTest.groovy | 6 +- .../documents/groovy/FieldMatchTest.groovy | 6 +- .../documents/groovy/FieldTest.groovy | 467 +++++++++--------- .../bitbadger/documents/groovy/OpTest.groovy | 24 +- .../documents/groovy/ParameterNameTest.groovy | 16 +- .../documents/groovy/ParameterTest.groovy | 26 +- .../groovy/jvm/ParametersTest.groovy | 122 +++++ .../integration/common/CountFunctions.groovy | 53 ++ .../jvm/integration/postgresql/CountIT.groovy | 25 + .../groovy/support/JsonDocument.groovy | 33 ++ .../groovy/support/SubDocument.groovy | 11 + src/jvm/src/test/kotlin/jvm/ParametersTest.kt | 11 +- .../jvm/integration/postgresql/CountIT.kt | 2 +- .../kotlin/jvm/integration/sqlite/CountIT.kt | 2 +- .../documents/scala/jvm/ParametersTest.scala | 103 ++++ .../integration/common/CountFunctions.scala | 47 ++ .../jvm/integration/postgresql/CountIT.scala | 46 ++ .../jvm/integration/sqlite/CountIT.scala | 38 ++ src/pom.xml | 2 +- 23 files changed, 820 insertions(+), 321 deletions(-) create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/ParametersTest.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CountFunctions.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CountIT.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CountIT.scala diff --git a/src/jvm/pom.xml b/src/jvm/pom.xml index fda3830..287370a 100644 --- a/src/jvm/pom.xml +++ b/src/jvm/pom.xml @@ -43,13 +43,13 @@ test - org.codehaus.groovy + org.apache.groovy groovy-test ${groovy.version} test - org.codehaus.groovy + org.apache.groovy groovy-test-junit5 ${groovy.version} test @@ -131,7 +131,10 @@ maven-surefire-plugin 2.22.2 - --add-opens java.base/java.lang=ALL-UNNAMED + + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy index 6d7e0d9..5a3e99d 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy @@ -6,7 +6,7 @@ import solutions.bitbadger.documents.AutoId //import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.groovy.support.* -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `AutoId` enum @@ -17,21 +17,21 @@ class AutoIdTest { @Test @DisplayName('Generates a UUID string') void generateUUID() { - assertEquals('The UUID should have been a 32-character string', 32, AutoId.generateUUID().length()) + 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("There should have been 8 characters in $result", 8, result.length()) + 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("There should have been 11 characters in $result", 11, result.length()) + assertEquals 11, result.length(), "There should have been 11 characters in $result" } @Test @@ -39,7 +39,7 @@ class AutoIdTest { void generateRandomStringIsRandom() { def result1 = AutoId.generateRandomString 16 def result2 = AutoId.generateRandomString 16 - assertNotEquals('There should have been 2 different strings generated', result1, result2) + assertNotEquals result1, result2, 'There should have been 2 different strings generated' } // TODO: resolve java.base open issue @@ -59,63 +59,63 @@ class AutoIdTest { @Test @DisplayName('needsAutoId returns false if disabled') void needsAutoIdFalseIfDisabled() { - assertFalse('Disabled Auto ID should always return false', AutoId.needsAutoId(AutoId.DISABLED, '', '')) + 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('Number Auto ID with 0 should return true', - AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 0), 'id')) + 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('Number Auto ID with 77 should return false', - AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 77), 'id')) + 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('Number Auto ID with 0 should return true', - AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 0), 'id')) + 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('Number Auto ID with 31 should return false', - AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 31), 'id')) + 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('Number Auto ID with 0 should return true', - AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(0), 'id')) + 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('Number Auto ID with 6 should return false', - AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(6), 'id')) + 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('Number Auto ID with 0 should return true', - AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(0L), 'id')) + 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('Number Auto ID with 2 should return false', - AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(2L), 'id')) + assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(2L), 'id'), + 'Number Auto ID with 2 should return false') } // TODO: resolve java.base open issue @@ -128,15 +128,15 @@ class AutoIdTest { @Test @DisplayName('needsAutoId returns true for UUID strategy and blank ID') void needsAutoIdTrueForUUIDWithBlank() { - assertTrue('UUID Auto ID with blank should return true', - AutoId.needsAutoId(AutoId.UUID, new StringIdClass(''), 'id')) + 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('UUID Auto ID with non-blank should return false', - AutoId.needsAutoId(AutoId.UUID, new StringIdClass('howdy'), 'id')) + assertFalse(AutoId.needsAutoId(AutoId.UUID, new StringIdClass('howdy'), 'id'), + 'UUID Auto ID with non-blank should return false') } // TODO: resolve java.base open issue @@ -149,22 +149,22 @@ class AutoIdTest { @Test @DisplayName('needsAutoId returns true for Random String strategy and blank ID') void needsAutoIdTrueForRandomWithBlank() { - assertTrue('Random String Auto ID with blank should return true', - AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass(''), 'id')) + 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('Random String Auto ID with non-blank should return false', - AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass('full'), 'id')) + assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass('full'), 'id'), + 'Random String Auto ID with non-blank should return false') } // TODO: resolve java.base open issue // @Test // @DisplayName('needsAutoId fails for Random String strategy and non-string ID') // void needsAutoIdFailsForRandomNonString() { -// assertThrows(DocumentException.class) { +// assertThrows(DocumentException) { // AutoId.needsAutoId(AutoId.RANDOM_STRING, new ShortIdClass((short) 55), 'id') // } // } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy index d276c7a..9682c3f 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy @@ -7,7 +7,7 @@ import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect //import solutions.bitbadger.documents.DocumentException -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Configuration` object @@ -18,19 +18,19 @@ class ConfigurationTest { @Test @DisplayName('Default ID field is "id"') void defaultIdField() { - assertEquals('Default ID field incorrect', 'id', Configuration.idField) + assertEquals 'id', Configuration.idField, 'Default ID field incorrect' } @Test @DisplayName('Default Auto ID strategy is DISABLED') void defaultAutoId() { - assertEquals('Default Auto ID strategy should be DISABLED', AutoId.DISABLED, Configuration.autoIdStrategy) + assertEquals AutoId.DISABLED, Configuration.autoIdStrategy, 'Default Auto ID strategy should be DISABLED' } @Test @DisplayName('Default ID string length should be 16') void defaultIdStringLength() { - assertEquals('Default ID string length should be 16', 16, Configuration.idStringLength) + assertEquals 16, Configuration.idStringLength, 'Default ID string length should be 16' } @Test @@ -40,7 +40,7 @@ class ConfigurationTest { // TODO: uncomment once java.base open issue resolved //assertThrows(DocumentException) { Configuration.dialect() } Configuration.connectionString = 'jdbc:postgresql:db' - assertEquals(Dialect.POSTGRESQL, Configuration.dialect()) + assertEquals Dialect.POSTGRESQL, Configuration.dialect() } finally { Configuration.connectionString = null } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy index 5d95b99..2aae920 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Dialect` enum @@ -16,27 +16,27 @@ class DialectTest { @Test @DisplayName('deriveFromConnectionString derives PostgreSQL correctly') void derivesPostgres() { - assertEquals('Dialect should have been PostgreSQL', Dialect.POSTGRESQL, - Dialect.deriveFromConnectionString('jdbc:postgresql:db')) + assertEquals(Dialect.POSTGRESQL, Dialect.deriveFromConnectionString('jdbc:postgresql:db'), + 'Dialect should have been PostgreSQL') } @Test @DisplayName('deriveFromConnectionString derives SQLite correctly') void derivesSQLite() { - assertEquals('Dialect should have been SQLite', Dialect.SQLITE, - Dialect.deriveFromConnectionString('jdbc:sqlite:memory')) + 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') + Dialect.deriveFromConnectionString 'SQL Server' + fail 'Dialect derivation should have failed' } catch (DocumentException ex) { - assertNotNull 'The exception message should not have been null', ex.message - assertTrue('The connection string should have been in the exception message', - ex.message.contains('[SQL Server]')) + 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy index 03eb50d..a79aa5e 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy @@ -4,7 +4,7 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import solutions.bitbadger.documents.DocumentIndex -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.assertEquals /** * Unit tests for the `DocumentIndex` enum @@ -15,12 +15,12 @@ class DocumentIndexTest { @Test @DisplayName('FULL uses proper SQL') void fullSQL() { - assertEquals('The SQL for Full is incorrect', '', DocumentIndex.FULL.sql) + assertEquals '', DocumentIndex.FULL.sql, 'The SQL for Full is incorrect' } @Test @DisplayName('OPTIMIZED uses proper SQL') void optimizedSQL() { - assertEquals('The SQL for Optimized is incorrect', ' jsonb_path_ops', DocumentIndex.OPTIMIZED.sql) + assertEquals ' jsonb_path_ops', DocumentIndex.OPTIMIZED.sql, 'The SQL for Optimized is incorrect' } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy index 6474dbb..f1078e7 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy @@ -4,7 +4,7 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import solutions.bitbadger.documents.FieldMatch -import static groovy.test.GroovyAssert.assertEquals +import static org.junit.jupiter.api.Assertions.assertEquals /** * Unit tests for the `FieldMatch` enum @@ -15,12 +15,12 @@ class FieldMatchTest { @Test @DisplayName('ANY uses proper SQL') void anySQL() { - assertEquals('ANY should use OR', 'OR', FieldMatch.ANY.sql) + assertEquals 'OR', FieldMatch.ANY.sql, 'ANY should use OR' } @Test @DisplayName('ALL uses proper SQL') void allSQL() { - assertEquals('ALL should use AND', 'AND', FieldMatch.ALL.sql) + assertEquals 'AND', FieldMatch.ALL.sql, 'ALL should use AND' } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy index e03e0e9..25aa3ca 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy @@ -10,7 +10,7 @@ import solutions.bitbadger.documents.FieldFormat import solutions.bitbadger.documents.Op import solutions.bitbadger.documents.support.ForceDialect -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Field` class @@ -38,264 +38,275 @@ class FieldTest { @Test @DisplayName('withParameterName works with colon prefix') void withParamNameColon() { - def field = Field.equal('abc', '22').withQualifier('me') - def withParam = field.withParameterName(':test') - assertNotSame('A new Field instance should have been created', field, withParam) - assertEquals('Name should have been preserved', field.name, withParam.name) - assertEquals('Comparison should have been preserved', field.comparison, withParam.comparison) - assertEquals('Parameter name not set correctly', ':test', withParam.parameterName) - assertEquals('Qualifier should have been preserved', field.qualifier, withParam.qualifier) + 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('A new Field instance should have been created', field, withParam) - assertEquals('Name should have been preserved', field.name, withParam.name) - assertEquals('Comparison should have been preserved', field.comparison, withParam.comparison) - assertEquals('Parameter name not set correctly', '@unit', withParam.parameterName) - assertEquals('Qualifier should have been preserved', field.qualifier, withParam.qualifier) + 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('A new Field instance should have been created', field, withQual) - assertEquals('Name should have been preserved', field.name, withQual.name) - assertEquals('Comparison should have been preserved', field.comparison, withQual.comparison) - assertEquals('Parameter Name should have been preserved', field.parameterName, withQual.parameterName) - assertEquals('Qualifier not set correctly', 'test', withQual.qualifier) + 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('Path not correct', "data->>'SomethingCool'", - Field.greaterOrEqual('SomethingCool', 18).path(Dialect.POSTGRESQL, FieldFormat.SQL)) + 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('Path not correct', "this.data->>'SomethingElse'", - Field.less('SomethingElse', 9).withQualifier('this').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + 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('Path not correct', "data#>>'{My,Nested,Field}'", - Field.equal('My.Nested.Field', 'howdy').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + 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('Path not correct', "bird.data#>>'{Nest,Away}'", - Field.equal('Nest.Away', 'doc').withQualifier('bird').path(Dialect.POSTGRESQL, FieldFormat.SQL)) + 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('Path not correct', "data->>'SomethingCool'", - Field.greaterOrEqual('SomethingCool', 18).path(Dialect.SQLITE, FieldFormat.SQL)) + 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('Path not correct', "this.data->>'SomethingElse'", - Field.less('SomethingElse', 9).withQualifier('this').path(Dialect.SQLITE, FieldFormat.SQL)) + 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('Path not correct', "data->'My'->'Nested'->>'Field'", - Field.equal('My.Nested.Field', 'howdy').path(Dialect.SQLITE, FieldFormat.SQL)) + 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('Path not correct', "bird.data->'Nest'->>'Away'", - Field.equal('Nest.Away', 'doc').withQualifier('bird').path(Dialect.SQLITE, FieldFormat.SQL)) + 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('Field WHERE clause not generated correctly', "data->>'that_field' IS NOT NULL", - Field.exists('that_field').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'that_field' IS NOT NULL", - Field.exists('that_field').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'a_field' IS NULL", - Field.notExists('a_field').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'a_field' IS NULL", - Field.notExists('a_field').toWhere()) + 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('Field WHERE clause not generated correctly', - "(data->>'age')::numeric BETWEEN @agemin AND @agemax", Field.between('age', 13, 17, '@age').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'city' BETWEEN :citymin AND :citymax", - Field.between('city', 'Atlanta', 'Chicago', ':city').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'age' BETWEEN @agemin AND @agemax", - Field.between('age', 13, 17, '@age').toWhere()) + 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('Field WHERE clause not generated correctly', - "(test.data->>'age')::numeric BETWEEN @agemin AND @agemax", - Field.between('age', 13, 17, '@age').withQualifier('test').toWhere()) + 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('Field WHERE clause not generated correctly', "unit.data->>'city' BETWEEN :citymin AND :citymax", - Field.between('city', 'Atlanta', 'Chicago', ':city').withQualifier('unit').toWhere()) + 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('Field WHERE clause not generated correctly', "my.data->>'age' BETWEEN @agemin AND @agemax", - Field.between('age', 13, 17, '@age').withQualifier('my').toWhere()) + 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('Field WHERE clause not generated correctly', - "(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)", - Field.any('even', List.of(2, 4, 6), ':nbr').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'test' IN (:city_0, :city_1)", - Field.any('test', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'test' IN (:city_0, :city_1)", - Field.any('test', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + 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('Field WHERE clause not generated correctly', "data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]", - Field.inArray('even', 'tbl', List.of(2, 4, 6, 8), ':it').toWhere()) + 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('Field WHERE clause not generated correctly', - "EXISTS (SELECT 1 FROM json_each(tbl.data, '\$.test') WHERE value IN (:city_0, :city_1))", - Field.inArray('test', 'tbl', List.of('Atlanta', 'Chicago'), ':city').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'some_field' = :value", - Field.equal('some_field', '', ':value').toWhere()) + 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('Field WHERE clause not generated correctly', "data->>'some_field' = :value", - Field.equal('some_field', '', ':value').toWhere()) + 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('Field WHERE clause not generated correctly', "test.data->>'no_field' IS NOT NULL", - Field.exists('no_field').withQualifier('test').toWhere()) + 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('Field WHERE clause not generated correctly', "test.data->>'no_field' IS NOT NULL", - Field.exists('no_field').withQualifier('test').toWhere()) + 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('Field WHERE clause not generated correctly', "(q.data->>'le_field')::numeric <= :it", - Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere()) + 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('Field WHERE clause not generated correctly', "q.data->>'le_field' <= :it", - Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere()) + assertEquals("q.data->>'le_field' <= :it", + Field.lessOrEqual('le_field', 18, ':it').withQualifier('q').toWhere(), + 'Field WHERE clause not generated correctly') } // ~~~ STATIC CONSTRUCTOR TESTS ~~~ @@ -303,236 +314,236 @@ class FieldTest { @Test @DisplayName('equal constructs a field w/o parameter name') void equalCtor() { - def field = Field.equal('Test', 14) - assertEquals('Field name not filled correctly', 'Test', field.name) - assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 14, field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Test', field.name) - assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 14, field.comparison.value) - assertEquals('Field parameter name not filled correctly', ':w', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Great', field.name) - assertEquals('Field comparison operation not filled correctly', Op.GREATER, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 'night', field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Great', field.name) - assertEquals('Field comparison operation not filled correctly', Op.GREATER, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 'night', field.comparison.value) + 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('Field parameter name not filled correctly', ':yeah', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Nice', field.name) - assertEquals('Field comparison operation not filled correctly', Op.GREATER_OR_EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 88L, field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Nice', field.name) - assertEquals('Field comparison operation not filled correctly', Op.GREATER_OR_EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 88L, field.comparison.value) - assertEquals('Field parameter name not filled correctly', ':nice', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Lesser', field.name) - assertEquals('Field comparison operation not filled correctly', Op.LESS, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 'seven', field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Lesser', field.name) - assertEquals('Field comparison operation not filled correctly', Op.LESS, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 'seven', field.comparison.value) - assertEquals('Field parameter name not filled correctly', ':max', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Nobody', field.name) - assertEquals('Field comparison operation not filled correctly', Op.LESS_OR_EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 'KNOWS', field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Nobody', field.name) - assertEquals('Field comparison operation not filled correctly', Op.LESS_OR_EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 'KNOWS', field.comparison.value) - assertEquals('Field parameter name not filled correctly', ':nope', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Park', field.name) - assertEquals('Field comparison operation not filled correctly', Op.NOT_EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 'here', field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Park', field.name) - assertEquals('Field comparison operation not filled correctly', Op.NOT_EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', 'here', field.comparison.value) - assertEquals('Field parameter name not filled correctly', ':now', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Age', field.name) - assertEquals('Field comparison operation not filled correctly', Op.BETWEEN, field.comparison.op) - assertEquals('Field comparison min value not filled correctly', 18, field.comparison.value.first) - assertEquals('Field comparison max value not filled correctly', 49, field.comparison.value.second, ) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Age', field.name) - assertEquals('Field comparison operation not filled correctly', Op.BETWEEN, field.comparison.op) - assertEquals('Field comparison min value not filled correctly', 18, field.comparison.value.first) - assertEquals('Field comparison max value not filled correctly', 49, field.comparison.value.second) - assertEquals('Field parameter name not filled correctly', ':limit', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Here', field.name) - assertEquals('Field comparison operation not filled correctly', Op.IN, field.comparison.op) - assertEquals('Field comparison value not filled correctly', List.of(8, 16, 32), field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Here', field.name) - assertEquals('Field comparison operation not filled correctly', Op.IN, field.comparison.op) - assertEquals('Field comparison value not filled correctly', List.of(8, 16, 32), field.comparison.value) - assertEquals('Field parameter name not filled correctly', ':list', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'ArrayField', field.name) - assertEquals('Field comparison operation not filled correctly', Op.IN_ARRAY, field.comparison.op) - assertEquals('Field comparison table not filled correctly', 'table', field.comparison.value.first) - assertEquals('Field comparison values not filled correctly', List.of('z'), field.comparison.value.second) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'ArrayField', field.name) - assertEquals('Field comparison operation not filled correctly', Op.IN_ARRAY, field.comparison.op) - assertEquals('Field comparison table not filled correctly', 'table', field.comparison.value.first) - assertEquals('Field comparison values not filled correctly', List.of('z'), field.comparison.value.second) - assertEquals('Field parameter name not filled correctly', ':a', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Groovy', field.name) - assertEquals('Field comparison operation not filled correctly', Op.EXISTS, field.comparison.op) - assertEquals('Field comparison value not filled correctly', '', field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Groovy', field.name) - assertEquals('Field comparison operation not filled correctly', Op.NOT_EXISTS, field.comparison.op) - assertEquals('Field comparison value not filled correctly', '', field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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('Field name not filled correctly', 'Tacos', field.name) - assertEquals('Field comparison operation not filled correctly', Op.EQUAL, field.comparison.op) - assertEquals('Field comparison value not filled correctly', '', field.comparison.value) - assertNull('The parameter name should have been null', field.parameterName) - assertNull('The qualifier should have been null', field.qualifier) + 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' } // TODO: fix java.base open issue @@ -545,56 +556,60 @@ class FieldTest { @Test @DisplayName('nameToPath creates a simple PostgreSQL SQL name') void nameToPathPostgresSimpleSQL() { - assertEquals('Path not constructed correctly', "data->>'Simple'", - Field.nameToPath('Simple', Dialect.POSTGRESQL, FieldFormat.SQL)) + 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('Path not constructed correctly', "data->>'Simple'", - Field.nameToPath('Simple', Dialect.SQLITE, FieldFormat.SQL)) + 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('Path not constructed correctly', "data#>>'{A,Long,Path,to,the,Property}'", - Field.nameToPath('A.Long.Path.to.the.Property', Dialect.POSTGRESQL, FieldFormat.SQL)) + 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('Path not constructed correctly', "data->'A'->'Long'->'Path'->'to'->'the'->>'Property'", - Field.nameToPath('A.Long.Path.to.the.Property', Dialect.SQLITE, FieldFormat.SQL)) + 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('Path not constructed correctly', "data->'Simple'", - Field.nameToPath('Simple', Dialect.POSTGRESQL, FieldFormat.JSON)) + 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('Path not constructed correctly', "data->'Simple'", - Field.nameToPath('Simple', Dialect.SQLITE, FieldFormat.JSON)) + 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('Path not constructed correctly', "data#>'{A,Long,Path,to,the,Property}'", - Field.nameToPath('A.Long.Path.to.the.Property', Dialect.POSTGRESQL, FieldFormat.JSON)) + 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('Path not constructed correctly', "data->'A'->'Long'->'Path'->'to'->'the'->'Property'", - Field.nameToPath('A.Long.Path.to.the.Property', Dialect.SQLITE, FieldFormat.JSON)) + 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy index 47118f0..f4bd6e3 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy @@ -4,7 +4,7 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import solutions.bitbadger.documents.Op -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.assertEquals /** * Unit tests for the `Op` enum @@ -15,66 +15,66 @@ class OpTest { @Test @DisplayName('EQUAL uses proper SQL') void equalSQL() { - assertEquals('The SQL for equal is incorrect', '=', Op.EQUAL.sql) + assertEquals '=', Op.EQUAL.sql, 'The SQL for equal is incorrect' } @Test @DisplayName('GREATER uses proper SQL') void greaterSQL() { - assertEquals('The SQL for greater is incorrect', '>', Op.GREATER.sql) + assertEquals '>', Op.GREATER.sql, 'The SQL for greater is incorrect' } @Test @DisplayName('GREATER_OR_EQUAL uses proper SQL') void greaterOrEqualSQL() { - assertEquals('The SQL for greater-or-equal is incorrect', '>=', Op.GREATER_OR_EQUAL.sql) + assertEquals '>=', Op.GREATER_OR_EQUAL.sql, 'The SQL for greater-or-equal is incorrect' } @Test @DisplayName('LESS uses proper SQL') void lessSQL() { - assertEquals('The SQL for less is incorrect', '<', Op.LESS.sql) + assertEquals '<', Op.LESS.sql, 'The SQL for less is incorrect' } @Test @DisplayName('LESS_OR_EQUAL uses proper SQL') void lessOrEqualSQL() { - assertEquals('The SQL for less-or-equal is incorrect', '<=', Op.LESS_OR_EQUAL.sql) + assertEquals '<=', Op.LESS_OR_EQUAL.sql, 'The SQL for less-or-equal is incorrect' } @Test @DisplayName('NOT_EQUAL uses proper SQL') void notEqualSQL() { - assertEquals('The SQL for not-equal is incorrect', '<>', Op.NOT_EQUAL.sql) + assertEquals '<>', Op.NOT_EQUAL.sql, 'The SQL for not-equal is incorrect' } @Test @DisplayName('BETWEEN uses proper SQL') void betweenSQL() { - assertEquals('The SQL for between is incorrect', 'BETWEEN', Op.BETWEEN.sql) + assertEquals 'BETWEEN', Op.BETWEEN.sql, 'The SQL for between is incorrect' } @Test @DisplayName('IN uses proper SQL') void inSQL() { - assertEquals('The SQL for in is incorrect', 'IN', Op.IN.sql) + assertEquals 'IN', Op.IN.sql, 'The SQL for in is incorrect' } @Test @DisplayName('IN_ARRAY uses proper SQL') void inArraySQL() { - assertEquals('The SQL for in-array is incorrect', '??|', Op.IN_ARRAY.sql) + assertEquals '??|', Op.IN_ARRAY.sql, 'The SQL for in-array is incorrect' } @Test @DisplayName('EXISTS uses proper SQL') void existsSQL() { - assertEquals('The SQL for exists is incorrect', 'IS NOT NULL', Op.EXISTS.sql) + assertEquals 'IS NOT NULL', Op.EXISTS.sql, 'The SQL for exists is incorrect' } @Test @DisplayName('NOT_EXISTS uses proper SQL') void notExistsSQL() { - assertEquals('The SQL for not-exists is incorrect', 'IS NULL', Op.NOT_EXISTS.sql) + assertEquals 'IS NULL', Op.NOT_EXISTS.sql, 'The SQL for not-exists is incorrect' } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy index 8beb54b..0acc5d0 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy @@ -3,7 +3,9 @@ package solutions.bitbadger.documents.groovy import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import solutions.bitbadger.documents.ParameterName -import static groovy.test.GroovyAssert.assertEquals + +import static org.junit.jupiter.api.Assertions.assertEquals + /** * Unit tests for the `ParameterName` class */ @@ -14,17 +16,17 @@ class ParameterNameTest { @DisplayName('derive works when given existing names') void withExisting() { def names = new ParameterName() - assertEquals('Name should have been :taco', ':taco', names.derive(':taco')) - assertEquals('Counter should not have advanced for named field', ':field0', names.derive(null)) + 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('Anonymous field name should have been returned', ':field0', names.derive(null)) - assertEquals('Counter should have advanced from previous call', ':field1', names.derive(null)) - assertEquals('Counter should have advanced from previous call', ':field2', names.derive(null)) - assertEquals('Counter should have advanced from previous call', ':field3', names.derive(null)) + 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy index 1cd06ab..62320b7 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test import solutions.bitbadger.documents.Parameter import solutions.bitbadger.documents.ParameterType -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Parameter` class @@ -15,27 +15,27 @@ import static groovy.test.GroovyAssert.* class ParameterTest { @Test - @DisplayName("Construction with colon-prefixed name") + @DisplayName('Construction with colon-prefixed name') void ctorWithColon() { - def p = new Parameter(":test", ParameterType.STRING, "ABC") - assertEquals("Parameter name was incorrect", ":test", p.name) - assertEquals("Parameter type was incorrect", ParameterType.STRING, p.type) - assertEquals("Parameter value was incorrect", "ABC", p.value) + 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") + @DisplayName('Construction with at-sign-prefixed name') void ctorWithAtSign() { - def p = new Parameter("@yo", ParameterType.NUMBER, null) - assertEquals("Parameter name was incorrect", "@yo", p.name) - assertEquals("Parameter type was incorrect", ParameterType.NUMBER, p.type) - assertNull("Parameter value was incorrect", p.value) + 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' } // TODO: resolve java.base open issue // @Test -// @DisplayName("Construction fails with incorrect prefix") +// @DisplayName('Construction fails with incorrect prefix') // void ctorFailsForPrefix() { -// assertThrows(DocumentException) { new Parameter("it", ParameterType.JSON, "") } +// assertThrows(DocumentException) { new Parameter('it', ParameterType.JSON, '') } // } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy new file mode 100644 index 0000000..d5963fb --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy @@ -0,0 +1,122 @@ +package solutions.bitbadger.documents.groovy.jvm + +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.jvm.Parameters +import solutions.bitbadger.documents.support.ForceDialect + +import static groovy.test.GroovyAssert.* + +/** + * Unit tests for the `Parameters` object + */ +@DisplayName('JVM | 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('There should have been 3 fields in the list', fields.size(), named.size()) + assertSame('The first field should be the same', fields[0], named[0]) + assertSame('The second field should be the same', fields[1], named[1]) + assertSame('The third field should be the same', fields[2], named[2]) + } + + @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('There should have been 4 fields in the list', fields.size(), named.size()) + assertNotSame('The first field should not be the same', fields[0], named[0]) + assertEquals('First parameter name incorrect', ':field0', named[0].parameterName) + assertSame('The second field should be the same', fields[1], named[1]) + assertNotSame('The third field should not be the same', fields[2], named[2]) + assertEquals('Third parameter name incorrect', ':field1', named[2].parameterName) + assertSame('The fourth field should be the same', fields[3], named[3]) + } + + @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('Parameters not replaced correctly', + 'SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?', + Parameters.replaceNamesInQuery(query, parameters)) + } + + @Test + @DisplayName('fieldNames generates a single parameter (PostgreSQL)') + void fieldNamesSinglePostgres() { + ForceDialect.postgres() + def nameParams = Parameters.fieldNames(List.of('test')).toList() + assertEquals('There should be one name parameter', 1, nameParams.size()) + assertEquals('The parameter name is incorrect', ':name', nameParams[0].name) + assertEquals('The parameter type is incorrect', ParameterType.STRING, nameParams[0].type) + assertEquals('The parameter value is incorrect', '{test}', nameParams[0].value) + } + + @Test + @DisplayName('fieldNames generates multiple parameters (PostgreSQL)') + void fieldNamesMultiplePostgres() { + ForceDialect.postgres() + def nameParams = Parameters.fieldNames(List.of('test', 'this', 'today')).toList() + assertEquals('There should be one name parameter', 1, nameParams.size()) + assertEquals('The parameter name is incorrect', ':name', nameParams[0].name) + assertEquals('The parameter type is incorrect', ParameterType.STRING, nameParams[0].type) + assertEquals('The parameter value is incorrect', '{test,this,today}', nameParams[0].value) + } + + @Test + @DisplayName('fieldNames generates a single parameter (SQLite)') + void fieldNamesSingleSQLite() { + ForceDialect.sqlite() + def nameParams = Parameters.fieldNames(List.of('test')).toList() + assertEquals('There should be one name parameter', 1, nameParams.size()) + assertEquals('The parameter name is incorrect', ':name0', nameParams[0].name) + assertEquals('The parameter type is incorrect', ParameterType.STRING, nameParams[0].type) + assertEquals('The parameter value is incorrect', 'test', nameParams[0].value) + } + + @Test + @DisplayName('fieldNames generates multiple parameters (SQLite)') + void fieldNamesMultipleSQLite() { + ForceDialect.sqlite() + def nameParams = Parameters.fieldNames(List.of('test', 'this', 'today')).toList() + assertEquals('There should be one name parameter', 3, nameParams.size()) + assertEquals('The first parameter name is incorrect', ':name0', nameParams[0].name) + assertEquals('The first parameter type is incorrect', ParameterType.STRING, nameParams[0].type) + assertEquals('The first parameter value is incorrect', 'test', nameParams[0].value) + assertEquals('The second parameter name is incorrect', ':name1', nameParams[1].name) + assertEquals('The second parameter type is incorrect', ParameterType.STRING, nameParams[1].type) + assertEquals('The second parameter value is incorrect', 'this', nameParams[1].value) + assertEquals('The third parameter name is incorrect', ':name2', nameParams[2].name) + assertEquals('The third parameter type is incorrect', ParameterType.STRING, nameParams[2].type) + assertEquals('The third parameter value is incorrect', 'today', nameParams[2].value) + } + +// TODO: resolve java.base open issue +// @Test +// @DisplayName('fieldNames fails if dialect not set') +// void fieldNamesFails() { +// assertThrows(DocumentException) { Parameters.fieldNames(List.of()) } +// } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy new file mode 100644 index 0000000..30899bb --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy @@ -0,0 +1,53 @@ +package solutions.bitbadger.documents.groovy.jvm.integration.common + +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.groovy.support.JsonDocument +import solutions.bitbadger.documents.support.ThrowawayDatabase + +import static solutions.bitbadger.documents.extensions.ConnExt.* +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static groovy.test.GroovyAssert.* + +class CountFunctions { + + static void all(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals('There should have been 5 documents in the table', 5L, countAll(db.conn, TEST_TABLE)) + } + + static void byFieldsNumeric(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals('There should have been 3 matching documents', 3L, + countByFields(db.conn, TEST_TABLE, List.of(Field.between('numValue', 10, 20)))) + } + + static void byFieldsAlpha(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals('There should have been 1 matching document', 1L, + countByFields(db.conn, TEST_TABLE, List.of(Field.between('value', 'aardvark', 'apple')))) + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals('There should have been 2 matching documents', 2L, + countByContains(db.conn, TEST_TABLE, Map.of('value', 'purple'))) + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals('There should have been no matching documents', 0L, + countByContains(db.conn, TEST_TABLE, Map.of('value', 'magenta'))) + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals('There should have been 2 matching documents', 2L, + countByJsonPath(db.conn, TEST_TABLE, '$.numValue ? (@ < 5)')) + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals('There should have been no matching documents', 0L, + countByJsonPath(db.conn, TEST_TABLE, '$.numValue ? (@ > 100)')) + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy new file mode 100644 index 0000000..b252bbb --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy @@ -0,0 +1,25 @@ +package solutions.bitbadger.documents.groovy.jvm.integration.postgresql + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.groovy.jvm.integration.common.CountFunctions +import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB + +/** + * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("JVM | Groovy | PostgreSQL: Count") +class CountIT { + + @Test + @DisplayName("all counts all documents") + void all() { + PgDB db = new PgDB() + try { + CountFunctions.all(db) + } finally { + db.close() + } + } + +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy new file mode 100644 index 0000000..172daf1 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy @@ -0,0 +1,33 @@ +package solutions.bitbadger.documents.groovy.support + +import solutions.bitbadger.documents.jvm.Document +import solutions.bitbadger.documents.support.ThrowawayDatabase + +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +class JsonDocument { + String id + String value + int numValue + SubDocument sub + + JsonDocument(String id, 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) { + for (doc in testDocuments) { + Document.insert(tableName, doc, db.conn) + } + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy new file mode 100644 index 0000000..c2efe4d --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy @@ -0,0 +1,11 @@ +package solutions.bitbadger.documents.groovy.support + +class SubDocument { + String foo + String bar + + SubDocument(String foo, String bar) { + this.foo = foo + this.bar = bar + } +} diff --git a/src/jvm/src/test/kotlin/jvm/ParametersTest.kt b/src/jvm/src/test/kotlin/jvm/ParametersTest.kt index ec435f0..33a1af2 100644 --- a/src/jvm/src/test/kotlin/jvm/ParametersTest.kt +++ b/src/jvm/src/test/kotlin/jvm/ParametersTest.kt @@ -4,6 +4,7 @@ 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.support.ForceDialect import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotSame @@ -20,7 +21,7 @@ class ParametersTest { */ @AfterEach fun cleanUp() { - Configuration.connectionString = null + ForceDialect.none() } @Test @@ -65,7 +66,7 @@ class ParametersTest { @Test @DisplayName("fieldNames generates a single parameter (PostgreSQL)") fun fieldNamesSinglePostgres() { - Configuration.connectionString = ":postgresql:" + 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") @@ -76,7 +77,7 @@ class ParametersTest { @Test @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") fun fieldNamesMultiplePostgres() { - Configuration.connectionString = ":postgresql:" + 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") @@ -87,7 +88,7 @@ class ParametersTest { @Test @DisplayName("fieldNames generates a single parameter (SQLite)") fun fieldNamesSingleSQLite() { - Configuration.connectionString = ":sqlite:" + 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") @@ -98,7 +99,7 @@ class ParametersTest { @Test @DisplayName("fieldNames generates multiple parameters (SQLite)") fun fieldNamesMultipleSQLite() { - Configuration.connectionString = ":sqlite:" + 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") diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt index 21a50c1..a80c2a7 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Count") +@DisplayName("JVM | Kotlin | PostgreSQL: Count") class CountIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt index 5d2d269..566cb4b 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt @@ -9,7 +9,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Count") +@DisplayName("JVM | Kotlin | SQLite: Count") class CountIT { @Test diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/ParametersTest.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/ParametersTest.scala new file mode 100644 index 0000000..1420bdc --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/ParametersTest.scala @@ -0,0 +1,103 @@ +package solutions.bitbadger.documents.scala.jvm + +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{DocumentException, Field, Parameter, ParameterType} +import solutions.bitbadger.documents.jvm.Parameters +import solutions.bitbadger.documents.support.ForceDialect + +import scala.jdk.CollectionConverters.* + +@DisplayName("JVM | 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.asJava).asScala.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.asJava).asScala.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).asJava + 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).asJava).asScala.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).asJava).asScala.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).asJava).asScala.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).asJava).asScala.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().asJava)) +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CountFunctions.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CountFunctions.scala new file mode 100644 index 0000000..553a425 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CountFunctions.scala @@ -0,0 +1,47 @@ +package solutions.bitbadger.documents.scala.jvm.integration.common + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.extensions.ConnExt.* +import solutions.bitbadger.documents.scala.support.JsonDocument +import solutions.bitbadger.documents.support.ThrowawayDatabase +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.jdk.CollectionConverters.* + +object CountFunctions { + + def all(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, countAll(db.getConn, TEST_TABLE), "There should have been 5 documents in the table") + + def byFieldsNumeric(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(3L, countByFields(db.getConn, TEST_TABLE, (Field.between("numValue", 10, 20) :: Nil).asJava), + "There should have been 3 matching documents") + + def byFieldsAlpha(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(1L, countByFields(db.getConn, TEST_TABLE, (Field.between("value", "aardvark", "apple") :: Nil).asJava), + "There should have been 1 matching document") + + def byContainsMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(2L, countByContains(db.getConn, TEST_TABLE, Map.Map1("value", "purple")), + "There should have been 2 matching documents") + + def byContainsNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(0L, countByContains(db.getConn, TEST_TABLE, Map.Map1("value", "magenta")), + "There should have been no matching documents") + + def byJsonPathMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(2L, countByJsonPath(db.getConn, TEST_TABLE, "$.numValue ? (@ < 5)"), + "There should have been 2 matching documents") + + def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(0L, countByJsonPath(db.getConn, TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no matching documents") +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CountIT.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CountIT.scala new file mode 100644 index 0000000..1533bde --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CountIT.scala @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.scala.jvm.integration.postgresql + +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB +import solutions.bitbadger.documents.scala.jvm.integration.common.CountFunctions + +import scala.util.Using + +@DisplayName("JVM | Scala | PostgreSQL: Count") +class CountIT { + + @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/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CountIT.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CountIT.scala new file mode 100644 index 0000000..c77f453 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CountIT.scala @@ -0,0 +1,38 @@ +package solutions.bitbadger.documents.scala.jvm.integration.sqlite + +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB +import solutions.bitbadger.documents.scala.jvm.integration.common.CountFunctions +import org.junit.jupiter.api.Assertions.assertThrows +import solutions.bitbadger.documents.DocumentException + +import scala.util.Using + +@DisplayName("JVM | Scala | SQLite: Count") +class CountIT { + + @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/pom.xml b/src/pom.xml index 0d195b4..06c5830 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -43,7 +43,7 @@ 2.1.10 1.8.0 3.5.2 - 3.0.24 + 4.0.26 3.46.1.2 42.7.5 -- 2.47.2 From a2c9f96b32e614b34bd1403d5e17571a0cd6c046 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 18 Mar 2025 23:19:58 -0400 Subject: [PATCH 58/88] Resolved closure/java.base open issue --- .../documents/groovy/AutoIdTest.groovy | 61 ++++++------- .../documents/groovy/ConfigurationTest.groovy | 5 +- .../documents/groovy/FieldTest.groovy | 26 +++--- .../documents/groovy/ParameterTest.groovy | 13 ++- .../groovy/jvm/ParametersTest.groovy | 86 +++++++++---------- .../integration/common/CountFunctions.groovy | 28 +++--- .../jvm/integration/postgresql/CountIT.groovy | 42 +++++++-- .../jvm/integration/sqlite/CountIT.groovy | 50 +++++++++++ .../groovy/query/CountQueryTest.groovy | 56 ++++++------ .../groovy/query/DefinitionQueryTest.groovy | 74 ++++++++-------- .../groovy/query/DeleteQueryTest.groovy | 59 ++++++------- .../groovy/query/DocumentQueryTest.groovy | 65 +++++++------- .../groovy/query/FindQueryTest.groovy | 63 +++++++------- .../groovy/query/PatchQueryTest.groovy | 61 ++++++------- .../groovy/query/QueryUtilsTest.groovy | 58 +++++++------ .../groovy/query/RemoveFieldsQueryTest.groovy | 64 +++++++------- .../documents/groovy/query/WhereTest.groovy | 56 ++++++------ .../groovy/support/JsonDocument.groovy | 4 +- 18 files changed, 462 insertions(+), 409 deletions(-) create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CountIT.groovy diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy index 5a3e99d..1cecfa8 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.groovy import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import solutions.bitbadger.documents.AutoId -//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.groovy.support.* import static org.junit.jupiter.api.Assertions.* @@ -42,19 +42,17 @@ class AutoIdTest { assertNotEquals result1, result2, 'There should have been 2 different strings generated' } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('needsAutoId fails for null document') -// void needsAutoIdFailsForNullDocument() { -// assertThrows(DocumentException) { AutoId.needsAutoId(AutoId.DISABLED, null, 'id') } -// } -// -// TODO: resolve java.base open issue -// @Test -// @DisplayName('needsAutoId fails for missing ID property') -// void needsAutoIdFailsForMissingId() { -// assertThrows(DocumentException) { AutoId.needsAutoId(AutoId.UUID, new IntIdClass(0), 'Id') } -// } + @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') @@ -118,12 +116,11 @@ class AutoIdTest { 'Number Auto ID with 2 should return false') } -// TODO: resolve java.base open issue -// @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 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') @@ -139,12 +136,11 @@ class AutoIdTest { 'UUID Auto ID with non-blank should return false') } -// TODO: resolve java.base open issue -// @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 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') @@ -160,12 +156,9 @@ class AutoIdTest { 'Random String Auto ID with non-blank should return false') } -// TODO: resolve java.base open issue -// @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') -// } -// } + @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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy index 9682c3f..87d7383 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy @@ -5,7 +5,7 @@ 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 solutions.bitbadger.documents.DocumentException import static org.junit.jupiter.api.Assertions.* @@ -37,8 +37,7 @@ class ConfigurationTest { @DisplayName('Dialect is derived from connection string') void dialectIsDerived() { try { - // TODO: uncomment once java.base open issue resolved - //assertThrows(DocumentException) { Configuration.dialect() } + assertThrows(DocumentException) { Configuration.dialect() } Configuration.connectionString = 'jdbc:postgresql:db' assertEquals Dialect.POSTGRESQL, Configuration.dialect() } finally { diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy index 25aa3ca..da2214e 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy @@ -4,7 +4,7 @@ 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.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldFormat import solutions.bitbadger.documents.Op @@ -28,12 +28,11 @@ class FieldTest { // ~~~ INSTANCE METHODS ~~~ -// TODO: fix java.base open issue -// @Test -// @DisplayName('withParameterName fails for invalid name') -// void withParamNameFails() { -// assertThrows(DocumentException) { Field.equal('it', '').withParameterName('2424') } -// } + @Test + @DisplayName('withParameterName fails for invalid name') + void withParamNameFails() { + assertThrows(DocumentException) { Field.equal('it', '').withParameterName('2424') } + } @Test @DisplayName('withParameterName works with colon prefix') @@ -351,7 +350,7 @@ class FieldTest { 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('Field parameter name not filled correctly', ':yeah', field.parameterName) + assertEquals ':yeah', field.parameterName, 'Field parameter name not filled correctly' assertNull field.qualifier, 'The qualifier should have been null' } @@ -546,12 +545,11 @@ class FieldTest { assertNull field.qualifier, 'The qualifier should have been null' } -// TODO: fix java.base open issue -// @Test -// @DisplayName('static constructors fail for invalid parameter name') -// void staticCtorsFailOnParamName() { -// assertThrows(DocumentException) { Field.equal('a', 'b', "that ain't it, Jack...") } -// } + @Test + @DisplayName('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') diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy index 62320b7..76564b1 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.groovy import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Parameter import solutions.bitbadger.documents.ParameterType @@ -32,10 +32,9 @@ class ParameterTest { assertNull p.value, 'Parameter value was incorrect' } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('Construction fails with incorrect prefix') -// void ctorFailsForPrefix() { -// assertThrows(DocumentException) { new Parameter('it', ParameterType.JSON, '') } -// } + @Test + @DisplayName('Construction fails with incorrect prefix') + void ctorFailsForPrefix() { + assertThrows(DocumentException) { new Parameter('it', ParameterType.JSON, '') } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy index d5963fb..a8581a7 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy @@ -3,14 +3,14 @@ package solutions.bitbadger.documents.groovy.jvm 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.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.Parameter import solutions.bitbadger.documents.ParameterType import solutions.bitbadger.documents.jvm.Parameters import solutions.bitbadger.documents.support.ForceDialect -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Parameters` object @@ -31,10 +31,10 @@ class ParametersTest { void nameFieldsNoChange() { def fields = List.of(Field.equal('a', '', ':test'), Field.exists('q'), Field.equal('b', '', ':me')) def named = Parameters.nameFields(fields).toList() - assertEquals('There should have been 3 fields in the list', fields.size(), named.size()) - assertSame('The first field should be the same', fields[0], named[0]) - assertSame('The second field should be the same', fields[1], named[1]) - assertSame('The third field should be the same', fields[2], named[2]) + 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 @@ -43,13 +43,13 @@ class ParametersTest { def fields = List.of(Field.equal('a', ''), Field.equal('e', '', ':hi'), Field.equal('b', ''), Field.notExists('z')) def named = Parameters.nameFields(fields).toList() - assertEquals('There should have been 4 fields in the list', fields.size(), named.size()) - assertNotSame('The first field should not be the same', fields[0], named[0]) - assertEquals('First parameter name incorrect', ':field0', named[0].parameterName) - assertSame('The second field should be the same', fields[1], named[1]) - assertNotSame('The third field should not be the same', fields[2], named[2]) - assertEquals('Third parameter name incorrect', ':field1', named[2].parameterName) - assertSame('The fourth field should be the same', fields[3], named[3]) + 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 @@ -58,9 +58,8 @@ class ParametersTest { 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('Parameters not replaced correctly', - 'SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?', - Parameters.replaceNamesInQuery(query, parameters)) + assertEquals('SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?', + Parameters.replaceNamesInQuery(query, parameters), 'Parameters not replaced correctly') } @Test @@ -68,10 +67,10 @@ class ParametersTest { void fieldNamesSinglePostgres() { ForceDialect.postgres() def nameParams = Parameters.fieldNames(List.of('test')).toList() - assertEquals('There should be one name parameter', 1, nameParams.size()) - assertEquals('The parameter name is incorrect', ':name', nameParams[0].name) - assertEquals('The parameter type is incorrect', ParameterType.STRING, nameParams[0].type) - assertEquals('The parameter value is incorrect', '{test}', nameParams[0].value) + 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 @@ -79,10 +78,10 @@ class ParametersTest { void fieldNamesMultiplePostgres() { ForceDialect.postgres() def nameParams = Parameters.fieldNames(List.of('test', 'this', 'today')).toList() - assertEquals('There should be one name parameter', 1, nameParams.size()) - assertEquals('The parameter name is incorrect', ':name', nameParams[0].name) - assertEquals('The parameter type is incorrect', ParameterType.STRING, nameParams[0].type) - assertEquals('The parameter value is incorrect', '{test,this,today}', nameParams[0].value) + 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 @@ -90,10 +89,10 @@ class ParametersTest { void fieldNamesSingleSQLite() { ForceDialect.sqlite() def nameParams = Parameters.fieldNames(List.of('test')).toList() - assertEquals('There should be one name parameter', 1, nameParams.size()) - assertEquals('The parameter name is incorrect', ':name0', nameParams[0].name) - assertEquals('The parameter type is incorrect', ParameterType.STRING, nameParams[0].type) - assertEquals('The parameter value is incorrect', 'test', nameParams[0].value) + 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 @@ -101,22 +100,21 @@ class ParametersTest { void fieldNamesMultipleSQLite() { ForceDialect.sqlite() def nameParams = Parameters.fieldNames(List.of('test', 'this', 'today')).toList() - assertEquals('There should be one name parameter', 3, nameParams.size()) - assertEquals('The first parameter name is incorrect', ':name0', nameParams[0].name) - assertEquals('The first parameter type is incorrect', ParameterType.STRING, nameParams[0].type) - assertEquals('The first parameter value is incorrect', 'test', nameParams[0].value) - assertEquals('The second parameter name is incorrect', ':name1', nameParams[1].name) - assertEquals('The second parameter type is incorrect', ParameterType.STRING, nameParams[1].type) - assertEquals('The second parameter value is incorrect', 'this', nameParams[1].value) - assertEquals('The third parameter name is incorrect', ':name2', nameParams[2].name) - assertEquals('The third parameter type is incorrect', ParameterType.STRING, nameParams[2].type) - assertEquals('The third parameter value is incorrect', 'today', nameParams[2].value) + 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' } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('fieldNames fails if dialect not set') -// void fieldNamesFails() { -// assertThrows(DocumentException) { Parameters.fieldNames(List.of()) } -// } + @Test + @DisplayName('fieldNames fails if dialect not set') + void fieldNamesFails() { + assertThrows(DocumentException) { Parameters.fieldNames(List.of()) } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy index 30899bb..673a7e4 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy @@ -6,48 +6,48 @@ import solutions.bitbadger.documents.support.ThrowawayDatabase import static solutions.bitbadger.documents.extensions.ConnExt.* import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* class CountFunctions { static void all(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('There should have been 5 documents in the table', 5L, countAll(db.conn, TEST_TABLE)) + assertEquals 5L, countAll(db.conn, TEST_TABLE), 'There should have been 5 documents in the table' } static void byFieldsNumeric(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('There should have been 3 matching documents', 3L, - countByFields(db.conn, TEST_TABLE, List.of(Field.between('numValue', 10, 20)))) + assertEquals(3L, countByFields(db.conn, 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('There should have been 1 matching document', 1L, - countByFields(db.conn, TEST_TABLE, List.of(Field.between('value', 'aardvark', 'apple')))) + assertEquals(1L, countByFields(db.conn, 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('There should have been 2 matching documents', 2L, - countByContains(db.conn, TEST_TABLE, Map.of('value', 'purple'))) + assertEquals(2L, countByContains(db.conn, TEST_TABLE, Map.of('value', 'purple')), + 'There should have been 2 matching documents') } static void byContainsNoMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('There should have been no matching documents', 0L, - countByContains(db.conn, TEST_TABLE, Map.of('value', 'magenta'))) + assertEquals(0L, countByContains(db.conn, TEST_TABLE, Map.of('value', 'magenta')), + 'There should have been no matching documents') } static void byJsonPathMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('There should have been 2 matching documents', 2L, - countByJsonPath(db.conn, TEST_TABLE, '$.numValue ? (@ < 5)')) + assertEquals(2L, countByJsonPath(db.conn, TEST_TABLE, '$.numValue ? (@ < 5)'), + 'There should have been 2 matching documents') } static void byJsonPathNoMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('There should have been no matching documents', 0L, - countByJsonPath(db.conn, TEST_TABLE, '$.numValue ? (@ > 100)')) + assertEquals(0L, countByJsonPath(db.conn, TEST_TABLE, '$.numValue ? (@ > 100)'), + 'There should have been no matching documents') } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy index b252bbb..49ad2fa 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy @@ -14,12 +14,42 @@ class CountIT { @Test @DisplayName("all counts all documents") void all() { - PgDB db = new PgDB() - try { - CountFunctions.all(db) - } finally { - db.close() - } + new PgDB().withCloseable { CountFunctions.all(it) } } + @Test + @DisplayName("byFields counts documents by a numeric value") + void byFieldsNumeric() { + new PgDB().withCloseable { CountFunctions.byFieldsNumeric(it) } + } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + void byFieldsAlpha() { + new PgDB().withCloseable { CountFunctions.byFieldsAlpha(it) } + } + + @Test + @DisplayName("byContains counts documents when matches are found") + void byContainsMatch() { + new PgDB().withCloseable { CountFunctions.byContainsMatch(it) } + } + + @Test + @DisplayName("byContains counts documents when no matches are found") + void byContainsNoMatch() { + new PgDB().withCloseable { CountFunctions.byContainsNoMatch(it) } + } + + @Test + @DisplayName("byJsonPath counts documents when matches are found") + void byJsonPathMatch() { + new PgDB().withCloseable { CountFunctions.byJsonPathMatch(it) } + } + + @Test + @DisplayName("byJsonPath counts documents when no matches are found") + void byJsonPathNoMatch() { + new PgDB().withCloseable { CountFunctions.byJsonPathNoMatch(it) } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CountIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CountIT.groovy new file mode 100644 index 0000000..0dd4dad --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CountIT.groovy @@ -0,0 +1,50 @@ +package solutions.bitbadger.documents.groovy.jvm.integration.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.groovy.jvm.integration.common.CountFunctions +import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB + +import static org.junit.jupiter.api.Assertions.assertThrows + +/** + * SQLite integration tests for the `Count` object / `count*` connection extension functions + */ +@DisplayName("JVM | Groovy | SQLite: Count") +class CountIT { + + @Test + @DisplayName("all counts all documents") + void all() { + new SQLiteDB().withCloseable { CountFunctions.all(it) } + } + + @Test + @DisplayName("byFields counts documents by a numeric value") + void byFieldsNumeric() { + new SQLiteDB().withCloseable { CountFunctions.byFieldsNumeric(it) } + } + + @Test + @DisplayName("byFields counts documents by a alphanumeric value") + void byFieldsAlpha() { + new SQLiteDB().withCloseable { CountFunctions.byFieldsAlpha(it) } + } + + @Test + @DisplayName("byContains fails") + void byContainsMatch() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { CountFunctions.byContainsMatch(db) } + } + } + + @Test + @DisplayName("byJsonPath fails") + void byJsonPathMatch() { + new SQLiteDB().withCloseable { db -> + assertThrows(DocumentException) { CountFunctions.byJsonPathMatch(db) } + } + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy index 54d3d96..95d62b5 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy @@ -3,13 +3,13 @@ package solutions.bitbadger.documents.groovy.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.query.CountQuery import solutions.bitbadger.documents.support.ForceDialect import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Count` object @@ -28,59 +28,55 @@ class CountQueryTest { @Test @DisplayName('all generates correctly') void all() { - assertEquals('Count query not constructed correctly', "SELECT COUNT(*) AS it FROM $TEST_TABLE".toString(), - CountQuery.all(TEST_TABLE)) + 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('Count query not constructed correctly', - "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0".toString(), - CountQuery.byFields(TEST_TABLE, List.of(Field.equal('test', '', ':field0')))) + 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('Count query not constructed correctly', - "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data->>'test' = :field0".toString(), - CountQuery.byFields(TEST_TABLE, List.of(Field.equal('test', '', ':field0')))) + 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('Count query not constructed correctly', - "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria".toString(), - CountQuery.byContains(TEST_TABLE)) + assertEquals("SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE data @> :criteria".toString(), + CountQuery.byContains(TEST_TABLE), 'Count query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byContains fails | SQLite') -// void byContainsSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { CountQuery.byContains(TEST_TABLE) } -// } + @Test + @DisplayName('byContains fails | SQLite') + void byContainsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { CountQuery.byContains(TEST_TABLE) } + } @Test @DisplayName('byJsonPath generates correctly | PostgreSQL') void byJsonPathPostgres() { ForceDialect.postgres() - assertEquals('Count query not constructed correctly', - "SELECT COUNT(*) AS it FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), - CountQuery.byJsonPath(TEST_TABLE)) + 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') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byJsonPath fails | SQLite') -// void byJsonPathSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { CountQuery.byJsonPath(TEST_TABLE) } -// } + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { CountQuery.byJsonPath(TEST_TABLE) } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy index bba2783..6ce06fb 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy @@ -4,13 +4,13 @@ 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.DocumentException import solutions.bitbadger.documents.DocumentIndex import solutions.bitbadger.documents.query.DefinitionQuery import solutions.bitbadger.documents.support.ForceDialect import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Definition` object @@ -29,9 +29,8 @@ class DefinitionQueryTest { @Test @DisplayName('ensureTableFor generates correctly') void ensureTableFor() { - assertEquals('CREATE TABLE statement not constructed correctly', - 'CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)', - DefinitionQuery.ensureTableFor('my.table', 'JSONB')) + assertEquals('CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)', + DefinitionQuery.ensureTableFor('my.table', 'JSONB'), 'CREATE TABLE statement not constructed correctly') } @Test @@ -50,79 +49,80 @@ class DefinitionQueryTest { DefinitionQuery.ensureTable(TEST_TABLE)) } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('ensureTable fails when no dialect is set') -// void ensureTableFailsUnknown() { -// assertThrows(DocumentException) { DefinitionQuery.ensureTable(TEST_TABLE) } -// } + @Test + @DisplayName('ensureTable fails when no dialect is set') + void ensureTableFailsUnknown() { + assertThrows(DocumentException) { DefinitionQuery.ensureTable(TEST_TABLE) } + } @Test @DisplayName('ensureKey generates correctly with schema') void ensureKeyWithSchema() { - assertEquals('CREATE INDEX for key statement with schema not constructed correctly', - "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))", - DefinitionQuery.ensureKey('test.table', Dialect.POSTGRESQL)) + 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 INDEX for key statement without schema not constructed correctly', + assertEquals( "CREATE UNIQUE INDEX IF NOT EXISTS idx_${TEST_TABLE}_key ON $TEST_TABLE ((data->>'id'))".toString(), - DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE)) + 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 for multiple field statement not constructed correctly', - "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)" - .toString(), + 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)) + Dialect.POSTGRESQL), + 'CREATE INDEX for multiple field statement not constructed correctly') } @Test @DisplayName('ensureIndexOn generates nested field | PostgreSQL') void ensureIndexOnNestedPostgres() { - assertEquals('CREATE INDEX for nested PostgreSQL field incorrect', - "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_nest ON $TEST_TABLE ((data#>>'{a,b,c}'))".toString(), - DefinitionQuery.ensureIndexOn(TEST_TABLE, 'nest', List.of('a.b.c'), Dialect.POSTGRESQL)) + 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 for nested SQLite field incorrect', + 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)) + 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 for full document index incorrect', - "CREATE INDEX IF NOT EXISTS idx_${TEST_TABLE}_document ON $TEST_TABLE USING GIN (data)".toString(), - DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL)) + 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 for optimized document index incorrect', + 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)) + DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED), + 'CREATE INDEX for optimized document index incorrect') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('ensureDocumentIndexOn fails | SQLite') -// void ensureDocumentIndexOnFailsSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) } -// } + @Test + @DisplayName('ensureDocumentIndexOn fails | SQLite') + void ensureDocumentIndexOnFailsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL) } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy index 06fe0b7..4b16942 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy @@ -3,13 +3,13 @@ package solutions.bitbadger.documents.groovy.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.query.DeleteQuery import solutions.bitbadger.documents.support.ForceDialect import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Delete` object @@ -29,66 +29,63 @@ class DeleteQueryTest { @DisplayName('byId generates correctly | PostgreSQL') void byIdPostgres() { ForceDialect.postgres() - assertEquals('Delete query not constructed correctly', - "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), DeleteQuery.byId(TEST_TABLE)) + 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 query not constructed correctly', - "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), DeleteQuery.byId(TEST_TABLE)) + 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 query not constructed correctly', - "DELETE FROM $TEST_TABLE WHERE data->>'a' = :b".toString(), - DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b')))) + 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 query not constructed correctly', - "DELETE FROM $TEST_TABLE WHERE data->>'a' = :b".toString(), - DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b')))) + 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 query not constructed correctly', - "DELETE FROM $TEST_TABLE WHERE data @> :criteria".toString(), DeleteQuery.byContains(TEST_TABLE)) + assertEquals("DELETE FROM $TEST_TABLE WHERE data @> :criteria".toString(), DeleteQuery.byContains(TEST_TABLE), + 'Delete query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byContains fails | SQLite') -// void byContainsSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { DeleteQuery.byContains(TEST_TABLE) } -// } + @Test + @DisplayName('byContains fails | SQLite') + void byContainsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { DeleteQuery.byContains(TEST_TABLE) } + } @Test @DisplayName('byJsonPath generates correctly | PostgreSQL') void byJsonPathPostgres() { ForceDialect.postgres() - assertEquals('Delete query not constructed correctly', - "DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), - DeleteQuery.byJsonPath(TEST_TABLE)) + assertEquals("DELETE FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + DeleteQuery.byJsonPath(TEST_TABLE), 'Delete query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byJsonPath fails | SQLite') -// void byJsonPathSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { DeleteQuery.byJsonPath(TEST_TABLE) } -// } + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { DeleteQuery.byJsonPath(TEST_TABLE) } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy index 1d7cc15..29edebb 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy @@ -5,12 +5,12 @@ 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.DocumentException import solutions.bitbadger.documents.query.DocumentQuery import solutions.bitbadger.documents.support.ForceDialect import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Document` object @@ -62,20 +62,20 @@ class DocumentQueryTest { @DisplayName('insert generates auto UUID | PostgreSQL') void insertAutoUUIDPostgres() { ForceDialect.postgres() - def query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID) - assertTrue("Query start not correct (actual: $query)", - query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"")) - assertTrue('Query end not correct', query.endsWith("\"}')")) + 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 start not correct (actual: $query)", - query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '")) - assertTrue('Query end not correct', query.endsWith("'))")) + 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 @@ -84,13 +84,14 @@ class DocumentQueryTest { try { ForceDialect.postgres() Configuration.idStringLength = 8 - def query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) - assertTrue("Query start not correct (actual: $query)", - query.startsWith("INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"")) - assertTrue('Query end not correct', query.endsWith("\"}')")) - assertEquals('Random string length incorrect', 8, + 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()) + .replace("\"}')", '').length(), + 'Random string length incorrect') } finally { Configuration.idStringLength = 16 } @@ -100,36 +101,36 @@ class DocumentQueryTest { @DisplayName('insert generates auto random string | SQLite') void insertAutoRandomSQLite() { ForceDialect.sqlite() - def query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) - assertTrue("Query start not correct (actual: $query)", - query.startsWith("INSERT INTO $TEST_TABLE VALUES (json_set(:data, '\$.id', '")) - assertTrue('Query end not correct', query.endsWith("'))")) - assertEquals('Random string length incorrect', Configuration.idStringLength, + 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()) + .length(), + 'Random string length incorrect') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('insert fails when no dialect is set') -// void insertFailsUnknown() { -// assertThrows(DocumentException) { DocumentQuery.insert(TEST_TABLE) } -// } + @Test + @DisplayName('insert fails when no dialect is set') + void insertFailsUnknown() { + assertThrows(DocumentException) { DocumentQuery.insert(TEST_TABLE) } + } @Test @DisplayName('save generates correctly') void save() { ForceDialect.postgres() - assertEquals('INSERT ON CONFLICT UPDATE statement not constructed correctly', + assertEquals( "INSERT INTO $TEST_TABLE VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data" .toString(), - DocumentQuery.save(TEST_TABLE)) + DocumentQuery.save(TEST_TABLE), 'INSERT ON CONFLICT UPDATE statement not constructed correctly') } @Test @DisplayName('update generates successfully') void update() { - assertEquals('Update query not constructed correctly', "UPDATE $TEST_TABLE SET data = :data".toString(), - DocumentQuery.update(TEST_TABLE)) + assertEquals("UPDATE $TEST_TABLE SET data = :data".toString(), DocumentQuery.update(TEST_TABLE), + 'Update query not constructed correctly') } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy index 4f3502f..7d557c2 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy @@ -3,13 +3,13 @@ package solutions.bitbadger.documents.groovy.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.query.FindQuery import solutions.bitbadger.documents.support.ForceDialect import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Find` object @@ -28,74 +28,71 @@ class FindQueryTest { @Test @DisplayName('all generates correctly') void all() { - assertEquals('Find query not constructed correctly', "SELECT data FROM $TEST_TABLE".toString(), - FindQuery.all(TEST_TABLE)) + 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('Find query not constructed correctly', - "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), FindQuery.byId(TEST_TABLE)) + 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('Find query not constructed correctly', - "SELECT data FROM $TEST_TABLE WHERE data->>'id' = :id".toString(), FindQuery.byId(TEST_TABLE)) + 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('Find query not constructed correctly', - "SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND (data->>'c')::numeric < :d".toString(), - FindQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b'), Field.less('c', 14, ':d')))) + 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('Find query not constructed correctly', - "SELECT data FROM $TEST_TABLE WHERE data->>'a' = :b AND data->>'c' < :d".toString(), - FindQuery.byFields(TEST_TABLE, List.of(Field.equal('a', '', ':b'), Field.less('c', 14, ':d')))) + 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('Find query not constructed correctly', - "SELECT data FROM $TEST_TABLE WHERE data @> :criteria".toString(), FindQuery.byContains(TEST_TABLE)) + assertEquals("SELECT data FROM $TEST_TABLE WHERE data @> :criteria".toString(), + FindQuery.byContains(TEST_TABLE), 'Find query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byContains fails | SQLite') -// void byContainsSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { FindQuery.byContains(TEST_TABLE) } -// } + @Test + @DisplayName('byContains fails | SQLite') + void byContainsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { FindQuery.byContains(TEST_TABLE) } + } @Test @DisplayName('byJsonPath generates correctly | PostgreSQL') void byJsonPathPostgres() { ForceDialect.postgres() - assertEquals('Find query not constructed correctly', - "SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), - FindQuery.byJsonPath(TEST_TABLE)) + assertEquals("SELECT data FROM $TEST_TABLE WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), + FindQuery.byJsonPath(TEST_TABLE), 'Find query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byJsonPath fails | SQLite') -// void byJsonPathSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { FindQuery.byJsonPath(TEST_TABLE) } -// } + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { FindQuery.byJsonPath(TEST_TABLE) } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy index f34c7a7..c362109 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy @@ -3,13 +3,13 @@ package solutions.bitbadger.documents.groovy.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.query.PatchQuery import solutions.bitbadger.documents.support.ForceDialect import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Patch` object @@ -29,69 +29,64 @@ class PatchQueryTest { @DisplayName('byId generates correctly | PostgreSQL') void byIdPostgres() { ForceDialect.postgres() - assertEquals('Patch query not constructed correctly', - "UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'id' = :id".toString(), - PatchQuery.byId(TEST_TABLE)) + 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('Patch query not constructed correctly', - "UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id".toString(), - PatchQuery.byId(TEST_TABLE)) + 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('Patch query not constructed correctly', - "UPDATE $TEST_TABLE SET data = data || :data WHERE data->>'z' = :y".toString(), - PatchQuery.byFields(TEST_TABLE, List.of(Field.equal('z', '', ':y')))) + 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('Patch query not constructed correctly', - "UPDATE $TEST_TABLE SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y".toString(), - PatchQuery.byFields(TEST_TABLE, List.of(Field.equal('z', '', ':y')))) + 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('Patch query not constructed correctly', - "UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria".toString(), - PatchQuery.byContains(TEST_TABLE)) + assertEquals("UPDATE $TEST_TABLE SET data = data || :data WHERE data @> :criteria".toString(), + PatchQuery.byContains(TEST_TABLE), 'Patch query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byContains fails | SQLite') -// void byContainsSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { PatchQuery.byContains(TEST_TABLE) } -// } + @Test + @DisplayName('byContains fails | SQLite') + void byContainsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { PatchQuery.byContains(TEST_TABLE) } + } @Test @DisplayName('byJsonPath generates correctly | PostgreSQL') void byJsonPathPostgres() { ForceDialect.postgres() - assertEquals('Patch query not constructed correctly', + assertEquals( "UPDATE $TEST_TABLE SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)".toString(), - PatchQuery.byJsonPath(TEST_TABLE)) + PatchQuery.byJsonPath(TEST_TABLE), 'Patch query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byJsonPath fails | SQLite') -// void byJsonPathSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { PatchQuery.byJsonPath(TEST_TABLE) } -// } + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { PatchQuery.byJsonPath(TEST_TABLE) } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy index 069bbec..0643482 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy @@ -9,7 +9,7 @@ import solutions.bitbadger.documents.FieldMatch import solutions.bitbadger.documents.query.QueryUtils import solutions.bitbadger.documents.support.ForceDialect -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `QueryUtils` class @@ -28,28 +28,28 @@ class QueryUtilsTest { @Test @DisplayName('statementWhere generates correctly') void statementWhere() { - assertEquals('Statements not combined correctly', 'x WHERE y', QueryUtils.statementWhere('x', 'y')) + 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)) + 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')) + 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)) + assertEquals "yo WHERE data->>'id' = :id", QueryUtils.byId('yo', 27) } @Test @@ -89,71 +89,75 @@ class QueryUtilsTest { @Test @DisplayName('orderBy generates for no fields') void orderByNone() { - assertEquals('ORDER BY should have been blank (PostgreSQL)', '', QueryUtils.orderBy(List.of(), - Dialect.POSTGRESQL)) - assertEquals('ORDER BY should have been blank (SQLite)', '', QueryUtils.orderBy(List.of(), Dialect.SQLITE)) + 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 not constructed correctly', " ORDER BY data->>'TestField'", - QueryUtils.orderBy(List.of(Field.named('TestField')), Dialect.POSTGRESQL)) + 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 not constructed correctly', " ORDER BY data->>'TestField'", - QueryUtils.orderBy(List.of(Field.named('TestField')), Dialect.SQLITE)) + 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 not constructed correctly', - " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", + assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", QueryUtils.orderBy(List.of(Field.named('Nested.Test.Field DESC'), Field.named('AnotherField'), Field.named('It DESC')), - Dialect.POSTGRESQL)) + Dialect.POSTGRESQL), + 'ORDER BY not constructed correctly') } @Test @DisplayName('orderBy generates multiple with direction | SQLite') void orderByMultipleSQLite() { - assertEquals('ORDER BY not constructed correctly', - " ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", + 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)) + Dialect.SQLITE), + 'ORDER BY not constructed correctly') } @Test @DisplayName('orderBy generates numeric ordering | PostgreSQL') void orderByNumericPostgres() { - assertEquals('ORDER BY not constructed correctly', " ORDER BY (data->>'Test')::numeric", - QueryUtils.orderBy(List.of(Field.named('n:Test')), Dialect.POSTGRESQL)) + 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 not constructed correctly', " ORDER BY data->>'Test'", - QueryUtils.orderBy(List.of(Field.named('n:Test')), Dialect.SQLITE)) + 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 not constructed correctly', " ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST", - QueryUtils.orderBy(List.of(Field.named('i:Test.Field DESC NULLS FIRST')), Dialect.POSTGRESQL)) + 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 not constructed correctly', - " ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST", - QueryUtils.orderBy(List.of(Field.named('i:Test.Field ASC NULLS LAST')), Dialect.SQLITE)) + 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy index 44acb89..546b06c 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.groovy.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.Parameter import solutions.bitbadger.documents.ParameterType @@ -11,7 +11,7 @@ import solutions.bitbadger.documents.query.RemoveFieldsQuery import solutions.bitbadger.documents.support.ForceDialect import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `RemoveFields` object @@ -31,77 +31,77 @@ class RemoveFieldsQueryTest { @DisplayName('byId generates correctly | PostgreSQL') void byIdPostgres() { ForceDialect.postgres() - assertEquals('Remove Fields query not constructed correctly', - "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}')))) + 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('Remove Field query not constructed correctly', + 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')))) + new Parameter(':name1', ParameterType.STRING, 'z'))), + 'Remove Field query not constructed correctly') } @Test @DisplayName('byFields generates correctly | PostgreSQL') void byFieldsPostgres() { ForceDialect.postgres() - assertEquals('Remove Field query not constructed correctly', - "UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data->>'f' > :g".toString(), + 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')))) + List.of(Field.greater('f', '', ':g'))), + 'Remove Field query not constructed correctly') } @Test @DisplayName('byFields generates correctly | SQLite') void byFieldsSQLite() { ForceDialect.sqlite() - assertEquals('Remove Field query not constructed correctly', - "UPDATE $TEST_TABLE SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g".toString(), + 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')))) + List.of(Field.greater('f', '', ':g'))), + 'Remove Field query not constructed correctly') } @Test @DisplayName('byContains generates correctly | PostgreSQL') void byContainsPostgres() { ForceDialect.postgres() - assertEquals('Remove Field query not constructed correctly', - "UPDATE $TEST_TABLE SET data = data - :name::text[] WHERE data @> :criteria".toString(), + 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}')))) + List.of(new Parameter(':name', ParameterType.STRING, '{m,n}'))), + 'Remove Field query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byContains fails | SQLite') -// void byContainsSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { RemoveFieldsQuery.byContains(TEST_TABLE, List.of()) } -// } + @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('Remove Field query not constructed correctly', + 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}')))) + List.of(new Parameter(':name', ParameterType.STRING, '{o,p}'))), + 'Remove Field query not constructed correctly') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('byJsonPath fails | SQLite') -// void byJsonPathSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { RemoveFieldsQuery.byJsonPath(TEST_TABLE, List.of()) } -// } + @Test + @DisplayName('byJsonPath fails | SQLite') + void byJsonPathSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { RemoveFieldsQuery.byJsonPath(TEST_TABLE, List.of()) } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy index d65a115..e8fea87 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy @@ -3,13 +3,13 @@ package solutions.bitbadger.documents.groovy.query import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -//import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch import solutions.bitbadger.documents.query.Where import solutions.bitbadger.documents.support.ForceDialect -import static groovy.test.GroovyAssert.* +import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Where` object @@ -28,28 +28,28 @@ class WhereTest { @Test @DisplayName('byFields is blank when given no fields') void byFieldsBlankIfEmpty() { - assertEquals('', Where.byFields(List.of())) + 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')))) + 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')))) + 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')))) + assertEquals "data->>'it' = :that", Where.byFields(List.of(Field.equal('it', '', ':that'))) } @Test @@ -98,78 +98,76 @@ class WhereTest { @DisplayName('byId generates defaults for alphanumeric key | PostgreSQL') void byIdDefaultAlphaPostgres() { ForceDialect.postgres() - assertEquals("data->>'id' = :id", Where.byId()) + 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)) + 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()) + assertEquals "data->>'id' = :id", Where.byId() } @Test @DisplayName('byId generates named ID | PostgreSQL') void byIdDefaultNamedPostgres() { ForceDialect.postgres() - assertEquals("data->>'id' = :key", Where.byId(':key')) + 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')) + assertEquals "data->>'id' = :key", Where.byId(':key') } @Test @DisplayName('jsonContains generates defaults | PostgreSQL') void jsonContainsDefaultPostgres() { ForceDialect.postgres() - assertEquals('data @> :criteria', Where.jsonContains()) + assertEquals 'data @> :criteria', Where.jsonContains() } @Test @DisplayName('jsonContains generates named parameter | PostgreSQL') void jsonContainsNamedPostgres() { ForceDialect.postgres() - assertEquals('data @> :it', Where.jsonContains(':it')) + assertEquals 'data @> :it', Where.jsonContains(':it') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('jsonContains fails | SQLite') -// void jsonContainsFailsSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { Where.jsonContains() } -// } + @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()) + 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')) + assertEquals 'jsonb_path_exists(data, :jp::jsonpath)', Where.jsonPathMatches(':jp') } -// TODO: resolve java.base open issue -// @Test -// @DisplayName('jsonPathMatches fails | SQLite') -// void jsonPathFailsSQLite() { -// ForceDialect.sqlite() -// assertThrows(DocumentException) { Where.jsonPathMatches() } -// } + @Test + @DisplayName('jsonPathMatches fails | SQLite') + void jsonPathFailsSQLite() { + ForceDialect.sqlite() + assertThrows(DocumentException) { Where.jsonPathMatches() } + } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy index 172daf1..4d94e97 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy @@ -26,8 +26,6 @@ class JsonDocument { new JsonDocument("five", "purple", 18)) static void load(ThrowawayDatabase db, String tableName = TEST_TABLE) { - for (doc in testDocuments) { - Document.insert(tableName, doc, db.conn) - } + testDocuments.forEach { Document.insert(tableName, it, db.conn) } } } -- 2.47.2 From 815f50633993f306a4d42f09a67944c4fd8d4ac1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 19 Mar 2025 12:25:33 -0400 Subject: [PATCH 59/88] Add custom ITs for Java/Scala/Groovy --- .../src/main/kotlin/extensions/Connection.kt | 1 + src/jvm/src/main/kotlin/jvm/Results.kt | 9 +-- .../integration/common/CustomFunctions.groovy | 72 +++++++++++++++++ .../jvm/integration/postgresql/CountIT.groovy | 14 ++-- .../integration/postgresql/CustomIT.groovy | 55 +++++++++++++ .../jvm/integration/sqlite/CustomIT.groovy | 55 +++++++++++++ .../groovy/support/JsonDocument.groovy | 2 +- .../groovy/support/SubDocument.groovy | 2 +- .../integration/common/CustomFunctions.java | 78 +++++++++++++++++++ .../jvm/integration/postgresql/CustomIT.java | 70 +++++++++++++++++ .../java/jvm/integration/sqlite/CustomIT.java | 70 +++++++++++++++++ .../documents/java/support/JsonDocument.java | 4 + .../jvm/integration/postgresql/CustomIT.kt | 2 +- .../kotlin/jvm/integration/sqlite/CustomIT.kt | 2 +- .../integration/common/CustomFunctions.scala | 75 ++++++++++++++++++ .../jvm/integration/postgresql/CustomIT.scala | 46 +++++++++++ .../jvm/integration/sqlite/CustomIT.scala | 46 +++++++++++ 17 files changed, 586 insertions(+), 17 deletions(-) create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CustomFunctions.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CustomIT.groovy create mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CustomIT.groovy create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CustomFunctions.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CustomIT.java create mode 100644 src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CustomIT.java create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CustomFunctions.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CustomIT.scala create mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CustomIT.scala diff --git a/src/jvm/src/main/kotlin/extensions/Connection.kt b/src/jvm/src/main/kotlin/extensions/Connection.kt index 9bdf51e..e539c22 100644 --- a/src/jvm/src/main/kotlin/extensions/Connection.kt +++ b/src/jvm/src/main/kotlin/extensions/Connection.kt @@ -56,6 +56,7 @@ fun Connection.customSingle( * @throws DocumentException If parameters are invalid */ @Throws(DocumentException::class) +@JvmOverloads fun Connection.customNonQuery(query: String, parameters: Collection> = listOf()) = Custom.nonQuery(query, parameters, this) diff --git a/src/jvm/src/main/kotlin/jvm/Results.kt b/src/jvm/src/main/kotlin/jvm/Results.kt index f670a67..0bea109 100644 --- a/src/jvm/src/main/kotlin/jvm/Results.kt +++ b/src/jvm/src/main/kotlin/jvm/Results.kt @@ -40,9 +40,8 @@ object Results { * @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 + * @throws DocumentException If there is a problem executing the query (unchecked) */ - @Throws(DocumentException::class) @JvmStatic fun toCustomList( stmt: PreparedStatement, clazz: Class, mapFunc: (ResultSet, Class) -> TDoc @@ -65,9 +64,8 @@ object Results { * @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 + * @throws DocumentException If the dialect has not been set (unchecked) */ - @Throws(DocumentException::class) @JvmStatic fun toCount(rs: ResultSet, clazz: Class<*>) = when (Configuration.dialect()) { @@ -81,9 +79,8 @@ object Results { * @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 + * @throws DocumentException If the dialect has not been set (unchecked) */ - @Throws(DocumentException::class) @JvmStatic fun toExists(rs: ResultSet, clazz: Class<*>) = when (Configuration.dialect()) { diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CustomFunctions.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CustomFunctions.groovy new file mode 100644 index 0000000..7147a8c --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CustomFunctions.groovy @@ -0,0 +1,72 @@ +package solutions.bitbadger.documents.groovy.jvm.integration.common + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Field +import solutions.bitbadger.documents.Parameter +import solutions.bitbadger.documents.ParameterType +import solutions.bitbadger.documents.groovy.support.JsonDocument +import solutions.bitbadger.documents.jvm.Results +import solutions.bitbadger.documents.query.CountQuery +import solutions.bitbadger.documents.query.DeleteQuery +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.support.ThrowawayDatabase + +import static org.junit.jupiter.api.Assertions.* +import static solutions.bitbadger.documents.extensions.ConnExt.* +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +class CustomFunctions { + + static void listEmpty(ThrowawayDatabase db) { + JsonDocument.load(db) + deleteByFields(db.conn, TEST_TABLE, List.of(Field.exists(Configuration.idField))) + def result = customList(db.conn, FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results.&fromData) + assertEquals(0, result.size(), "There should have been no results") + } + + static void listAll(ThrowawayDatabase db) { + JsonDocument.load(db) + def result = customList(db.conn, FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results.&fromData) + assertEquals(5, result.size(), "There should have been 5 results") + } + + static void singleNone(ThrowawayDatabase db) { + assertNull(customSingle(db.conn, FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results.&fromData), + "There should not have been a document returned") + + } + + static void singleOne(ThrowawayDatabase db) { + JsonDocument.load(db) + assertNotNull( + customSingle(db.conn, FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results.&fromData), + "There should not have been a document returned") + } + + static void nonQueryChanges(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals(5L, customScalar(db.conn, CountQuery.all(TEST_TABLE), List.of(), Long.class, Results.&toCount), + "There should have been 5 documents in the table") + customNonQuery(db.conn, "DELETE FROM $TEST_TABLE") + assertEquals(0L, customScalar(db.conn, CountQuery.all(TEST_TABLE), List.of(), Long.class, Results.&toCount), + "There should have been no documents in the table") + } + + static void nonQueryNoChanges(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals(5L, customScalar(db.conn, CountQuery.all(TEST_TABLE), List.of(), Long.class, Results.&toCount), + "There should have been 5 documents in the table") + customNonQuery(db.conn, DeleteQuery.byId(TEST_TABLE, "eighty-two"), + List.of(new Parameter(":id", ParameterType.STRING, "eighty-two"))) + assertEquals(5L, customScalar(db.conn, CountQuery.all(TEST_TABLE), List.of(), Long.class, Results.&toCount), + "There should still have been 5 documents in the table") + } + + static void scalar(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals(3L, + customScalar(db.conn, "SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", List.of(), Long.class, + Results.&toCount), + "The number 3 should have been returned") + } +} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy index 49ad2fa..0763644 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy @@ -14,42 +14,42 @@ class CountIT { @Test @DisplayName("all counts all documents") void all() { - new PgDB().withCloseable { CountFunctions.all(it) } + new PgDB().withCloseable(CountFunctions.&all) } @Test @DisplayName("byFields counts documents by a numeric value") void byFieldsNumeric() { - new PgDB().withCloseable { CountFunctions.byFieldsNumeric(it) } + new PgDB().withCloseable(CountFunctions.&byFieldsNumeric) } @Test @DisplayName("byFields counts documents by a alphanumeric value") void byFieldsAlpha() { - new PgDB().withCloseable { CountFunctions.byFieldsAlpha(it) } + new PgDB().withCloseable(CountFunctions.&byFieldsAlpha) } @Test @DisplayName("byContains counts documents when matches are found") void byContainsMatch() { - new PgDB().withCloseable { CountFunctions.byContainsMatch(it) } + new PgDB().withCloseable(CountFunctions.&byContainsMatch) } @Test @DisplayName("byContains counts documents when no matches are found") void byContainsNoMatch() { - new PgDB().withCloseable { CountFunctions.byContainsNoMatch(it) } + new PgDB().withCloseable(CountFunctions.&byContainsNoMatch) } @Test @DisplayName("byJsonPath counts documents when matches are found") void byJsonPathMatch() { - new PgDB().withCloseable { CountFunctions.byJsonPathMatch(it) } + new PgDB().withCloseable(CountFunctions.&byJsonPathMatch) } @Test @DisplayName("byJsonPath counts documents when no matches are found") void byJsonPathNoMatch() { - new PgDB().withCloseable { CountFunctions.byJsonPathNoMatch(it) } + new PgDB().withCloseable(CountFunctions.&byJsonPathNoMatch) } } diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CustomIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CustomIT.groovy new file mode 100644 index 0000000..2250f01 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CustomIT.groovy @@ -0,0 +1,55 @@ +package solutions.bitbadger.documents.groovy.jvm.integration.postgresql + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.groovy.jvm.integration.common.CustomFunctions +import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB + +/** + * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("JVM | Groovy | PostgreSQL: Custom") +class CustomIT { + + @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("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("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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CustomIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CustomIT.groovy new file mode 100644 index 0000000..8c3d155 --- /dev/null +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CustomIT.groovy @@ -0,0 +1,55 @@ +package solutions.bitbadger.documents.groovy.jvm.integration.sqlite + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import solutions.bitbadger.documents.groovy.jvm.integration.common.CustomFunctions +import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB + +/** + * SQLite integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("JVM | Groovy | SQLite: Custom") +class CustomIT { + + @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("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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy index 4d94e97..78ba92e 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy @@ -11,7 +11,7 @@ class JsonDocument { int numValue SubDocument sub - JsonDocument(String id, String value = "", int numValue = 0, SubDocument sub = null) { + JsonDocument(String id = null, String value = "", int numValue = 0, SubDocument sub = null) { this.id = id this.value = value this.numValue = numValue diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy index c2efe4d..744e498 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy +++ b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy @@ -4,7 +4,7 @@ class SubDocument { String foo String bar - SubDocument(String foo, String bar) { + SubDocument(String foo = "", String bar = "") { this.foo = foo this.bar = bar } diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CustomFunctions.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CustomFunctions.java new file mode 100644 index 0000000..a1bdfe9 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CustomFunctions.java @@ -0,0 +1,78 @@ +package solutions.bitbadger.documents.java.jvm.integration.common; + +import solutions.bitbadger.documents.*; +import solutions.bitbadger.documents.java.support.JsonDocument; +import solutions.bitbadger.documents.jvm.Results; +import solutions.bitbadger.documents.query.CountQuery; +import solutions.bitbadger.documents.query.DeleteQuery; +import solutions.bitbadger.documents.query.FindQuery; +import solutions.bitbadger.documents.support.ThrowawayDatabase; + +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static solutions.bitbadger.documents.extensions.ConnExt.*; +import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; + +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 singleNone(ThrowawayDatabase db) throws DocumentException { + assertNull( + customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData), + "There should not have been a document returned"); + } + + public static void singleOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertNotNull( + customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData), + "There should not have been a document returned"); + } + + 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/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CustomIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CustomIT.java new file mode 100644 index 0000000..3396255 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CustomIT.java @@ -0,0 +1,70 @@ +package solutions.bitbadger.documents.java.jvm.integration.postgresql; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB; +import solutions.bitbadger.documents.java.jvm.integration.common.CustomFunctions; + +/** + * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("JVM | Java | PostgreSQL: Custom") +final public class CustomIT { + + @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("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("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/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CustomIT.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CustomIT.java new file mode 100644 index 0000000..76acab9 --- /dev/null +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CustomIT.java @@ -0,0 +1,70 @@ +package solutions.bitbadger.documents.java.jvm.integration.sqlite; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.java.jvm.integration.common.CustomFunctions; +import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB; + +/** + * SQLite integration tests for the `Custom` object / `custom*` connection extension functions + */ +@DisplayName("JVM | Java | SQLite: Custom") +final public class CustomIT { + + @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("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("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/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java index 8b118cc..ec6e639 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java +++ b/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java @@ -62,6 +62,10 @@ public class JsonDocument { public JsonDocument(String id) { this(id, "", 0, null); } + + public JsonDocument() { + this(null); + } private static final List testDocuments = List.of( new JsonDocument("one", "FIRST!", 0), diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt b/src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt index a8ea10e..25272f1 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt @@ -8,7 +8,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Custom") +@DisplayName("JVM | Kotlin | PostgreSQL: Custom") class CustomIT { @Test diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt b/src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt index 5d9b3dc..03ef8ee 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt +++ b/src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt @@ -7,7 +7,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Custom` object / `custom*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Custom") +@DisplayName("JVM | Kotlin | SQLite: Custom") class CustomIT { @Test diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CustomFunctions.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CustomFunctions.scala new file mode 100644 index 0000000..0d7e5a0 --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CustomFunctions.scala @@ -0,0 +1,75 @@ +package solutions.bitbadger.documents.scala.jvm.integration.common + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.{Configuration, Field, Parameter, ParameterType} +import solutions.bitbadger.documents.extensions.ConnExt.* +import solutions.bitbadger.documents.jvm.Results +import solutions.bitbadger.documents.query.{CountQuery, DeleteQuery, FindQuery} +import solutions.bitbadger.documents.scala.support.JsonDocument +import solutions.bitbadger.documents.support.ThrowawayDatabase +import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE + +import scala.annotation.nowarn +import scala.jdk.CollectionConverters.* + +object CustomFunctions { + + def listEmpty(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + deleteByFields(db.getConn, TEST_TABLE, (Field.exists(Configuration.idField) :: Nil).asJava) + @nowarn + val result = customList(db.getConn, FindQuery.all(TEST_TABLE), List().asJava, classOf[JsonDocument], + Results.fromData) + assertEquals(0, result.size, "There should have been no results") + + def listAll(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + @nowarn + val result = customList(db.getConn, FindQuery.all(TEST_TABLE), List().asJava, classOf[JsonDocument], + Results.fromData) + assertEquals(5, result.size, "There should have been 5 results") + + @nowarn + def singleNone(db: ThrowawayDatabase): Unit = + assertNull( + customSingle(db.getConn, FindQuery.all(TEST_TABLE), List().asJava, classOf[JsonDocument], Results.fromData), + "There should not have been a document returned") + + @nowarn + def singleOne(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertNotNull( + customSingle(db.getConn, FindQuery.all(TEST_TABLE), List().asJava, classOf[JsonDocument], Results.fromData), + "There should not have been a document returned") + + @nowarn + def nonQueryChanges(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, + customScalar(db.getConn, CountQuery.all(TEST_TABLE), List().asJava, classOf[Long], Results.toCount), + "There should have been 5 documents in the table") + customNonQuery(db.getConn, s"DELETE FROM $TEST_TABLE") + assertEquals(0L, + customScalar(db.getConn, CountQuery.all(TEST_TABLE), List().asJava, classOf[Long], Results.toCount), + "There should have been no documents in the table") + + @nowarn + def nonQueryNoChanges(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(5L, + customScalar(db.getConn, CountQuery.all(TEST_TABLE), List().asJava, classOf[Long], Results.toCount), + "There should have been 5 documents in the table") + customNonQuery(db.getConn, DeleteQuery.byId(TEST_TABLE, "eighty-two"), + (Parameter(":id", ParameterType.STRING, "eighty-two") :: Nil).asJava) + assertEquals(5L, + customScalar(db.getConn, CountQuery.all(TEST_TABLE), List().asJava, classOf[Long], Results.toCount), + "There should still have been 5 documents in the table") + + @nowarn + def scalar(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals(3L, + customScalar(db.getConn, s"SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", List().asJava, classOf[Long], + Results.toCount), + "The number 3 should have been returned") +} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CustomIT.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CustomIT.scala new file mode 100644 index 0000000..4e74f7d --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CustomIT.scala @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.scala.jvm.integration.postgresql + +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB +import solutions.bitbadger.documents.scala.jvm.integration.common.CustomFunctions + +import scala.util.Using + +@DisplayName("JVM | Scala | PostgreSQL: Custom") +class CustomIT { + + @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("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("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/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CustomIT.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CustomIT.scala new file mode 100644 index 0000000..36c08ab --- /dev/null +++ b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CustomIT.scala @@ -0,0 +1,46 @@ +package solutions.bitbadger.documents.scala.jvm.integration.sqlite + +import org.junit.jupiter.api.{DisplayName, Test} +import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB +import solutions.bitbadger.documents.scala.jvm.integration.common.CustomFunctions + +import scala.util.Using + +@DisplayName("JVM | Scala | SQLite: Custom") +class CustomIT { + + @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("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("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) } +} -- 2.47.2 From 50188d939e80c3039926c285e2b826a3a2a44f42 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 25 Mar 2025 07:20:30 -0400 Subject: [PATCH 60/88] Reorg modules; complete impls/tests --- .idea/compiler.xml | 13 +- .idea/encodings.xml | 17 + .idea/kotlinc.xml | 12 +- .idea/modules.xml | 4 +- .idea/scala_compiler.xml | 2 +- java.iml | 8 + pom.xml | 50 +- src/core/core.iml | 8 + src/{jvm => core}/pom.xml | 121 ++--- src/core/src/main/java/module-info.java | 9 + src/{jvm => core}/src/main/kotlin/AutoId.kt | 0 .../src/main/kotlin/Comparison.kt | 0 .../src/main/kotlin/Configuration.kt | 0 src/{jvm => core}/src/main/kotlin/Dialect.kt | 0 .../src/main/kotlin/DocumentException.kt | 0 .../src/main/kotlin/DocumentIndex.kt | 0 .../src/main/kotlin/DocumentSerializer.kt | 0 src/{jvm => core}/src/main/kotlin/Field.kt | 0 .../src/main/kotlin/FieldFormat.kt | 0 .../src/main/kotlin/FieldMatch.kt | 0 src/{jvm => core}/src/main/kotlin/Op.kt | 0 .../src/main/kotlin/Parameter.kt | 0 .../src/main/kotlin/ParameterName.kt | 0 .../src/main/kotlin/ParameterType.kt | 0 .../src/main/kotlin/java}/Count.kt | 14 +- .../src/main/kotlin/java}/Custom.kt | 30 +- .../src/main/kotlin/java}/Definition.kt | 11 +- .../src/main/kotlin/java}/Delete.kt | 14 +- .../src/main/kotlin/java}/Document.kt | 12 +- .../src/main/kotlin/java}/DocumentConfig.kt | 2 +- .../src/main/kotlin/java}/Exists.kt | 15 +- .../jvm => core/src/main/kotlin/java}/Find.kt | 52 +- .../kotlin/java}/NullDocumentSerializer.kt | 2 +- .../src/main/kotlin/java}/Parameters.kt | 2 +- .../src/main/kotlin/java}/Patch.kt | 27 +- .../src/main/kotlin/java}/RemoveFields.kt | 26 +- .../src/main/kotlin/java}/Results.kt | 3 +- .../kotlin/java}/extensions/Connection.kt | 6 +- .../src/main/kotlin/query/CountQuery.kt | 0 .../src/main/kotlin/query/DefinitionQuery.kt | 0 .../src/main/kotlin/query/DeleteQuery.kt | 0 .../src/main/kotlin/query/DocumentQuery.kt | 0 .../src/main/kotlin/query/ExistsQuery.kt | 0 .../src/main/kotlin/query/FindQuery.kt | 0 .../src/main/kotlin/query/PatchQuery.kt | 0 .../src/main/kotlin/query/Query.kt | 0 .../main/kotlin/query/RemoveFieldsQuery.kt | 0 .../src/main/kotlin/query/Where.kt | 0 src/core/src/test/java/module-info.java | 20 + .../core/tests}/java/AutoIdTest.java | 5 +- .../core/tests/java}/ByteIdClass.java | 2 +- .../core/tests}/java/ConfigurationTest.java | 4 +- .../core/tests/java}/CountQueryTest.java | 8 +- .../core/tests/java}/DefinitionQueryTest.java | 8 +- .../core/tests/java}/DeleteQueryTest.java | 8 +- .../core/tests}/java/DialectTest.java | 4 +- .../core/tests}/java/DocumentIndexTest.java | 4 +- .../core/tests/java}/DocumentQueryTest.java | 8 +- .../core/tests/java}/ExistsQueryTest.java | 8 +- .../core/tests}/java/FieldMatchTest.java | 4 +- .../documents/core/tests}/java/FieldTest.java | 6 +- .../core/tests/java}/FindQueryTest.java | 8 +- .../core/tests/java}/IntIdClass.java | 2 +- .../core/tests/java}/LongIdClass.java | 2 +- .../documents/core/tests}/java/OpTest.java | 4 +- .../core/tests}/java/ParameterNameTest.java | 4 +- .../core/tests}/java/ParameterTest.java | 4 +- .../core/tests/java}/ParametersTest.java | 6 +- .../core/tests/java}/PatchQueryTest.java | 8 +- .../core/tests/java}/QueryUtilsTest.java | 6 +- .../tests/java}/RemoveFieldsQueryTest.java | 8 +- .../core/tests/java}/ShortIdClass.java | 2 +- .../core/tests/java}/StringIdClass.java | 2 +- .../documents/core/tests/java}/WhereTest.java | 6 +- .../tests/java/integration/ArrayDocument.java | 41 ++ .../java/integration}/CountFunctions.java | 9 +- .../java/integration}/CustomFunctions.java | 23 +- .../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 | 8 +- .../tests/java/integration/NumIdDocument.java | 32 ++ .../java/integration/PatchFunctions.java | 85 +++ .../java/integration/PostgreSQLCountIT.java} | 9 +- .../java/integration/PostgreSQLCustomIT.java} | 9 +- .../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/PostgreSQLPatchIT.java | 77 +++ .../integration/PostgreSQLRemoveFieldsIT.java | 109 ++++ .../integration/RemoveFieldsFunctions.java | 115 ++++ .../java/integration/SQLiteCountIT.java} | 9 +- .../java/integration/SQLiteCustomIT.java} | 9 +- .../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/SQLitePatchIT.java | 63 +++ .../integration/SQLiteRemoveFieldsIT.java | 79 +++ .../tests/java/integration}/SubDocument.java | 2 +- .../src/test/kotlin/AutoIdTest.kt | 13 +- .../src/test/kotlin/ComparisonTest.kt | 11 +- .../src/test/kotlin/ConfigurationTest.kt | 8 +- .../src/test/kotlin}/CountQueryTest.kt | 13 +- .../src/test/kotlin}/DefinitionQueryTest.kt | 7 +- .../src/test/kotlin}/DeleteQueryTest.kt | 7 +- .../src/test/kotlin/DialectTest.kt | 6 +- .../src/test/kotlin/DocumentIndexTest.kt | 5 +- .../src/test/kotlin}/DocumentQueryTest.kt | 7 +- .../src/test/kotlin}/ExistsQueryTest.kt | 9 +- .../src/test/kotlin/FieldMatchTest.kt | 5 +- .../src/test/kotlin/FieldTest.kt | 6 +- .../src/test/kotlin}/FindQueryTest.kt | 7 +- .../src/test/kotlin}/ForceDialect.kt | 2 +- src/{jvm => core}/src/test/kotlin/OpTest.kt | 5 +- .../src/test/kotlin/ParameterNameTest.kt | 5 +- .../src/test/kotlin/ParameterTest.kt | 7 +- .../src/test/kotlin}/ParametersTest.kt | 6 +- .../src/test/kotlin}/PatchQueryTest.kt | 7 +- .../src/test/kotlin}/QueryTest.kt | 6 +- .../src/test/kotlin}/RemoveFieldsQueryTest.kt | 7 +- src/{kotlin => core}/src/test/kotlin/Types.kt | 16 +- .../src/test/kotlin}/WhereTest.kt | 6 +- .../kotlin/integration/CountFunctions.kt} | 9 +- .../kotlin/integration/CustomFunctions.kt} | 38 +- .../integration/DefinitionFunctions.kt} | 11 +- .../kotlin/integration/DeleteFunctions.kt} | 10 +- .../kotlin/integration/DocumentFunctions.kt} | 29 +- .../kotlin/integration/ExistsFunctions.kt} | 8 +- .../test/kotlin/integration/FindFunctions.kt} | 93 ++-- .../integration}/JacksonDocumentSerializer.kt | 8 +- .../test/kotlin/integration/PatchFunctions.kt | 88 +++ .../src/test/kotlin/integration}/PgDB.kt | 11 +- .../kotlin/integration/PostgreSQLCountIT.kt | 46 ++ .../kotlin/integration/PostgreSQLCustomIT.kt | 47 ++ .../integration/PostgreSQLDefinitionIT.kt} | 15 +- .../kotlin/integration/PostgreSQLDeleteIT.kt | 51 ++ .../integration/PostgreSQLDocumentIT.kt | 56 ++ .../kotlin/integration/PostgreSQLExistsIT.kt | 51 ++ .../kotlin/integration/PostgreSQLFindIT.kt | 171 ++++++ .../kotlin/integration/PostgreSQLPatchIT.kt | 51 ++ .../integration/PostgreSQLRemoveFieldsIT.kt | 71 +++ .../integration/RemoveFieldsFunctions.kt | 107 ++++ .../test/kotlin/integration/SQLiteCountIT.kt} | 17 +- .../test/kotlin/integration/SQLiteCustomIT.kt | 46 ++ .../src/test/kotlin/integration}/SQLiteDB.kt | 10 +- .../kotlin/integration/SQLiteDefinitionIT.kt} | 15 +- .../kotlin/integration/SQLiteDeleteIT.kt} | 19 +- .../kotlin/integration/SQLiteDocumentIT.kt | 56 ++ .../kotlin/integration/SQLiteExistsIT.kt} | 19 +- .../test/kotlin/integration/SQLiteFindIT.kt | 127 +++++ .../test/kotlin/integration/SQLitePatchIT.kt} | 19 +- .../integration/SQLiteRemoveFieldsIT.kt | 55 ++ .../kotlin/integration}/ThrowawayDatabase.kt | 2 +- src/groovy/groovy.iml | 8 + src/groovy/pom.xml | 109 ++++ src/groovy/src/main/groovy/.gitkeep | 0 src/groovy/src/main/java/module-info.java | 3 + ...rg.codehaus.groovy.runtime.ExtensionModule | 3 + .../documents/groovy/tests}/AutoIdTest.groovy | 5 +- .../groovy/tests}/ByteIdClass.groovy | 2 +- .../groovy/tests}/ConfigurationTest.groovy | 4 +- .../groovy/tests}/CountQueryTest.groovy | 7 +- .../groovy/tests}/DefinitionQueryTest.groovy | 7 +- .../groovy/tests}/DeleteQueryTest.groovy | 7 +- .../groovy/tests}/DialectTest.groovy | 4 +- .../groovy/tests}/DocumentIndexTest.groovy | 4 +- .../groovy/tests}/DocumentQueryTest.groovy | 7 +- .../groovy/tests}/FieldMatchTest.groovy | 4 +- .../documents/groovy/tests}/FieldTest.groovy | 5 +- .../groovy/tests}/FindQueryTest.groovy | 7 +- .../groovy/tests/ForceDialect.groovy | 19 + .../documents/groovy/tests}/IntIdClass.groovy | 2 +- .../groovy/tests}/LongIdClass.groovy | 2 +- .../documents/groovy/tests}/OpTest.groovy | 4 +- .../groovy/tests}/ParameterNameTest.groovy | 4 +- .../groovy/tests}/ParameterTest.groovy | 4 +- .../groovy/tests}/ParametersTest.groovy | 7 +- .../groovy/tests}/PatchQueryTest.groovy | 7 +- .../groovy/tests}/QueryUtilsTest.groovy | 5 +- .../tests}/RemoveFieldsQueryTest.groovy | 7 +- .../groovy/tests}/ShortIdClass.groovy | 2 +- .../groovy/tests}/StringIdClass.groovy | 2 +- .../documents/groovy/tests/Types.groovy | 6 + .../documents/groovy/tests}/WhereTest.groovy | 5 +- .../tests/integration/ArrayDocument.groovy | 18 + .../tests/integration/CountFunctions.groovy | 50 ++ .../tests/integration/CustomFunctions.groovy | 69 +++ .../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 | 9 +- .../tests/integration/NumIdDocument.groovy | 11 + .../tests/integration/PatchFunctions.groovy | 75 +++ .../groovy/tests/integration/PgDB.groovy | 50 ++ .../integration/PostgreSQLCountIT.groovy | 53 ++ .../integration/PostgreSQLCustomIT.groovy | 53 ++ .../integration/PostgreSQLDefinitionIT.groovy | 35 ++ .../integration/PostgreSQLDeleteIT.groovy | 59 +++ .../integration/PostgreSQLDocumentIT.groovy | 65 +++ .../integration/PostgreSQLExistsIT.groovy | 59 +++ .../tests/integration/PostgreSQLFindIT.groovy | 203 +++++++ .../integration/PostgreSQLPatchIT.groovy | 59 +++ .../PostgreSQLRemoveFieldsIT.groovy | 83 +++ .../integration/RemoveFieldsFunctions.groovy | 104 ++++ .../tests/integration/SQLiteCountIT.groovy} | 22 +- .../tests/integration/SQLiteCustomIT.groovy | 53 ++ .../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/SQLitePatchIT.groovy | 54 ++ .../integration/SQLiteRemoveFieldsIT.groovy | 66 +++ .../tests/integration}/SubDocument.groovy | 2 +- .../integration/ThrowawayDatabase.groovy | 24 + src/groovy/src/test/java/module-info.java | 15 + .../integration/common/CountFunctions.groovy | 53 -- .../integration/common/CustomFunctions.groovy | 72 --- .../jvm/integration/postgresql/CountIT.groovy | 55 -- .../integration/postgresql/CustomIT.groovy | 55 -- .../jvm/integration/sqlite/CustomIT.groovy | 55 -- .../integration/common/CountFunctions.scala | 47 -- .../integration/common/CustomFunctions.scala | 75 --- .../documents/scala/support/ByteIdClass.scala | 3 - .../documents/scala/support/IntIdClass.scala | 3 - .../documents/scala/support/LongIdClass.scala | 3 - .../scala/support/ShortIdClass.scala | 3 - .../scala/support/StringIdClass.scala | 3 - src/{kotlin => kotlinx}/pom.xml | 41 +- src/kotlinx/src/main/java/module-info.java | 10 + .../src/main/kotlin/Count.kt | 10 +- .../src/main/kotlin/Custom.kt | 4 +- .../src/main/kotlin/Definition.kt | 4 +- .../src/main/kotlin/Delete.kt | 8 +- .../src/main/kotlin/Document.kt | 4 +- .../src/main/kotlin/DocumentConfig.kt | 2 +- .../src/main/kotlin/Exists.kt | 6 +- .../src/main/kotlin/Find.kt | 4 +- .../src/main/kotlin/Parameters.kt | 10 +- .../src/main/kotlin/Patch.kt | 4 +- .../src/main/kotlin/RemoveFields.kt | 6 +- .../src/main/kotlin/Results.kt | 4 +- .../src/main/kotlin/extensions/Connection.kt | 4 +- src/kotlinx/src/test/java/module-info.java | 14 + .../src/test/kotlin/DocumentConfigTest.kt | 5 +- .../src/test/kotlin}/Types.kt | 18 +- .../test/kotlin/integration/CountFunctions.kt | 72 +++ .../kotlin/integration/CustomFunctions.kt | 83 +++ .../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 +++++++++++ .../kotlin/integration/PatchFunctions.kt} | 17 +- .../src/test/kotlin/integration/PgDB.kt | 54 ++ .../kotlin/integration/PostgreSQLCountIT.kt} | 21 +- .../kotlin/integration/PostgreSQLCustomIT.kt} | 21 +- .../integration/PostgreSQLDefinitionIT.kt | 31 ++ .../kotlin/integration/PostgreSQLDeleteIT.kt} | 23 +- .../integration/PostgreSQLDocumentIT.kt} | 25 +- .../kotlin/integration/PostgreSQLExistsIT.kt} | 23 +- .../kotlin/integration/PostgreSQLFindIT.kt} | 71 ++- .../kotlin/integration/PostgreSQLPatchIT.kt} | 23 +- .../integration/PostgreSQLRemoveFieldsIT.kt} | 31 +- .../integration/RemoveFieldsFunctions.kt} | 20 +- .../test/kotlin/integration/SQLiteCountIT.kt | 40 ++ .../kotlin/integration/SQLiteCustomIT.kt} | 21 +- .../src/test/kotlin/integration/SQLiteDB.kt | 35 ++ .../kotlin/integration/SQLiteDefinitionIT.kt | 35 ++ .../test/kotlin/integration/SQLiteDeleteIT.kt | 45 ++ .../kotlin/integration/SQLiteDocumentIT.kt} | 25 +- .../test/kotlin/integration/SQLiteExistsIT.kt | 45 ++ .../test/kotlin/integration/SQLiteFindIT.kt} | 51 +- .../test/kotlin/integration/SQLitePatchIT.kt | 45 ++ .../integration/SQLiteRemoveFieldsIT.kt} | 23 +- .../kotlin/integration/ThrowawayDatabase.kt | 20 + src/pom.xml | 98 ---- src/scala/pom.xml | 94 ++++ src/{jvm/jvm.iml => scala/scala.iml} | 2 +- .../bitbadger/documents/scala/Count.scala | 105 ++++ .../bitbadger/documents/scala/Custom.scala | 210 ++++++++ .../documents/scala/Definition.scala | 72 +++ .../bitbadger/documents/scala/Delete.scala | 98 ++++ .../bitbadger/documents/scala/Document.scala | 72 +++ .../bitbadger/documents/scala/Exists.scala | 106 ++++ .../bitbadger/documents/scala/Find.scala | 372 +++++++++++++ .../documents/scala/Parameters.scala | 87 +++ .../bitbadger/documents/scala/Patch.scala | 120 +++++ .../documents/scala/RemoveFields.scala | 120 +++++ .../bitbadger/documents/scala/Results.scala | 77 +++ .../documents/scala/extensions/package.scala | 499 ++++++++++++++++++ .../documents/scala/tests}/AutoIdTest.scala | 10 +- .../documents/scala/tests/ByteIdClass.scala | 3 + .../scala/tests}/ConfigurationTest.scala | 9 +- .../scala/tests}/CountQueryTest.scala | 11 +- .../scala/tests}/DefinitionQueryTest.scala | 11 +- .../scala/tests}/DeleteQueryTest.scala | 11 +- .../documents/scala/tests}/DialectTest.scala | 10 +- .../scala/tests}/DocumentIndexTest.scala | 7 +- .../scala/tests}/DocumentQueryTest.scala | 11 +- .../scala/tests}/ExistsQueryTest.scala | 11 +- .../scala/tests}/FieldMatchTest.scala | 9 +- .../documents/scala/tests}/FieldTest.scala | 14 +- .../scala/tests}/FindQueryTest.scala | 11 +- .../documents/scala/tests/ForceDialect.scala | 17 + .../documents/scala/tests/IntIdClass.scala | 3 + .../documents/scala/tests/LongIdClass.scala | 3 + .../documents/scala/tests}/OpTest.scala | 7 +- .../scala/tests}/ParameterNameTest.scala | 7 +- .../scala/tests}/ParameterTest.scala | 7 +- .../scala/tests}/ParametersTest.scala | 31 +- .../scala/tests}/PatchQueryTest.scala | 11 +- .../scala/tests}/QueryUtilsTest.scala | 10 +- .../scala/tests}/RemoveFieldsQueryTest.scala | 11 +- .../documents/scala/tests/ShortIdClass.scala | 3 + .../documents/scala/tests/StringIdClass.scala | 3 + .../documents/scala/tests}/WhereTest.scala | 10 +- .../tests/integration/ArrayDocument.scala | 11 + .../tests/integration/CountFunctions.scala | 42 ++ .../tests/integration/CustomFunctions.scala | 52 ++ .../integration/DefinitionFunctions.scala | 36 ++ .../tests/integration/DeleteFunctions.scala | 56 ++ .../tests/integration/DocumentFunctions.scala | 111 ++++ .../tests/integration/ExistsFunctions.scala | 46 ++ .../tests/integration/FindFunctions.scala | 217 ++++++++ .../JacksonDocumentSerializer.scala | 17 + .../tests/integration}/JsonDocument.scala | 9 +- .../tests/integration/NumIdDocument.scala | 3 + .../tests/integration/PatchFunctions.scala | 65 +++ .../scala/tests/integration/PgDB.scala | 45 ++ .../integration/PostgreSQLCountIT.scala} | 9 +- .../integration/PostgreSQLCustomIT.scala} | 9 +- .../integration/PostgreSQLDefinitionIT.scala | 31 ++ .../integration/PostgreSQLDeleteIT.scala | 51 ++ .../integration/PostgreSQLDocumentIT.scala | 56 ++ .../integration/PostgreSQLExistsIT.scala | 51 ++ .../tests/integration/PostgreSQLFindIT.scala | 172 ++++++ .../tests/integration/PostgreSQLPatchIT.scala | 51 ++ .../PostgreSQLRemoveFieldsIT.scala | 71 +++ .../integration/RemoveFieldsFunctions.scala | 92 ++++ .../tests/integration/SQLiteCountIT.scala} | 11 +- .../tests/integration/SQLiteCustomIT.scala} | 9 +- .../scala/tests/integration/SQLiteDB.scala | 30 ++ .../integration/SQLiteDefinitionIT.scala | 30 ++ .../tests/integration/SQLiteDeleteIT.scala | 43 ++ .../tests/integration/SQLiteDocumentIT.scala | 56 ++ .../tests/integration/SQLiteExistsIT.scala | 43 ++ .../tests/integration/SQLiteFindIT.scala | 127 +++++ .../tests/integration/SQLitePatchIT.scala | 44 ++ .../integration/SQLiteRemoveFieldsIT.scala | 57 ++ .../tests/integration}/SubDocument.scala | 2 +- .../tests/integration/ThrowawayDatabase.scala | 28 + .../documents/scala/tests/package.scala | 6 + 363 files changed, 11626 insertions(+), 1574 deletions(-) create mode 100644 java.iml create mode 100644 src/core/core.iml rename src/{jvm => core}/pom.xml (54%) create mode 100644 src/core/src/main/java/module-info.java rename src/{jvm => core}/src/main/kotlin/AutoId.kt (100%) rename src/{jvm => core}/src/main/kotlin/Comparison.kt (100%) rename src/{jvm => core}/src/main/kotlin/Configuration.kt (100%) rename src/{jvm => core}/src/main/kotlin/Dialect.kt (100%) rename src/{jvm => core}/src/main/kotlin/DocumentException.kt (100%) rename src/{jvm => core}/src/main/kotlin/DocumentIndex.kt (100%) rename src/{jvm => core}/src/main/kotlin/DocumentSerializer.kt (100%) rename src/{jvm => core}/src/main/kotlin/Field.kt (100%) rename src/{jvm => core}/src/main/kotlin/FieldFormat.kt (100%) rename src/{jvm => core}/src/main/kotlin/FieldMatch.kt (100%) rename src/{jvm => core}/src/main/kotlin/Op.kt (100%) rename src/{jvm => core}/src/main/kotlin/Parameter.kt (100%) rename src/{jvm => core}/src/main/kotlin/ParameterName.kt (100%) rename src/{jvm => core}/src/main/kotlin/ParameterType.kt (100%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Count.kt (94%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Custom.kt (85%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Definition.kt (88%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Delete.kt (90%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Document.kt (92%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/DocumentConfig.kt (86%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Exists.kt (96%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Find.kt (92%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/NullDocumentSerializer.kt (94%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Parameters.kt (99%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Patch.kt (92%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/RemoveFields.kt (94%) rename src/{jvm/src/main/kotlin/jvm => core/src/main/kotlin/java}/Results.kt (98%) rename src/{jvm/src/main/kotlin => core/src/main/kotlin/java}/extensions/Connection.kt (99%) rename src/{jvm => core}/src/main/kotlin/query/CountQuery.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/DefinitionQuery.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/DeleteQuery.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/DocumentQuery.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/ExistsQuery.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/FindQuery.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/PatchQuery.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/Query.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/RemoveFieldsQuery.kt (100%) rename src/{jvm => core}/src/main/kotlin/query/Where.kt (100%) create mode 100644 src/core/src/test/java/module-info.java rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/AutoIdTest.java (98%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/support => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/ByteIdClass.java (80%) rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/ConfigurationTest.java (94%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/CountQueryTest.java (92%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/DefinitionQueryTest.java (95%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/DeleteQueryTest.java (93%) rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/DialectTest.java (94%) rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/DocumentIndexTest.java (87%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/DocumentQueryTest.java (95%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/ExistsQueryTest.java (93%) rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/FieldMatchTest.java (85%) rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/FieldTest.java (99%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/FindQueryTest.java (93%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/support => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/IntIdClass.java (80%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/support => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/LongIdClass.java (80%) rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/OpTest.java (96%) rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/ParameterNameTest.java (92%) rename src/{jvm/src/test/java/solutions/bitbadger/documents => core/src/test/java/solutions/bitbadger/documents/core/tests}/java/ParameterTest.java (93%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/jvm => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/ParametersTest.java (97%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/PatchQueryTest.java (93%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/QueryUtilsTest.java (97%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/RemoveFieldsQueryTest.java (94%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/support => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/ShortIdClass.java (81%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/support => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/StringIdClass.java (81%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/query => core/src/test/java/solutions/bitbadger/documents/core/tests/java}/WhereTest.java (97%) create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/ArrayDocument.java rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common => core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration}/CountFunctions.java (87%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common => core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration}/CustomFunctions.java (83%) 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 rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/support => core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration}/JsonDocument.java (88%) 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 rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java => core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCountIT.java} (86%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CustomIT.java => core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java} (85%) 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/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 rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java => core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCountIT.java} (84%) rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CustomIT.java => core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java} (86%) 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/SQLitePatchIT.java create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteRemoveFieldsIT.java rename src/{jvm/src/test/java/solutions/bitbadger/documents/java/support => core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration}/SubDocument.java (87%) rename src/{jvm => core}/src/test/kotlin/AutoIdTest.kt (94%) rename src/{jvm => core}/src/test/kotlin/ComparisonTest.kt (95%) rename src/{jvm => core}/src/test/kotlin/ConfigurationTest.kt (80%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/CountQueryTest.kt (87%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/DefinitionQueryTest.kt (95%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/DeleteQueryTest.kt (93%) rename src/{jvm => core}/src/test/kotlin/DialectTest.kt (87%) rename src/{jvm => core}/src/test/kotlin/DocumentIndexTest.kt (78%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/DocumentQueryTest.kt (96%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/ExistsQueryTest.kt (91%) rename src/{jvm => core}/src/test/kotlin/FieldMatchTest.kt (76%) rename src/{jvm => core}/src/test/kotlin/FieldTest.kt (99%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/FindQueryTest.kt (93%) rename src/{jvm/src/test/kotlin/support => core/src/test/kotlin}/ForceDialect.kt (90%) rename src/{jvm => core}/src/test/kotlin/OpTest.kt (94%) rename src/{jvm => core}/src/test/kotlin/ParameterNameTest.kt (87%) rename src/{jvm => core}/src/test/kotlin/ParameterTest.kt (83%) rename src/{jvm/src/test/kotlin/jvm => core/src/test/kotlin}/ParametersTest.kt (97%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/PatchQueryTest.kt (93%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/QueryTest.kt (97%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/RemoveFieldsQueryTest.kt (94%) rename src/{kotlin => core}/src/test/kotlin/Types.kt (75%) rename src/{jvm/src/test/kotlin/query => core/src/test/kotlin}/WhereTest.kt (97%) rename src/{jvm/src/test/kotlin/jvm/integration/common/Count.kt => core/src/test/kotlin/integration/CountFunctions.kt} (88%) rename src/{jvm/src/test/kotlin/jvm/integration/common/Custom.kt => core/src/test/kotlin/integration/CustomFunctions.kt} (73%) rename src/{jvm/src/test/kotlin/jvm/integration/common/Definition.kt => core/src/test/kotlin/integration/DefinitionFunctions.kt} (84%) rename src/{jvm/src/test/kotlin/jvm/integration/common/Delete.kt => core/src/test/kotlin/integration/DeleteFunctions.kt} (90%) rename src/{jvm/src/test/kotlin/jvm/integration/common/Document.kt => core/src/test/kotlin/integration/DocumentFunctions.kt} (86%) rename src/{jvm/src/test/kotlin/jvm/integration/common/Exists.kt => core/src/test/kotlin/integration/ExistsFunctions.kt} (91%) rename src/{jvm/src/test/kotlin/jvm/integration/common/Find.kt => core/src/test/kotlin/integration/FindFunctions.kt} (79%) rename src/{jvm/src/test/kotlin/support => core/src/test/kotlin/integration}/JacksonDocumentSerializer.kt (68%) create mode 100644 src/core/src/test/kotlin/integration/PatchFunctions.kt rename src/{jvm/src/test/kotlin/jvm/integration/postgresql => core/src/test/kotlin/integration}/PgDB.kt (82%) create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt => core/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt} (58%) 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/PostgreSQLPatchIT.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt create mode 100644 src/core/src/test/kotlin/integration/RemoveFieldsFunctions.kt rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt => core/src/test/kotlin/integration/SQLiteCountIT.kt} (59%) create mode 100644 src/core/src/test/kotlin/integration/SQLiteCustomIT.kt rename src/{jvm/src/test/kotlin/jvm/integration/sqlite => core/src/test/kotlin/integration}/SQLiteDB.kt (74%) rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt => core/src/test/kotlin/integration/SQLiteDefinitionIT.kt} (68%) rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt => core/src/test/kotlin/integration/SQLiteDeleteIT.kt} (59%) create mode 100644 src/core/src/test/kotlin/integration/SQLiteDocumentIT.kt rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt => core/src/test/kotlin/integration/SQLiteExistsIT.kt} (61%) create mode 100644 src/core/src/test/kotlin/integration/SQLiteFindIT.kt rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt => core/src/test/kotlin/integration/SQLitePatchIT.kt} (60%) create mode 100644 src/core/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt rename src/{jvm/src/test/kotlin/support => core/src/test/kotlin/integration}/ThrowawayDatabase.kt (88%) 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/resources/META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/AutoIdTest.groovy (97%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/ByteIdClass.groovy (62%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/ConfigurationTest.groovy (93%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/CountQueryTest.groovy (91%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/DefinitionQueryTest.groovy (95%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/DeleteQueryTest.groovy (92%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/DialectTest.groovy (94%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/DocumentIndexTest.groovy (87%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/DocumentQueryTest.groovy (95%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/FieldMatchTest.groovy (86%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/FieldTest.groovy (99%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/FindQueryTest.groovy (93%) create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ForceDialect.groovy rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/IntIdClass.groovy (61%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/LongIdClass.groovy (62%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/OpTest.groovy (96%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/ParameterNameTest.groovy (92%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/ParameterTest.groovy (93%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/ParametersTest.groovy (96%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/PatchQueryTest.groovy (92%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/QueryUtilsTest.groovy (97%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/RemoveFieldsQueryTest.groovy (94%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/ShortIdClass.groovy (63%) rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/StringIdClass.groovy (64%) create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/Types.groovy rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests}/WhereTest.groovy (97%) 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 rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration}/JsonDocument.groovy (71%) 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/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 rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CountIT.groovy => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCountIT.groovy} (61%) 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/SQLitePatchIT.groovy create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteRemoveFieldsIT.groovy rename src/{jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support => groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration}/SubDocument.groovy (71%) 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 delete mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy delete mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CustomFunctions.groovy delete mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy delete mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CustomIT.groovy delete mode 100644 src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CustomIT.groovy delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CountFunctions.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CustomFunctions.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ByteIdClass.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/IntIdClass.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/LongIdClass.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ShortIdClass.scala delete mode 100644 src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/StringIdClass.scala rename src/{kotlin => kotlinx}/pom.xml (73%) create mode 100644 src/kotlinx/src/main/java/module-info.java rename src/{kotlin => kotlinx}/src/main/kotlin/Count.kt (93%) rename src/{kotlin => kotlinx}/src/main/kotlin/Custom.kt (97%) rename src/{kotlin => kotlinx}/src/main/kotlin/Definition.kt (95%) rename src/{kotlin => kotlinx}/src/main/kotlin/Delete.kt (94%) rename src/{kotlin => kotlinx}/src/main/kotlin/Document.kt (97%) rename src/{kotlin => kotlinx}/src/main/kotlin/DocumentConfig.kt (94%) rename src/{kotlin => kotlinx}/src/main/kotlin/Exists.kt (96%) rename src/{kotlin => kotlinx}/src/main/kotlin/Find.kt (99%) rename src/{kotlin => kotlinx}/src/main/kotlin/Parameters.kt (91%) rename src/{kotlin => kotlinx}/src/main/kotlin/Patch.kt (98%) rename src/{kotlin => kotlinx}/src/main/kotlin/RemoveFields.kt (96%) rename src/{kotlin => kotlinx}/src/main/kotlin/Results.kt (95%) rename src/{kotlin => kotlinx}/src/main/kotlin/extensions/Connection.kt (99%) create mode 100644 src/kotlinx/src/test/java/module-info.java rename src/{kotlin => kotlinx}/src/test/kotlin/DocumentConfigTest.kt (81%) rename src/{jvm/src/test/kotlin/support => kotlinx/src/test/kotlin}/Types.kt (81%) 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 rename src/{jvm/src/test/kotlin/jvm/integration/common/Patch.kt => kotlinx/src/test/kotlin/integration/PatchFunctions.kt} (85%) create mode 100644 src/kotlinx/src/test/kotlin/integration/PgDB.kt rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt => kotlinx/src/test/kotlin/integration/PostgreSQLCountIT.kt} (65%) rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt => kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt} (62%) create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt => kotlinx/src/test/kotlin/integration/PostgreSQLDeleteIT.kt} (63%) rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt => kotlinx/src/test/kotlin/integration/PostgreSQLDocumentIT.kt} (62%) rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt => kotlinx/src/test/kotlin/integration/PostgreSQLExistsIT.kt} (64%) rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt => kotlinx/src/test/kotlin/integration/PostgreSQLFindIT.kt} (66%) rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt => kotlinx/src/test/kotlin/integration/PostgreSQLPatchIT.kt} (63%) rename src/{jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt => kotlinx/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt} (63%) rename src/{jvm/src/test/kotlin/jvm/integration/common/RemoveFields.kt => kotlinx/src/test/kotlin/integration/RemoveFieldsFunctions.kt} (86%) create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteCountIT.kt rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt => kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt} (61%) 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 rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt => kotlinx/src/test/kotlin/integration/SQLiteDocumentIT.kt} (61%) create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteExistsIT.kt rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt => kotlinx/src/test/kotlin/integration/SQLiteFindIT.kt} (62%) create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLitePatchIT.kt rename src/{jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt => kotlinx/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt} (67%) create mode 100644 src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt delete mode 100644 src/pom.xml create mode 100644 src/scala/pom.xml rename src/{jvm/jvm.iml => scala/scala.iml} (76%) create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Count.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Custom.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Definition.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Delete.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Document.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Exists.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Find.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Parameters.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Patch.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/RemoveFields.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/Results.scala create mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/extensions/package.scala rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/AutoIdTest.scala (95%) create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ByteIdClass.scala rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/ConfigurationTest.scala (87%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/CountQueryTest.scala (90%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/DefinitionQueryTest.scala (94%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/DeleteQueryTest.scala (91%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/DialectTest.scala (91%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/DocumentIndexTest.scala (80%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/DocumentQueryTest.scala (93%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/ExistsQueryTest.scala (90%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/FieldMatchTest.scala (73%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/FieldTest.scala (98%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/FindQueryTest.scala (92%) create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ForceDialect.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/IntIdClass.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/LongIdClass.scala rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/OpTest.scala (95%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/ParameterNameTest.scala (89%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/ParameterTest.scala (91%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/ParametersTest.scala (83%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/PatchQueryTest.scala (90%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/QueryUtilsTest.scala (96%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/RemoveFieldsQueryTest.scala (91%) create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ShortIdClass.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/StringIdClass.scala rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/query => scala/src/test/scala/solutions/bitbadger/documents/scala/tests}/WhereTest.scala (95%) create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ArrayDocument.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CountFunctions.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CustomFunctions.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DefinitionFunctions.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DeleteFunctions.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DocumentFunctions.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ExistsFunctions.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/FindFunctions.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JacksonDocumentSerializer.scala rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/support => scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration}/JsonDocument.scala (64%) create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/NumIdDocument.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PatchFunctions.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PgDB.scala rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CountIT.scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCountIT.scala} (81%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CustomIT.scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCustomIT.scala} (79%) create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDefinitionIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDeleteIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDocumentIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLExistsIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLFindIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLPatchIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLRemoveFieldsIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/RemoveFieldsFunctions.scala rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CountIT.scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCountIT.scala} (78%) rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CustomIT.scala => scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCustomIT.scala} (80%) create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDB.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDefinitionIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDeleteIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDocumentIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteExistsIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteFindIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLitePatchIT.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteRemoveFieldsIT.scala rename src/{jvm/src/test/scala/solutions/bitbadger/documents/scala/support => scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration}/SubDocument.scala (50%) create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ThrowawayDatabase.scala create mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/package.scala diff --git a/.idea/compiler.xml b/.idea/compiler.xml index afb5348..ea58415 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -6,13 +6,20 @@ - - + + + + - + + + + + + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 3a6db4a..66a447d 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,13 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index c22b6fa..d1e0db8 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,16 @@ + + + + + + - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 46a3f3f..a3d569e 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,9 @@ - + + + \ No newline at end of file diff --git a/.idea/scala_compiler.xml b/.idea/scala_compiler.xml index 6cdd90a..96c87c5 100644 --- a/.idea/scala_compiler.xml +++ b/.idea/scala_compiler.xml @@ -2,6 +2,6 @@ \ No newline at end of file diff --git a/java.iml b/java.iml new file mode 100644 index 0000000..0ae9cfd --- /dev/null +++ b/java.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index b32d087..7dfbc5a 100644 --- a/pom.xml +++ b/pom.xml @@ -38,13 +38,57 @@ UTF-8 official - 11 - 2.1.0 + 17 + ${java.version} + 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 - src + ./src/core + ./src/groovy + ./src/kotlinx + ./src/scala + + + 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/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/jvm/pom.xml b/src/core/pom.xml similarity index 54% rename from src/jvm/pom.xml rename to src/core/pom.xml index 287370a..fd79794 100644 --- a/src/jvm/pom.xml +++ b/src/core/pom.xml @@ -3,61 +3,27 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - solutions.bitbadger.documents - jvm - 4.0.0-alpha1-SNAPSHOT - jar - solutions.bitbadger documents 4.0.0-alpha1-SNAPSHOT + ../../pom.xml + solutions.bitbadger.documents + core + ${project.groupId}:${project.artifactId} - Expose a document store interface for PostgreSQL and SQLite (Standard JVM Library) + Expose a document store interface for PostgreSQL and SQLite (Core Library) https://bitbadger.solutions/open-source/relational-documents/jvm/ - - 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 - - - 2.18.2 + UTF-8 + official + 1.8 - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - test - - - org.scala-lang - scala3-library_3 - ${scala.version} - test - - - org.apache.groovy - groovy-test - ${groovy.version} - test - - - org.apache.groovy - groovy-test-junit5 - ${groovy.version} - test - - - - src/main/kotlin org.jetbrains.kotlin @@ -72,6 +38,7 @@ + ${project.basedir}/src/main/java ${project.basedir}/src/main/kotlin @@ -84,62 +51,20 @@ - ${project.basedir}/src/test/kotlin ${project.basedir}/src/test/java - ${project.basedir}/src/test/scala + ${project.basedir}/src/test/kotlin - - net.alchim31.maven - scala-maven-plugin - 4.9.2 - - - - testCompile - - - - - ${java.version} - ${java.version} - - - - org.codehaus.gmaven - gmaven-plugin - 1.5 - - ${java.version} - - - - - 2.0 - - - generateTestStubs - testCompile - - - - maven-surefire-plugin - 2.22.2 - - - --add-opens=java.base/java.lang=ALL-UNNAMED - --add-opens=java.base/java.util=ALL-UNNAMED - - + ${surefire.version} maven-failsafe-plugin - 2.22.2 + ${failsafe.version} @@ -160,4 +85,24 @@ - + + + + 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/jvm/src/main/kotlin/AutoId.kt b/src/core/src/main/kotlin/AutoId.kt similarity index 100% rename from src/jvm/src/main/kotlin/AutoId.kt rename to src/core/src/main/kotlin/AutoId.kt diff --git a/src/jvm/src/main/kotlin/Comparison.kt b/src/core/src/main/kotlin/Comparison.kt similarity index 100% rename from src/jvm/src/main/kotlin/Comparison.kt rename to src/core/src/main/kotlin/Comparison.kt diff --git a/src/jvm/src/main/kotlin/Configuration.kt b/src/core/src/main/kotlin/Configuration.kt similarity index 100% rename from src/jvm/src/main/kotlin/Configuration.kt rename to src/core/src/main/kotlin/Configuration.kt diff --git a/src/jvm/src/main/kotlin/Dialect.kt b/src/core/src/main/kotlin/Dialect.kt similarity index 100% rename from src/jvm/src/main/kotlin/Dialect.kt rename to src/core/src/main/kotlin/Dialect.kt diff --git a/src/jvm/src/main/kotlin/DocumentException.kt b/src/core/src/main/kotlin/DocumentException.kt similarity index 100% rename from src/jvm/src/main/kotlin/DocumentException.kt rename to src/core/src/main/kotlin/DocumentException.kt diff --git a/src/jvm/src/main/kotlin/DocumentIndex.kt b/src/core/src/main/kotlin/DocumentIndex.kt similarity index 100% rename from src/jvm/src/main/kotlin/DocumentIndex.kt rename to src/core/src/main/kotlin/DocumentIndex.kt diff --git a/src/jvm/src/main/kotlin/DocumentSerializer.kt b/src/core/src/main/kotlin/DocumentSerializer.kt similarity index 100% rename from src/jvm/src/main/kotlin/DocumentSerializer.kt rename to src/core/src/main/kotlin/DocumentSerializer.kt diff --git a/src/jvm/src/main/kotlin/Field.kt b/src/core/src/main/kotlin/Field.kt similarity index 100% rename from src/jvm/src/main/kotlin/Field.kt rename to src/core/src/main/kotlin/Field.kt diff --git a/src/jvm/src/main/kotlin/FieldFormat.kt b/src/core/src/main/kotlin/FieldFormat.kt similarity index 100% rename from src/jvm/src/main/kotlin/FieldFormat.kt rename to src/core/src/main/kotlin/FieldFormat.kt diff --git a/src/jvm/src/main/kotlin/FieldMatch.kt b/src/core/src/main/kotlin/FieldMatch.kt similarity index 100% rename from src/jvm/src/main/kotlin/FieldMatch.kt rename to src/core/src/main/kotlin/FieldMatch.kt diff --git a/src/jvm/src/main/kotlin/Op.kt b/src/core/src/main/kotlin/Op.kt similarity index 100% rename from src/jvm/src/main/kotlin/Op.kt rename to src/core/src/main/kotlin/Op.kt diff --git a/src/jvm/src/main/kotlin/Parameter.kt b/src/core/src/main/kotlin/Parameter.kt similarity index 100% rename from src/jvm/src/main/kotlin/Parameter.kt rename to src/core/src/main/kotlin/Parameter.kt diff --git a/src/jvm/src/main/kotlin/ParameterName.kt b/src/core/src/main/kotlin/ParameterName.kt similarity index 100% rename from src/jvm/src/main/kotlin/ParameterName.kt rename to src/core/src/main/kotlin/ParameterName.kt diff --git a/src/jvm/src/main/kotlin/ParameterType.kt b/src/core/src/main/kotlin/ParameterType.kt similarity index 100% rename from src/jvm/src/main/kotlin/ParameterType.kt rename to src/core/src/main/kotlin/ParameterType.kt diff --git a/src/jvm/src/main/kotlin/jvm/Count.kt b/src/core/src/main/kotlin/java/Count.kt similarity index 94% rename from src/jvm/src/main/kotlin/jvm/Count.kt rename to src/core/src/main/kotlin/java/Count.kt index 25f87e3..bd599e9 100644 --- a/src/jvm/src/main/kotlin/jvm/Count.kt +++ b/src/core/src/main/kotlin/java/Count.kt @@ -1,8 +1,7 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.* import solutions.bitbadger.documents.query.CountQuery -import solutions.bitbadger.documents.extensions.customScalar import java.sql.Connection import kotlin.jvm.Throws @@ -22,7 +21,7 @@ object Count { @Throws(DocumentException::class) @JvmStatic fun all(tableName: String, conn: Connection) = - conn.customScalar(CountQuery.all(tableName), listOf(), Long::class.java, Results::toCount) + Custom.scalar(CountQuery.all(tableName), listOf(), Long::class.java, conn, Results::toCount) /** * Count all documents in the table @@ -56,10 +55,11 @@ object Count { conn: Connection ): Long { val named = Parameters.nameFields(fields) - return conn.customScalar( + return Custom.scalar( CountQuery.byFields(tableName, named, howMatched), Parameters.addFields(named), Long::class.java, + conn, Results::toCount ) } @@ -91,10 +91,11 @@ object Count { @Throws(DocumentException::class) @JvmStatic fun byContains(tableName: String, criteria: TContains, conn: Connection) = - conn.customScalar( + Custom.scalar( CountQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Long::class.java, + conn, Results::toCount ) @@ -123,10 +124,11 @@ object Count { @Throws(DocumentException::class) @JvmStatic fun byJsonPath(tableName: String, path: String, conn: Connection) = - conn.customScalar( + Custom.scalar( CountQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), Long::class.java, + conn, Results::toCount ) diff --git a/src/jvm/src/main/kotlin/jvm/Custom.kt b/src/core/src/main/kotlin/java/Custom.kt similarity index 85% rename from src/jvm/src/main/kotlin/jvm/Custom.kt rename to src/core/src/main/kotlin/java/Custom.kt index d438b64..f341153 100644 --- a/src/jvm/src/main/kotlin/jvm/Custom.kt +++ b/src/core/src/main/kotlin/java/Custom.kt @@ -1,10 +1,12 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Parameter import java.sql.Connection import java.sql.ResultSet +import java.sql.SQLException +import java.util.* import kotlin.jvm.Throws object Custom { @@ -57,7 +59,7 @@ object Custom { * @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 The document if one matches the query, `null` otherwise + * @return An `Optional` value, with the document if one matches the query * @throws DocumentException If parameters are invalid */ @Throws(DocumentException::class) @@ -68,7 +70,7 @@ object Custom { clazz: Class, conn: Connection, mapFunc: (ResultSet, Class) -> TDoc - ) = list("$query LIMIT 1", parameters, clazz, conn, mapFunc).singleOrNull() + ) = Optional.ofNullable(list("$query LIMIT 1", parameters, clazz, conn, mapFunc).singleOrNull()) /** * Execute a query that returns one or no results @@ -95,12 +97,16 @@ object Custom { * @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 + * @throws DocumentException If parameters are invalid or if the query fails */ @Throws(DocumentException::class) @JvmStatic fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) { - Parameters.apply(conn, query, parameters).use { it.executeUpdate() } + try { + Parameters.apply(conn, query, parameters).use { it.executeUpdate() } + } catch (ex: SQLException) { + throw DocumentException("Unable to execute non-query: ${ex.message}", ex) + } } /** @@ -108,7 +114,7 @@ object Custom { * * @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, or if parameters are invalid + * @throws DocumentException If no connection string has been set, if parameters are invalid, or if the query fails */ @Throws(DocumentException::class) @JvmStatic @@ -124,7 +130,7 @@ object Custom { * @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 + * @throws DocumentException If parameters are invalid or if the query fails */ @Throws(DocumentException::class) @JvmStatic @@ -135,9 +141,13 @@ object Custom { conn: Connection, mapFunc: (ResultSet, Class) -> T ) = Parameters.apply(conn, query, parameters).use { stmt -> - stmt.executeQuery().use { rs -> - rs.next() - mapFunc(rs, clazz) + try { + stmt.executeQuery().use { rs -> + rs.next() + mapFunc(rs, clazz) + } + } catch (ex: SQLException) { + throw DocumentException("Unable to retrieve scalar value: ${ex.message}", ex) } } diff --git a/src/jvm/src/main/kotlin/jvm/Definition.kt b/src/core/src/main/kotlin/java/Definition.kt similarity index 88% rename from src/jvm/src/main/kotlin/jvm/Definition.kt rename to src/core/src/main/kotlin/java/Definition.kt index 585e7d9..baf7b9b 100644 --- a/src/jvm/src/main/kotlin/jvm/Definition.kt +++ b/src/core/src/main/kotlin/java/Definition.kt @@ -1,9 +1,8 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.DocumentIndex -import solutions.bitbadger.documents.extensions.customNonQuery import solutions.bitbadger.documents.query.DefinitionQuery import java.sql.Connection import kotlin.jvm.Throws @@ -24,8 +23,8 @@ object Definition { @JvmStatic fun ensureTable(tableName: String, conn: Connection) = Configuration.dialect("ensure $tableName exists").let { - conn.customNonQuery(DefinitionQuery.ensureTable(tableName, it)) - conn.customNonQuery(DefinitionQuery.ensureKey(tableName, it)) + Custom.nonQuery(DefinitionQuery.ensureTable(tableName, it), conn = conn) + Custom.nonQuery(DefinitionQuery.ensureKey(tableName, it), conn = conn) } /** @@ -51,7 +50,7 @@ object Definition { @Throws(DocumentException::class) @JvmStatic fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection, conn: Connection) = - conn.customNonQuery(DefinitionQuery.ensureIndexOn(tableName, indexName, fields)) + Custom.nonQuery(DefinitionQuery.ensureIndexOn(tableName, indexName, fields), conn = conn) /** * Create an index on field(s) within documents in the specified table if necessary @@ -77,7 +76,7 @@ object Definition { @Throws(DocumentException::class) @JvmStatic fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) = - conn.customNonQuery(DefinitionQuery.ensureDocumentIndexOn(tableName, indexType)) + Custom.nonQuery(DefinitionQuery.ensureDocumentIndexOn(tableName, indexType), conn = conn) /** * Create a document index on a table (PostgreSQL only) diff --git a/src/jvm/src/main/kotlin/jvm/Delete.kt b/src/core/src/main/kotlin/java/Delete.kt similarity index 90% rename from src/jvm/src/main/kotlin/jvm/Delete.kt rename to src/core/src/main/kotlin/java/Delete.kt index 7cba82a..969ac5e 100644 --- a/src/jvm/src/main/kotlin/jvm/Delete.kt +++ b/src/core/src/main/kotlin/java/Delete.kt @@ -1,7 +1,6 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.extensions.customNonQuery import solutions.bitbadger.documents.query.DeleteQuery import java.sql.Connection import kotlin.jvm.Throws @@ -22,9 +21,10 @@ object Delete { @Throws(DocumentException::class) @JvmStatic fun byId(tableName: String, docId: TKey, conn: Connection) = - conn.customNonQuery( + Custom.nonQuery( DeleteQuery.byId(tableName, docId), - Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))) + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), + conn ) /** @@ -53,7 +53,7 @@ object Delete { @JvmOverloads fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { val named = Parameters.nameFields(fields) - conn.customNonQuery(DeleteQuery.byFields(tableName, named, howMatched), Parameters.addFields(named)) + Custom.nonQuery(DeleteQuery.byFields(tableName, named, howMatched), Parameters.addFields(named), conn) } /** @@ -81,7 +81,7 @@ object Delete { @Throws(DocumentException::class) @JvmStatic fun byContains(tableName: String, criteria: TContains, conn: Connection) = - conn.customNonQuery(DeleteQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) + Custom.nonQuery(DeleteQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), conn) /** * Delete documents using a JSON containment query (PostgreSQL only) @@ -106,7 +106,7 @@ object Delete { @Throws(DocumentException::class) @JvmStatic fun byJsonPath(tableName: String, path: String, conn: Connection) = - conn.customNonQuery(DeleteQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path))) + Custom.nonQuery(DeleteQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), conn) /** * Delete documents using a JSON Path match query (PostgreSQL only) diff --git a/src/jvm/src/main/kotlin/jvm/Document.kt b/src/core/src/main/kotlin/java/Document.kt similarity index 92% rename from src/jvm/src/main/kotlin/jvm/Document.kt rename to src/core/src/main/kotlin/java/Document.kt index 69c4e64..018b49c 100644 --- a/src/jvm/src/main/kotlin/jvm/Document.kt +++ b/src/core/src/main/kotlin/java/Document.kt @@ -1,10 +1,9 @@ -package solutions.bitbadger.documents.jvm +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.extensions.customNonQuery import solutions.bitbadger.documents.query.DocumentQuery import solutions.bitbadger.documents.query.Where import solutions.bitbadger.documents.query.statementWhere @@ -33,7 +32,7 @@ object Document { } else { DocumentQuery.insert(tableName, strategy) } - conn.customNonQuery(query, listOf(Parameters.json(":data", document))) + Custom.nonQuery(query, listOf(Parameters.json(":data", document)), conn) } /** @@ -60,7 +59,7 @@ object Document { @Throws(DocumentException::class) @JvmStatic fun save(tableName: String, document: TDoc, conn: Connection) = - conn.customNonQuery(DocumentQuery.save(tableName), listOf(Parameters.json(":data", document))) + 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") @@ -86,12 +85,13 @@ object Document { @Throws(DocumentException::class) @JvmStatic fun update(tableName: String, docId: TKey, document: TDoc, conn: Connection) = - conn.customNonQuery( + 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 ) /** diff --git a/src/jvm/src/main/kotlin/jvm/DocumentConfig.kt b/src/core/src/main/kotlin/java/DocumentConfig.kt similarity index 86% rename from src/jvm/src/main/kotlin/jvm/DocumentConfig.kt rename to src/core/src/main/kotlin/java/DocumentConfig.kt index d8b39fd..f817b4b 100644 --- a/src/jvm/src/main/kotlin/jvm/DocumentConfig.kt +++ b/src/core/src/main/kotlin/java/DocumentConfig.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.DocumentSerializer diff --git a/src/jvm/src/main/kotlin/jvm/Exists.kt b/src/core/src/main/kotlin/java/Exists.kt similarity index 96% rename from src/jvm/src/main/kotlin/jvm/Exists.kt rename to src/core/src/main/kotlin/java/Exists.kt index 990129e..bbfa151 100644 --- a/src/jvm/src/main/kotlin/jvm/Exists.kt +++ b/src/core/src/main/kotlin/java/Exists.kt @@ -1,7 +1,6 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.extensions.customScalar import solutions.bitbadger.documents.query.ExistsQuery import java.sql.Connection import kotlin.jvm.Throws @@ -23,10 +22,11 @@ object Exists { @Throws(DocumentException::class) @JvmStatic fun byId(tableName: String, docId: TKey, conn: Connection) = - conn.customScalar( + Custom.scalar( ExistsQuery.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), Boolean::class.java, + conn, Results::toExists ) @@ -63,10 +63,11 @@ object Exists { conn: Connection ): Boolean { val named = Parameters.nameFields(fields) - return conn.customScalar( + return Custom.scalar( ExistsQuery.byFields(tableName, named, howMatched), Parameters.addFields(named), Boolean::class.java, + conn, Results::toExists ) } @@ -98,10 +99,11 @@ object Exists { @Throws(DocumentException::class) @JvmStatic fun byContains(tableName: String, criteria: TContains, conn: Connection) = - conn.customScalar( + Custom.scalar( ExistsQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), Boolean::class.java, + conn, Results::toExists ) @@ -130,10 +132,11 @@ object Exists { @Throws(DocumentException::class) @JvmStatic fun byJsonPath(tableName: String, path: String, conn: Connection) = - conn.customScalar( + Custom.scalar( ExistsQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), Boolean::class.java, + conn, Results::toExists ) diff --git a/src/jvm/src/main/kotlin/jvm/Find.kt b/src/core/src/main/kotlin/java/Find.kt similarity index 92% rename from src/jvm/src/main/kotlin/jvm/Find.kt rename to src/core/src/main/kotlin/java/Find.kt index b63d52f..09d8bef 100644 --- a/src/jvm/src/main/kotlin/jvm/Find.kt +++ b/src/core/src/main/kotlin/java/Find.kt @@ -1,11 +1,10 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.extensions.customList -import solutions.bitbadger.documents.extensions.customSingle import solutions.bitbadger.documents.query.FindQuery import solutions.bitbadger.documents.query.orderBy import java.sql.Connection +import java.util.Optional import kotlin.jvm.Throws /** @@ -26,7 +25,7 @@ object Find { @Throws(DocumentException::class) @JvmStatic fun all(tableName: String, clazz: Class, orderBy: Collection>? = null, conn: Connection) = - conn.customList(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), clazz, Results::fromData) + Custom.list(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), clazz, conn, Results::fromData) /** * Retrieve all documents in the given table @@ -64,16 +63,17 @@ object Find { * @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 The document if it is found, `null` otherwise + * @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) = - conn.customSingle( + Custom.single( FindQuery.byId(tableName, docId), Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))), clazz, + conn, Results::fromData ) @@ -83,7 +83,7 @@ object Find { * @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 The document if it is found, `null` otherwise + * @return An `Optional` item with the document if it is found * @throws DocumentException If no connection string has been set */ @Throws(DocumentException::class) @@ -114,10 +114,11 @@ object Find { conn: Connection ): List { val named = Parameters.nameFields(fields) - return conn.customList( + return Custom.list( FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), Parameters.addFields(named), clazz, + conn, Results::fromData ) } @@ -187,10 +188,11 @@ object Find { orderBy: Collection>? = null, conn: Connection ) = - conn.customList( + Custom.list( FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameters.json(":criteria", criteria)), clazz, + conn, Results::fromData ) @@ -250,10 +252,11 @@ object Find { orderBy: Collection>? = null, conn: Connection ) = - conn.customList( + Custom.list( FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameter(":path", ParameterType.STRING, path)), clazz, + conn, Results::fromData ) @@ -297,7 +300,7 @@ object Find { * @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 + * @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) @@ -309,12 +312,13 @@ object Find { howMatched: FieldMatch? = null, orderBy: Collection>? = null, conn: Connection - ): TDoc? { + ): Optional { val named = Parameters.nameFields(fields) - return conn.customSingle( + return Custom.single( FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""), Parameters.addFields(named), clazz, + conn, Results::fromData ) } @@ -327,7 +331,7 @@ object Find { * @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 The first document matching the field comparison, or `null` if no matches are found + * @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) @@ -350,7 +354,7 @@ object Find { * @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 The first document matching the field comparison, or `null` if no matches are found + * @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) @@ -371,7 +375,7 @@ object Find { * @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 + * @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) @@ -383,10 +387,11 @@ object Find { orderBy: Collection>? = null, conn: Connection ) = - conn.customSingle( + Custom.single( FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameters.json(":criteria", criteria)), clazz, + conn, Results::fromData ) @@ -397,7 +402,7 @@ object Find { * @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 The first document matching the JSON containment query, or `null` if no matches are found + * @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) @@ -417,7 +422,7 @@ object Find { * @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 The first document matching the JSON containment query, or `null` if no matches are found + * @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) @@ -439,7 +444,7 @@ object Find { * @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 The first document matching the JSON Path match query, or `null` if no matches are found + * @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) @@ -451,10 +456,11 @@ object Find { orderBy: Collection>? = null, conn: Connection ) = - conn.customSingle( + Custom.single( FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(Parameter(":path", ParameterType.STRING, path)), clazz, + conn, Results::fromData ) @@ -465,7 +471,7 @@ object Find { * @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 The first document matching the JSON Path match query, or `null` if no matches are found + * @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) @@ -480,7 +486,7 @@ object Find { * @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 The first document matching the JSON Path match query, or `null` if no matches are found + * @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) diff --git a/src/jvm/src/main/kotlin/jvm/NullDocumentSerializer.kt b/src/core/src/main/kotlin/java/NullDocumentSerializer.kt similarity index 94% rename from src/jvm/src/main/kotlin/jvm/NullDocumentSerializer.kt rename to src/core/src/main/kotlin/java/NullDocumentSerializer.kt index 41a7b63..233a6ce 100644 --- a/src/jvm/src/main/kotlin/jvm/NullDocumentSerializer.kt +++ b/src/core/src/main/kotlin/java/NullDocumentSerializer.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.DocumentSerializer diff --git a/src/jvm/src/main/kotlin/jvm/Parameters.kt b/src/core/src/main/kotlin/java/Parameters.kt similarity index 99% rename from src/jvm/src/main/kotlin/jvm/Parameters.kt rename to src/core/src/main/kotlin/java/Parameters.kt index a614480..b45b64a 100644 --- a/src/jvm/src/main/kotlin/jvm/Parameters.kt +++ b/src/core/src/main/kotlin/java/Parameters.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.* import solutions.bitbadger.documents.ParameterName diff --git a/src/jvm/src/main/kotlin/jvm/Patch.kt b/src/core/src/main/kotlin/java/Patch.kt similarity index 92% rename from src/jvm/src/main/kotlin/jvm/Patch.kt rename to src/core/src/main/kotlin/java/Patch.kt index 1097d9c..1a03d00 100644 --- a/src/jvm/src/main/kotlin/jvm/Patch.kt +++ b/src/core/src/main/kotlin/java/Patch.kt @@ -1,7 +1,6 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.extensions.customNonQuery import solutions.bitbadger.documents.query.PatchQuery import java.sql.Connection import kotlin.jvm.Throws @@ -23,12 +22,13 @@ object Patch { @Throws(DocumentException::class) @JvmStatic fun byId(tableName: String, docId: TKey, patch: TPatch, conn: Connection) = - conn.customNonQuery( + Custom.nonQuery( PatchQuery.byId(tableName, docId), Parameters.addFields( listOf(Field.equal(Configuration.idField, docId, ":id")), mutableListOf(Parameters.json(":data", patch)) - ) + ), + conn ) /** @@ -64,11 +64,10 @@ object Patch { conn: Connection ) { val named = Parameters.nameFields(fields) - conn.customNonQuery( - PatchQuery.byFields(tableName, named, howMatched), Parameters.addFields( - named, - mutableListOf(Parameters.json(":data", patch)) - ) + Custom.nonQuery( + PatchQuery.byFields(tableName, named, howMatched), + Parameters.addFields(named, mutableListOf(Parameters.json(":data", patch))), + conn ) } @@ -104,9 +103,10 @@ object Patch { @Throws(DocumentException::class) @JvmStatic fun byContains(tableName: String, criteria: TContains, patch: TPatch, conn: Connection) = - conn.customNonQuery( + Custom.nonQuery( PatchQuery.byContains(tableName), - listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch)) + listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch)), + conn ) /** @@ -134,9 +134,10 @@ object Patch { @Throws(DocumentException::class) @JvmStatic fun byJsonPath(tableName: String, path: String, patch: TPatch, conn: Connection) = - conn.customNonQuery( + Custom.nonQuery( PatchQuery.byJsonPath(tableName), - listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch)) + listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch)), + conn ) /** diff --git a/src/jvm/src/main/kotlin/jvm/RemoveFields.kt b/src/core/src/main/kotlin/java/RemoveFields.kt similarity index 94% rename from src/jvm/src/main/kotlin/jvm/RemoveFields.kt rename to src/core/src/main/kotlin/java/RemoveFields.kt index b69fbf1..540d504 100644 --- a/src/jvm/src/main/kotlin/jvm/RemoveFields.kt +++ b/src/core/src/main/kotlin/java/RemoveFields.kt @@ -1,7 +1,6 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.extensions.customNonQuery import solutions.bitbadger.documents.query.RemoveFieldsQuery import java.sql.Connection import kotlin.jvm.Throws @@ -38,12 +37,10 @@ object RemoveFields { @JvmStatic fun byId(tableName: String, docId: TKey, toRemove: Collection, conn: Connection) { val nameParams = Parameters.fieldNames(toRemove) - conn.customNonQuery( + Custom.nonQuery( RemoveFieldsQuery.byId(tableName, nameParams, docId), - Parameters.addFields( - listOf(Field.equal(Configuration.idField, docId, ":id")), - translatePath(nameParams) - ) + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id")), translatePath(nameParams)), + conn ) } @@ -81,9 +78,10 @@ object RemoveFields { ) { val named = Parameters.nameFields(fields) val nameParams = Parameters.fieldNames(toRemove) - conn.customNonQuery( + Custom.nonQuery( RemoveFieldsQuery.byFields(tableName, nameParams, named, howMatched), - Parameters.addFields(named, translatePath(nameParams)) + Parameters.addFields(named, translatePath(nameParams)), + conn ) } @@ -125,9 +123,10 @@ object RemoveFields { conn: Connection ) { val nameParams = Parameters.fieldNames(toRemove) - conn.customNonQuery( + Custom.nonQuery( RemoveFieldsQuery.byContains(tableName, nameParams), - listOf(Parameters.json(":criteria", criteria), *nameParams.toTypedArray()) + listOf(Parameters.json(":criteria", criteria), *nameParams.toTypedArray()), + conn ) } @@ -157,9 +156,10 @@ object RemoveFields { @JvmStatic fun byJsonPath(tableName: String, path: String, toRemove: Collection, conn: Connection) { val nameParams = Parameters.fieldNames(toRemove) - conn.customNonQuery( + Custom.nonQuery( RemoveFieldsQuery.byJsonPath(tableName, nameParams), - listOf(Parameter(":path", ParameterType.STRING, path), *nameParams.toTypedArray()) + listOf(Parameter(":path", ParameterType.STRING, path), *nameParams.toTypedArray()), + conn ) } diff --git a/src/jvm/src/main/kotlin/jvm/Results.kt b/src/core/src/main/kotlin/java/Results.kt similarity index 98% rename from src/jvm/src/main/kotlin/jvm/Results.kt rename to src/core/src/main/kotlin/java/Results.kt index 0bea109..d79b533 100644 --- a/src/jvm/src/main/kotlin/jvm/Results.kt +++ b/src/core/src/main/kotlin/java/Results.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.jvm +package solutions.bitbadger.documents.java import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect @@ -6,7 +6,6 @@ import solutions.bitbadger.documents.DocumentException import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.SQLException -import kotlin.jvm.Throws object Results { diff --git a/src/jvm/src/main/kotlin/extensions/Connection.kt b/src/core/src/main/kotlin/java/extensions/Connection.kt similarity index 99% rename from src/jvm/src/main/kotlin/extensions/Connection.kt rename to src/core/src/main/kotlin/java/extensions/Connection.kt index e539c22..f28a103 100644 --- a/src/jvm/src/main/kotlin/extensions/Connection.kt +++ b/src/core/src/main/kotlin/java/extensions/Connection.kt @@ -1,9 +1,9 @@ @file:JvmName("ConnExt") -package solutions.bitbadger.documents.extensions +package solutions.bitbadger.documents.java.extensions import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.jvm.* +import solutions.bitbadger.documents.java.* import java.sql.Connection import java.sql.ResultSet import kotlin.jvm.Throws @@ -430,6 +430,7 @@ fun Connection.patchById(tableName: String, docId: TKey, patch: T * @throws DocumentException If no dialect has been configured, or if parameters are invalid */ @Throws(DocumentException::class) +@JvmOverloads fun Connection.patchByFields( tableName: String, fields: Collection>, @@ -490,6 +491,7 @@ fun Connection.removeFieldsById(tableName: String, docId: TKey, toRemove: * @throws DocumentException If no dialect has been configured, or if parameters are invalid */ @Throws(DocumentException::class) +@JvmOverloads fun Connection.removeFieldsByFields( tableName: String, fields: Collection>, diff --git a/src/jvm/src/main/kotlin/query/CountQuery.kt b/src/core/src/main/kotlin/query/CountQuery.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/CountQuery.kt rename to src/core/src/main/kotlin/query/CountQuery.kt diff --git a/src/jvm/src/main/kotlin/query/DefinitionQuery.kt b/src/core/src/main/kotlin/query/DefinitionQuery.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/DefinitionQuery.kt rename to src/core/src/main/kotlin/query/DefinitionQuery.kt diff --git a/src/jvm/src/main/kotlin/query/DeleteQuery.kt b/src/core/src/main/kotlin/query/DeleteQuery.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/DeleteQuery.kt rename to src/core/src/main/kotlin/query/DeleteQuery.kt diff --git a/src/jvm/src/main/kotlin/query/DocumentQuery.kt b/src/core/src/main/kotlin/query/DocumentQuery.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/DocumentQuery.kt rename to src/core/src/main/kotlin/query/DocumentQuery.kt diff --git a/src/jvm/src/main/kotlin/query/ExistsQuery.kt b/src/core/src/main/kotlin/query/ExistsQuery.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/ExistsQuery.kt rename to src/core/src/main/kotlin/query/ExistsQuery.kt diff --git a/src/jvm/src/main/kotlin/query/FindQuery.kt b/src/core/src/main/kotlin/query/FindQuery.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/FindQuery.kt rename to src/core/src/main/kotlin/query/FindQuery.kt diff --git a/src/jvm/src/main/kotlin/query/PatchQuery.kt b/src/core/src/main/kotlin/query/PatchQuery.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/PatchQuery.kt rename to src/core/src/main/kotlin/query/PatchQuery.kt diff --git a/src/jvm/src/main/kotlin/query/Query.kt b/src/core/src/main/kotlin/query/Query.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/Query.kt rename to src/core/src/main/kotlin/query/Query.kt diff --git a/src/jvm/src/main/kotlin/query/RemoveFieldsQuery.kt b/src/core/src/main/kotlin/query/RemoveFieldsQuery.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/RemoveFieldsQuery.kt rename to src/core/src/main/kotlin/query/RemoveFieldsQuery.kt diff --git a/src/jvm/src/main/kotlin/query/Where.kt b/src/core/src/main/kotlin/query/Where.kt similarity index 100% rename from src/jvm/src/main/kotlin/query/Where.kt rename to src/core/src/main/kotlin/query/Where.kt 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/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/AutoIdTest.java similarity index 98% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/AutoIdTest.java index b157359..8f6c0f8 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/AutoIdTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/AutoIdTest.java @@ -1,17 +1,16 @@ -package solutions.bitbadger.documents.java; +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 solutions.bitbadger.documents.java.support.*; import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `AutoId` enum */ -@DisplayName("JVM | Java | AutoId") +@DisplayName("Core | Java | AutoId") final public class AutoIdTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ByteIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ByteIdClass.java similarity index 80% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ByteIdClass.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ByteIdClass.java index bf2be5d..72e872f 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ByteIdClass.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ByteIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.support; +package solutions.bitbadger.documents.core.tests.java; public class ByteIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ConfigurationTest.java similarity index 94% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ConfigurationTest.java index 2a7ef00..d198c6c 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ConfigurationTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ConfigurationTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; /** * Unit tests for the `Configuration` object */ -@DisplayName("JVM | Java | Configuration") +@DisplayName("Core | Java | Configuration") final public class ConfigurationTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/CountQueryTest.java similarity index 92% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/CountQueryTest.java index 02b916f..71f3c74 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/CountQueryTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/CountQueryTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -6,18 +6,18 @@ import org.junit.jupiter.api.Test; import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.Field; import solutions.bitbadger.documents.query.CountQuery; -import solutions.bitbadger.documents.support.ForceDialect; +import 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.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; /** * Unit tests for the `Count` object */ -@DisplayName("JVM | Java | Query | CountQuery") +@DisplayName("Core | Java | Query | CountQuery") final public class CountQueryTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DefinitionQueryTest.java similarity index 95% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DefinitionQueryTest.java index 3558285..219c31f 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DefinitionQueryTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DefinitionQueryTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -7,18 +7,18 @@ import solutions.bitbadger.documents.Dialect; import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.DocumentIndex; import solutions.bitbadger.documents.query.DefinitionQuery; -import solutions.bitbadger.documents.support.ForceDialect; +import 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.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; /** * Unit tests for the `Definition` object */ -@DisplayName("JVM | Java | Query | DefinitionQuery") +@DisplayName("Core | Java | Query | DefinitionQuery") final public class DefinitionQueryTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DeleteQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DeleteQueryTest.java similarity index 93% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DeleteQueryTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DeleteQueryTest.java index 4922f66..3128652 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DeleteQueryTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DeleteQueryTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -6,18 +6,18 @@ import org.junit.jupiter.api.Test; import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.Field; import solutions.bitbadger.documents.query.DeleteQuery; -import solutions.bitbadger.documents.support.ForceDialect; +import 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.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; /** * Unit tests for the `Delete` object */ -@DisplayName("JVM | Java | Query | DeleteQuery") +@DisplayName("Core | Java | Query | DeleteQuery") final public class DeleteQueryTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/DialectTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DialectTest.java similarity index 94% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/DialectTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DialectTest.java index 91fbb2f..543986d 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/DialectTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DialectTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,7 +10,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Dialect` enum */ -@DisplayName("JVM | Java | Dialect") +@DisplayName("Core | Java | Dialect") final public class DialectTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentIndexTest.java similarity index 87% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentIndexTest.java index 72b3d79..7354ebb 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/DocumentIndexTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentIndexTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `DocumentIndex` enum */ -@DisplayName("JVM | Java | DocumentIndex") +@DisplayName("Core | Java | DocumentIndex") final public class DocumentIndexTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DocumentQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentQueryTest.java similarity index 95% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DocumentQueryTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentQueryTest.java index 26307f5..ada52b3 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/DocumentQueryTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/DocumentQueryTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -7,15 +7,15 @@ import solutions.bitbadger.documents.AutoId; import solutions.bitbadger.documents.Configuration; import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.query.DocumentQuery; -import solutions.bitbadger.documents.support.ForceDialect; +import solutions.bitbadger.documents.core.tests.ForceDialect; import static org.junit.jupiter.api.Assertions.*; -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; /** * Unit tests for the `Document` object */ -@DisplayName("JVM | Java | Query | DocumentQuery") +@DisplayName("Core | Java | Query | DocumentQuery") final public class DocumentQueryTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/ExistsQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ExistsQueryTest.java similarity index 93% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/ExistsQueryTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ExistsQueryTest.java index 82a6c52..d4b2e57 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/ExistsQueryTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ExistsQueryTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -6,18 +6,18 @@ 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.support.ForceDialect; +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.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; /** * Unit tests for the `Exists` object */ -@DisplayName("JVM | Java | Query | ExistsQuery") +@DisplayName("Core | Java | Query | ExistsQuery") final public class ExistsQueryTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldMatchTest.java similarity index 85% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldMatchTest.java index f9dab5f..54a9096 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldMatchTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldMatchTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `FieldMatch` enum */ -@DisplayName("JVM | Java | FieldMatch") +@DisplayName("Core | Java | FieldMatch") final public class FieldMatchTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldTest.java similarity index 99% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldTest.java index 0c2f668..193869a 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/FieldTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FieldTest.java @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.java; +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.support.ForceDialect; +import solutions.bitbadger.documents.core.tests.ForceDialect; import java.util.Collection; import java.util.List; @@ -15,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Field` class */ -@DisplayName("JVM | Java | Field") +@DisplayName("Core | Java | Field") final public class FieldTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/FindQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FindQueryTest.java similarity index 93% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/FindQueryTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FindQueryTest.java index bcc717e..900c96b 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/FindQueryTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/FindQueryTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -6,18 +6,18 @@ import org.junit.jupiter.api.Test; import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.Field; import solutions.bitbadger.documents.query.FindQuery; -import solutions.bitbadger.documents.support.ForceDialect; +import 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.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; /** * Unit tests for the `Find` object */ -@DisplayName("JVM | Java | Query | FindQuery") +@DisplayName("Core | Java | Query | FindQuery") final public class FindQueryTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/IntIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/IntIdClass.java similarity index 80% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/support/IntIdClass.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/IntIdClass.java index 2db8732..2acdef6 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/IntIdClass.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/IntIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.support; +package solutions.bitbadger.documents.core.tests.java; public class IntIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/LongIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/LongIdClass.java similarity index 80% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/support/LongIdClass.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/LongIdClass.java index 665e2c1..0ea90a9 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/LongIdClass.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/LongIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.support; +package solutions.bitbadger.documents.core.tests.java; public class LongIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/OpTest.java similarity index 96% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/OpTest.java index f216ee4..5ecfd48 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/OpTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/OpTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `Op` enum */ -@DisplayName("JVM | Java | Op") +@DisplayName("Core | Java | Op") final public class OpTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterNameTest.java similarity index 92% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterNameTest.java index 1c18044..32d88e7 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterNameTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterNameTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the `ParameterName` class */ -@DisplayName("JVM | Java | ParameterName") +@DisplayName("Core | Java | ParameterName") final public class ParameterNameTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterTest.java similarity index 93% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterTest.java index 9ca4432..abe2ae3 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/ParameterTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParameterTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Parameter` class */ -@DisplayName("JVM | Java | Parameter") +@DisplayName("Core | Java | Parameter") final public class ParameterTest { @Test diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParametersTest.java similarity index 97% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParametersTest.java index 4c5f39f..c74fac6 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/ParametersTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ParametersTest.java @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.java.jvm; +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.jvm.Parameters; +import solutions.bitbadger.documents.java.Parameters; import java.util.List; @@ -13,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for the `Parameters` object */ -@DisplayName("JVM | Java | Parameters") +@DisplayName("Core | Java | Parameters") final public class ParametersTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/PatchQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/PatchQueryTest.java similarity index 93% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/PatchQueryTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/PatchQueryTest.java index 56afbdc..f0f6b7a 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/PatchQueryTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/PatchQueryTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -6,18 +6,18 @@ import org.junit.jupiter.api.Test; import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.Field; import solutions.bitbadger.documents.query.PatchQuery; -import solutions.bitbadger.documents.support.ForceDialect; +import 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.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; /** * Unit tests for the `Patch` object */ -@DisplayName("JVM | Java | Query | PatchQuery") +@DisplayName("Core | Java | Query | PatchQuery") final public class PatchQueryTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/QueryUtilsTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/QueryUtilsTest.java similarity index 97% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/QueryUtilsTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/QueryUtilsTest.java index f815db0..48d8ffb 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/QueryUtilsTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/QueryUtilsTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -8,7 +8,7 @@ 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.support.ForceDialect; +import solutions.bitbadger.documents.core.tests.ForceDialect; import java.util.List; @@ -17,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** * Unit tests for the package-level query functions, presented as `QueryUtils` for Java-compatible use */ -@DisplayName("JVM | Java | Query | QueryUtils") +@DisplayName("Core | Java | Query | QueryUtils") final public class QueryUtilsTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/RemoveFieldsQueryTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/RemoveFieldsQueryTest.java similarity index 94% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/RemoveFieldsQueryTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/RemoveFieldsQueryTest.java index 027352e..10d9de0 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/RemoveFieldsQueryTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/RemoveFieldsQueryTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -8,18 +8,18 @@ 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.support.ForceDialect; +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.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; /** * Unit tests for the `RemoveFields` object */ -@DisplayName("JVM | Java | Query | RemoveFieldsQuery") +@DisplayName("Core | Java | Query | RemoveFieldsQuery") final public class RemoveFieldsQueryTest { /** diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ShortIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ShortIdClass.java similarity index 81% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ShortIdClass.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ShortIdClass.java index 6608387..f064677 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/ShortIdClass.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/ShortIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.support; +package solutions.bitbadger.documents.core.tests.java; public class ShortIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/StringIdClass.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/StringIdClass.java similarity index 81% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/support/StringIdClass.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/StringIdClass.java index 8264d2b..b47d34d 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/StringIdClass.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/StringIdClass.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.support; +package solutions.bitbadger.documents.core.tests.java; public class StringIdClass { diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/WhereTest.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/WhereTest.java similarity index 97% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/query/WhereTest.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/WhereTest.java index 13de7d9..575effa 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/query/WhereTest.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/WhereTest.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.query; +package solutions.bitbadger.documents.core.tests.java; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -7,7 +7,7 @@ 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.support.ForceDialect; +import solutions.bitbadger.documents.core.tests.ForceDialect; import java.util.List; @@ -17,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; /** * Unit tests for the `Where` object */ -@DisplayName("JVM | Java | Query | Where") +@DisplayName("Core | Java | Query | Where") final public class WhereTest { /** 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/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CountFunctions.java similarity index 87% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CountFunctions.java index a502241..b851ce0 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CountFunctions.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CountFunctions.java @@ -1,16 +1,15 @@ -package solutions.bitbadger.documents.java.jvm.integration.common; +package solutions.bitbadger.documents.core.tests.java.integration; import solutions.bitbadger.documents.DocumentException; import solutions.bitbadger.documents.Field; -import solutions.bitbadger.documents.java.support.JsonDocument; -import solutions.bitbadger.documents.support.ThrowawayDatabase; +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.extensions.ConnExt.*; -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; /** * Integration tests for the `Count` object diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CustomFunctions.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java similarity index 83% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CustomFunctions.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java index a1bdfe9..7c6ee1b 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/common/CustomFunctions.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/CustomFunctions.java @@ -1,19 +1,18 @@ -package solutions.bitbadger.documents.java.jvm.integration.common; +package solutions.bitbadger.documents.core.tests.java.integration; import solutions.bitbadger.documents.*; -import solutions.bitbadger.documents.java.support.JsonDocument; -import solutions.bitbadger.documents.jvm.Results; +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 solutions.bitbadger.documents.support.ThrowawayDatabase; import java.util.Collection; import java.util.List; import static org.junit.jupiter.api.Assertions.*; -import static solutions.bitbadger.documents.extensions.ConnExt.*; -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.java.extensions.ConnExt.*; final public class CustomFunctions { @@ -33,16 +32,18 @@ final public class CustomFunctions { } public static void singleNone(ThrowawayDatabase db) throws DocumentException { - assertNull( - customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData), + 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); - assertNotNull( - customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData), - "There should not have been a document returned"); + 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 nonQueryChanges(ThrowawayDatabase db) throws DocumentException { 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/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonDocument.java similarity index 88% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonDocument.java index ec6e639..cb85902 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/JsonDocument.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonDocument.java @@ -1,13 +1,13 @@ -package solutions.bitbadger.documents.java.support; +package solutions.bitbadger.documents.core.tests.java.integration; import solutions.bitbadger.documents.DocumentException; -import solutions.bitbadger.documents.jvm.Document; -import solutions.bitbadger.documents.support.ThrowawayDatabase; +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.support.TypesKt.TEST_TABLE; +import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE; public class JsonDocument { 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..f980c09 --- /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 value; + + public int getKey() { + return key; + } + + public void setKey(int key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public NumIdDocument(int key, String value) { + this.key = key; + this.value = value; + } + + 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/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCountIT.java similarity index 86% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCountIT.java index def860b..2d4c829 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CountIT.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCountIT.java @@ -1,16 +1,15 @@ -package solutions.bitbadger.documents.java.jvm.integration.postgresql; +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.java.jvm.integration.common.CountFunctions; -import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB; +import solutions.bitbadger.documents.core.tests.integration.PgDB; /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("JVM | Java | PostgreSQL: Count") -public class CountIT { +@DisplayName("Core | Java | PostgreSQL: Count") +public class PostgreSQLCountIT { @Test @DisplayName("all counts all documents") diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CustomIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java similarity index 85% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CustomIT.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java index 3396255..d2c2b01 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/postgresql/CustomIT.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLCustomIT.java @@ -1,16 +1,15 @@ -package solutions.bitbadger.documents.java.jvm.integration.postgresql; +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.jvm.integration.postgresql.PgDB; -import solutions.bitbadger.documents.java.jvm.integration.common.CustomFunctions; +import solutions.bitbadger.documents.core.tests.integration.PgDB; /** * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions */ -@DisplayName("JVM | Java | PostgreSQL: Custom") -final public class CustomIT { +@DisplayName("Core | Java | PostgreSQL: Custom") +final public class PostgreSQLCustomIT { @Test @DisplayName("list succeeds with empty list") 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/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/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCountIT.java similarity index 84% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCountIT.java index 1d9078b..bc8947e 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CountIT.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCountIT.java @@ -1,18 +1,17 @@ -package solutions.bitbadger.documents.java.jvm.integration.sqlite; +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.java.jvm.integration.common.CountFunctions; -import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB; +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("JVM | Java | SQLite: Count") -public class CountIT { +@DisplayName("Core | Java | SQLite: Count") +public class SQLiteCountIT { @Test @DisplayName("all counts all documents") diff --git a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CustomIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java similarity index 86% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CustomIT.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java index 76acab9..6398554 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/jvm/integration/sqlite/CustomIT.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteCustomIT.java @@ -1,16 +1,15 @@ -package solutions.bitbadger.documents.java.jvm.integration.sqlite; +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.java.jvm.integration.common.CustomFunctions; -import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB; +import solutions.bitbadger.documents.core.tests.integration.SQLiteDB; /** * SQLite integration tests for the `Custom` object / `custom*` connection extension functions */ -@DisplayName("JVM | Java | SQLite: Custom") -final public class CustomIT { +@DisplayName("Core | Java | SQLite: Custom") +final public class SQLiteCustomIT { @Test @DisplayName("list succeeds with empty list") 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/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/jvm/src/test/java/solutions/bitbadger/documents/java/support/SubDocument.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SubDocument.java similarity index 87% rename from src/jvm/src/test/java/solutions/bitbadger/documents/java/support/SubDocument.java rename to src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SubDocument.java index 6d5bdf9..843b51d 100644 --- a/src/jvm/src/test/java/solutions/bitbadger/documents/java/support/SubDocument.java +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SubDocument.java @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.java.support; +package solutions.bitbadger.documents.core.tests.java.integration; public class SubDocument { diff --git a/src/jvm/src/test/kotlin/AutoIdTest.kt b/src/core/src/test/kotlin/AutoIdTest.kt similarity index 94% rename from src/jvm/src/test/kotlin/AutoIdTest.kt rename to src/core/src/test/kotlin/AutoIdTest.kt index 87099ae..84f6f3e 100644 --- a/src/jvm/src/test/kotlin/AutoIdTest.kt +++ b/src/core/src/test/kotlin/AutoIdTest.kt @@ -1,9 +1,10 @@ -package solutions.bitbadger.documents +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.support.* +import solutions.bitbadger.documents.AutoId +import solutions.bitbadger.documents.DocumentException import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals @@ -12,7 +13,7 @@ import kotlin.test.assertTrue /** * Unit tests for the `AutoId` enum */ -@DisplayName("JVM | Kotlin | AutoId") +@DisplayName("Core | Kotlin | AutoId") class AutoIdTest { @Test @@ -158,3 +159,9 @@ class AutoIdTest { 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/jvm/src/test/kotlin/ComparisonTest.kt b/src/core/src/test/kotlin/ComparisonTest.kt similarity index 95% rename from src/jvm/src/test/kotlin/ComparisonTest.kt rename to src/core/src/test/kotlin/ComparisonTest.kt index 46c3cae..397ad3f 100644 --- a/src/jvm/src/test/kotlin/ComparisonTest.kt +++ b/src/core/src/test/kotlin/ComparisonTest.kt @@ -1,7 +1,8 @@ -package solutions.bitbadger.documents +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 @@ -9,7 +10,7 @@ import kotlin.test.assertTrue /** * Unit tests for the `ComparisonBetween` class */ -@DisplayName("JVM | Kotlin | ComparisonBetween") +@DisplayName("Core | Kotlin | ComparisonBetween") class ComparisonBetweenTest { @Test @@ -50,7 +51,7 @@ class ComparisonBetweenTest { /** * Unit tests for the `ComparisonIn` class */ -@DisplayName("JVM | Kotlin | ComparisonIn") +@DisplayName("Core | Kotlin | ComparisonIn") class ComparisonInTest { @Test @@ -92,7 +93,7 @@ class ComparisonInTest { /** * Unit tests for the `ComparisonInArray` class */ -@DisplayName("JVM | Kotlin | ComparisonInArray") +@DisplayName("Core | Kotlin | ComparisonInArray") class ComparisonInArrayTest { @Test @@ -138,7 +139,7 @@ class ComparisonInArrayTest { /** * Unit tests for the `ComparisonSingle` class */ -@DisplayName("JVM | Kotlin | ComparisonSingle") +@DisplayName("Core | Kotlin | ComparisonSingle") class ComparisonSingleTest { @Test diff --git a/src/jvm/src/test/kotlin/ConfigurationTest.kt b/src/core/src/test/kotlin/ConfigurationTest.kt similarity index 80% rename from src/jvm/src/test/kotlin/ConfigurationTest.kt rename to src/core/src/test/kotlin/ConfigurationTest.kt index bdc8e97..a740366 100644 --- a/src/jvm/src/test/kotlin/ConfigurationTest.kt +++ b/src/core/src/test/kotlin/ConfigurationTest.kt @@ -1,14 +1,18 @@ -package solutions.bitbadger.documents +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("JVM | Kotlin | Configuration") +@DisplayName("Core | Kotlin | Configuration") class ConfigurationTest { @Test diff --git a/src/jvm/src/test/kotlin/query/CountQueryTest.kt b/src/core/src/test/kotlin/CountQueryTest.kt similarity index 87% rename from src/jvm/src/test/kotlin/query/CountQueryTest.kt rename to src/core/src/test/kotlin/CountQueryTest.kt index 9c3d413..d820473 100644 --- a/src/jvm/src/test/kotlin/query/CountQueryTest.kt +++ b/src/core/src/test/kotlin/CountQueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.core.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,14 +6,13 @@ 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.support.ForceDialect -import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.query.CountQuery import kotlin.test.assertEquals /** * Unit tests for the `Count` object */ -@DisplayName("JVM | Kotlin | Query | CountQuery") +@DisplayName("Core | Kotlin | Query | CountQuery") class CountQueryTest { /** @@ -27,7 +26,11 @@ class CountQueryTest { @Test @DisplayName("all generates correctly") fun all() = - assertEquals("SELECT COUNT(*) AS it FROM $TEST_TABLE", CountQuery.all(TEST_TABLE), "Count query not constructed correctly") + assertEquals( + "SELECT COUNT(*) AS it FROM $TEST_TABLE", + CountQuery.all(TEST_TABLE), + "Count query not constructed correctly" + ) @Test @DisplayName("byFields generates correctly | PostgreSQL") diff --git a/src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt b/src/core/src/test/kotlin/DefinitionQueryTest.kt similarity index 95% rename from src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt rename to src/core/src/test/kotlin/DefinitionQueryTest.kt index a0ccf41..697fffa 100644 --- a/src/jvm/src/test/kotlin/query/DefinitionQueryTest.kt +++ b/src/core/src/test/kotlin/DefinitionQueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.core.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -7,14 +7,13 @@ 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.support.ForceDialect -import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.query.DefinitionQuery import kotlin.test.assertEquals /** * Unit tests for the `Definition` object */ -@DisplayName("JVM | Kotlin | Query | DefinitionQuery") +@DisplayName("Core | Kotlin | Query | DefinitionQuery") class DefinitionQueryTest { /** diff --git a/src/jvm/src/test/kotlin/query/DeleteQueryTest.kt b/src/core/src/test/kotlin/DeleteQueryTest.kt similarity index 93% rename from src/jvm/src/test/kotlin/query/DeleteQueryTest.kt rename to src/core/src/test/kotlin/DeleteQueryTest.kt index cc56578..f33721b 100644 --- a/src/jvm/src/test/kotlin/query/DeleteQueryTest.kt +++ b/src/core/src/test/kotlin/DeleteQueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.core.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,14 +6,13 @@ 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.support.ForceDialect -import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.query.DeleteQuery import kotlin.test.assertEquals /** * Unit tests for the `Delete` object */ -@DisplayName("JVM | Kotlin | Query | DeleteQuery") +@DisplayName("Core | Kotlin | Query | DeleteQuery") class DeleteQueryTest { /** diff --git a/src/jvm/src/test/kotlin/DialectTest.kt b/src/core/src/test/kotlin/DialectTest.kt similarity index 87% rename from src/jvm/src/test/kotlin/DialectTest.kt rename to src/core/src/test/kotlin/DialectTest.kt index eb97769..ef7e916 100644 --- a/src/jvm/src/test/kotlin/DialectTest.kt +++ b/src/core/src/test/kotlin/DialectTest.kt @@ -1,7 +1,9 @@ -package solutions.bitbadger.documents +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 @@ -10,7 +12,7 @@ import kotlin.test.fail /** * Unit tests for the `Dialect` enum */ -@DisplayName("JVM | Kotlin | Dialect") +@DisplayName("Core | Kotlin | Dialect") class DialectTest { @Test diff --git a/src/jvm/src/test/kotlin/DocumentIndexTest.kt b/src/core/src/test/kotlin/DocumentIndexTest.kt similarity index 78% rename from src/jvm/src/test/kotlin/DocumentIndexTest.kt rename to src/core/src/test/kotlin/DocumentIndexTest.kt index fccbb44..5ee6ef0 100644 --- a/src/jvm/src/test/kotlin/DocumentIndexTest.kt +++ b/src/core/src/test/kotlin/DocumentIndexTest.kt @@ -1,13 +1,14 @@ -package solutions.bitbadger.documents +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("JVM | Kotlin | DocumentIndex") +@DisplayName("Core | Kotlin | DocumentIndex") class DocumentIndexTest { @Test diff --git a/src/jvm/src/test/kotlin/query/DocumentQueryTest.kt b/src/core/src/test/kotlin/DocumentQueryTest.kt similarity index 96% rename from src/jvm/src/test/kotlin/query/DocumentQueryTest.kt rename to src/core/src/test/kotlin/DocumentQueryTest.kt index e57340e..3565b41 100644 --- a/src/jvm/src/test/kotlin/query/DocumentQueryTest.kt +++ b/src/core/src/test/kotlin/DocumentQueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.core.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -7,15 +7,14 @@ 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.support.ForceDialect -import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.query.DocumentQuery import kotlin.test.assertEquals import kotlin.test.assertTrue /** * Unit tests for the `Document` object */ -@DisplayName("JVM | Kotlin | Query | DocumentQuery") +@DisplayName("Core | Kotlin | Query | DocumentQuery") class DocumentQueryTest { /** diff --git a/src/jvm/src/test/kotlin/query/ExistsQueryTest.kt b/src/core/src/test/kotlin/ExistsQueryTest.kt similarity index 91% rename from src/jvm/src/test/kotlin/query/ExistsQueryTest.kt rename to src/core/src/test/kotlin/ExistsQueryTest.kt index 991a036..bfd066e 100644 --- a/src/jvm/src/test/kotlin/query/ExistsQueryTest.kt +++ b/src/core/src/test/kotlin/ExistsQueryTest.kt @@ -1,21 +1,18 @@ -package solutions.bitbadger.documents.query +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.Configuration -import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.query.ExistsQuery import kotlin.test.assertEquals /** * Unit tests for the `Exists` object */ -@DisplayName("JVM | Kotlin | Query | ExistsQuery") +@DisplayName("Core | Kotlin | Query | ExistsQuery") class ExistsQueryTest { /** diff --git a/src/jvm/src/test/kotlin/FieldMatchTest.kt b/src/core/src/test/kotlin/FieldMatchTest.kt similarity index 76% rename from src/jvm/src/test/kotlin/FieldMatchTest.kt rename to src/core/src/test/kotlin/FieldMatchTest.kt index 3976773..25ac7d7 100644 --- a/src/jvm/src/test/kotlin/FieldMatchTest.kt +++ b/src/core/src/test/kotlin/FieldMatchTest.kt @@ -1,13 +1,14 @@ -package solutions.bitbadger.documents +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("JVM | Kotlin | FieldMatch") +@DisplayName("Core | Kotlin | FieldMatch") class FieldMatchTest { @Test diff --git a/src/jvm/src/test/kotlin/FieldTest.kt b/src/core/src/test/kotlin/FieldTest.kt similarity index 99% rename from src/jvm/src/test/kotlin/FieldTest.kt rename to src/core/src/test/kotlin/FieldTest.kt index d7a616a..3753e33 100644 --- a/src/jvm/src/test/kotlin/FieldTest.kt +++ b/src/core/src/test/kotlin/FieldTest.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents +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.support.ForceDialect +import solutions.bitbadger.documents.* import kotlin.test.assertEquals import kotlin.test.assertNotSame import kotlin.test.assertNull @@ -12,7 +12,7 @@ import kotlin.test.assertNull /** * Unit tests for the `Field` class */ -@DisplayName("JVM | Kotlin | Field") +@DisplayName("Core | Kotlin | Field") class FieldTest { /** diff --git a/src/jvm/src/test/kotlin/query/FindQueryTest.kt b/src/core/src/test/kotlin/FindQueryTest.kt similarity index 93% rename from src/jvm/src/test/kotlin/query/FindQueryTest.kt rename to src/core/src/test/kotlin/FindQueryTest.kt index e458bf2..1f67bdd 100644 --- a/src/jvm/src/test/kotlin/query/FindQueryTest.kt +++ b/src/core/src/test/kotlin/FindQueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.core.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,14 +6,13 @@ 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.support.ForceDialect -import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.query.FindQuery import kotlin.test.assertEquals /** * Unit tests for the `Find` object */ -@DisplayName("JVM | Kotlin | Query | FindQuery") +@DisplayName("Core | Kotlin | Query | FindQuery") class FindQueryTest { /** diff --git a/src/jvm/src/test/kotlin/support/ForceDialect.kt b/src/core/src/test/kotlin/ForceDialect.kt similarity index 90% rename from src/jvm/src/test/kotlin/support/ForceDialect.kt rename to src/core/src/test/kotlin/ForceDialect.kt index 681361c..d77dee1 100644 --- a/src/jvm/src/test/kotlin/support/ForceDialect.kt +++ b/src/core/src/test/kotlin/ForceDialect.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.support +package solutions.bitbadger.documents.core.tests import solutions.bitbadger.documents.Configuration diff --git a/src/jvm/src/test/kotlin/OpTest.kt b/src/core/src/test/kotlin/OpTest.kt similarity index 94% rename from src/jvm/src/test/kotlin/OpTest.kt rename to src/core/src/test/kotlin/OpTest.kt index 19e0d91..1011512 100644 --- a/src/jvm/src/test/kotlin/OpTest.kt +++ b/src/core/src/test/kotlin/OpTest.kt @@ -1,13 +1,14 @@ -package solutions.bitbadger.documents +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("JVM | Kotlin | Op") +@DisplayName("Core | Kotlin | Op") class OpTest { @Test diff --git a/src/jvm/src/test/kotlin/ParameterNameTest.kt b/src/core/src/test/kotlin/ParameterNameTest.kt similarity index 87% rename from src/jvm/src/test/kotlin/ParameterNameTest.kt rename to src/core/src/test/kotlin/ParameterNameTest.kt index 4e67e4a..f6c6f51 100644 --- a/src/jvm/src/test/kotlin/ParameterNameTest.kt +++ b/src/core/src/test/kotlin/ParameterNameTest.kt @@ -1,13 +1,14 @@ -package solutions.bitbadger.documents +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("JVM | Kotlin | ParameterName") +@DisplayName("Core | Kotlin | ParameterName") class ParameterNameTest { @Test diff --git a/src/jvm/src/test/kotlin/ParameterTest.kt b/src/core/src/test/kotlin/ParameterTest.kt similarity index 83% rename from src/jvm/src/test/kotlin/ParameterTest.kt rename to src/core/src/test/kotlin/ParameterTest.kt index 0ec84ae..7b26ebe 100644 --- a/src/jvm/src/test/kotlin/ParameterTest.kt +++ b/src/core/src/test/kotlin/ParameterTest.kt @@ -1,7 +1,10 @@ -package solutions.bitbadger.documents +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 @@ -9,7 +12,7 @@ import kotlin.test.assertNull /** * Unit tests for the `Parameter` class */ -@DisplayName("JVM | Kotlin | Parameter") +@DisplayName("Core | Kotlin | Parameter") class ParameterTest { @Test diff --git a/src/jvm/src/test/kotlin/jvm/ParametersTest.kt b/src/core/src/test/kotlin/ParametersTest.kt similarity index 97% rename from src/jvm/src/test/kotlin/jvm/ParametersTest.kt rename to src/core/src/test/kotlin/ParametersTest.kt index 33a1af2..15dcddc 100644 --- a/src/jvm/src/test/kotlin/jvm/ParametersTest.kt +++ b/src/core/src/test/kotlin/ParametersTest.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.jvm +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.support.ForceDialect +import solutions.bitbadger.documents.java.Parameters import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotSame @@ -13,7 +13,7 @@ import kotlin.test.assertSame /** * Unit tests for the `Parameters` object */ -@DisplayName("JVM | Kotlin | Parameters") +@DisplayName("Core | Kotlin | Parameters") class ParametersTest { /** diff --git a/src/jvm/src/test/kotlin/query/PatchQueryTest.kt b/src/core/src/test/kotlin/PatchQueryTest.kt similarity index 93% rename from src/jvm/src/test/kotlin/query/PatchQueryTest.kt rename to src/core/src/test/kotlin/PatchQueryTest.kt index dbb4169..0d17457 100644 --- a/src/jvm/src/test/kotlin/query/PatchQueryTest.kt +++ b/src/core/src/test/kotlin/PatchQueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.core.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,14 +6,13 @@ 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.support.ForceDialect -import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.query.PatchQuery import kotlin.test.assertEquals /** * Unit tests for the `Patch` object */ -@DisplayName("JVM | Kotlin | Query | PatchQuery") +@DisplayName("Core | Kotlin | Query | PatchQuery") class PatchQueryTest { /** diff --git a/src/jvm/src/test/kotlin/query/QueryTest.kt b/src/core/src/test/kotlin/QueryTest.kt similarity index 97% rename from src/jvm/src/test/kotlin/query/QueryTest.kt rename to src/core/src/test/kotlin/QueryTest.kt index 1d3ec1d..3bd65f6 100644 --- a/src/jvm/src/test/kotlin/query/QueryTest.kt +++ b/src/core/src/test/kotlin/QueryTest.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.query +package solutions.bitbadger.documents.core.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,13 +6,13 @@ 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.support.ForceDialect +import solutions.bitbadger.documents.query.* import kotlin.test.assertEquals /** * Unit tests for the package-level query functions */ -@DisplayName("JVM | Kotlin | Query | Package Functions") +@DisplayName("Core | Kotlin | Query | Package Functions") class QueryTest { /** diff --git a/src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt b/src/core/src/test/kotlin/RemoveFieldsQueryTest.kt similarity index 94% rename from src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt rename to src/core/src/test/kotlin/RemoveFieldsQueryTest.kt index 06a2523..fde9db1 100644 --- a/src/jvm/src/test/kotlin/query/RemoveFieldsQueryTest.kt +++ b/src/core/src/test/kotlin/RemoveFieldsQueryTest.kt @@ -1,18 +1,17 @@ -package solutions.bitbadger.documents.query +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.support.ForceDialect -import solutions.bitbadger.documents.support.TEST_TABLE +import solutions.bitbadger.documents.query.RemoveFieldsQuery import kotlin.test.assertEquals /** * Unit tests for the `RemoveFields` object */ -@DisplayName("JVM | Kotlin | Query | RemoveFieldsQuery") +@DisplayName("Core | Kotlin | Query | RemoveFieldsQuery") class RemoveFieldsQueryTest { /** diff --git a/src/kotlin/src/test/kotlin/Types.kt b/src/core/src/test/kotlin/Types.kt similarity index 75% rename from src/kotlin/src/test/kotlin/Types.kt rename to src/core/src/test/kotlin/Types.kt index 51d0b38..cc222fa 100644 --- a/src/kotlin/src/test/kotlin/Types.kt +++ b/src/core/src/test/kotlin/Types.kt @@ -1,22 +1,19 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.core.tests -import kotlinx.serialization.Serializable -import solutions.bitbadger.documents.extensions.insert +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase +import solutions.bitbadger.documents.java.Document -/** The test table name to use for integration tests */ +/** The test table name to use for unit/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()) @@ -31,7 +28,6 @@ data class ArrayDocument(val id: String, val values: List) { } } -@Serializable data class JsonDocument(val id: String, val value: String = "", val numValue: Int = 0, val sub: SubDocument? = null) { constructor() : this("") @@ -46,7 +42,7 @@ data class JsonDocument(val id: String, val value: String = "", val numValue: In JsonDocument("five", "purple", 18, null) ) -// fun load(db: ThrowawayDatabase, tableName: String = TEST_TABLE) = -// testDocuments.forEach { db.conn.insert(tableName, it) } + fun load(db: ThrowawayDatabase, tableName: String = TEST_TABLE) = + testDocuments.forEach { Document.insert(tableName, it, db.conn) } } } diff --git a/src/jvm/src/test/kotlin/query/WhereTest.kt b/src/core/src/test/kotlin/WhereTest.kt similarity index 97% rename from src/jvm/src/test/kotlin/query/WhereTest.kt rename to src/core/src/test/kotlin/WhereTest.kt index c436ec8..58cc59d 100644 --- a/src/jvm/src/test/kotlin/query/WhereTest.kt +++ b/src/core/src/test/kotlin/WhereTest.kt @@ -1,17 +1,17 @@ -package solutions.bitbadger.documents.query +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.support.ForceDialect +import solutions.bitbadger.documents.query.Where import kotlin.test.assertEquals /** * Unit tests for the `Where` object */ -@DisplayName("JVM | Kotlin | Query | Where") +@DisplayName("Core | Kotlin | Query | Where") class WhereTest { /** diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/Count.kt b/src/core/src/test/kotlin/integration/CountFunctions.kt similarity index 88% rename from src/jvm/src/test/kotlin/jvm/integration/common/Count.kt rename to src/core/src/test/kotlin/integration/CountFunctions.kt index 5ed8ecc..48155bf 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Count.kt +++ b/src/core/src/test/kotlin/integration/CountFunctions.kt @@ -1,14 +1,15 @@ -package solutions.bitbadger.documents.jvm.integration.common +package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.extensions.* -import solutions.bitbadger.documents.support.* +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 Count { +object CountFunctions { fun all(db: ThrowawayDatabase) { JsonDocument.load(db) diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt b/src/core/src/test/kotlin/integration/CustomFunctions.kt similarity index 73% rename from src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt rename to src/core/src/test/kotlin/integration/CustomFunctions.kt index 19e0404..e25fff4 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Custom.kt +++ b/src/core/src/test/kotlin/integration/CustomFunctions.kt @@ -1,44 +1,54 @@ -package solutions.bitbadger.documents.jvm.integration.common +package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.extensions.* -import solutions.bitbadger.documents.jvm.Results +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.support.* -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull +import kotlin.test.* /** * Integration tests for the `Custom` object */ -object Custom { +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) + 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) + 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 singleNone(db: ThrowawayDatabase) = - assertNull( - db.conn.customSingle(FindQuery.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData), + 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) - assertNotNull( - db.conn.customSingle(FindQuery.all(TEST_TABLE), listOf(), JsonDocument::class.java, Results::fromData), + assertTrue( + db.conn.customSingle( + FindQuery.all(TEST_TABLE), + listOf(), + JsonDocument::class.java, + Results::fromData + ).isPresent, "There should not have been a document returned" ) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/Definition.kt b/src/core/src/test/kotlin/integration/DefinitionFunctions.kt similarity index 84% rename from src/jvm/src/test/kotlin/jvm/integration/common/Definition.kt rename to src/core/src/test/kotlin/integration/DefinitionFunctions.kt index 0d7aa26..dde6574 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Definition.kt +++ b/src/core/src/test/kotlin/integration/DefinitionFunctions.kt @@ -1,18 +1,15 @@ -package solutions.bitbadger.documents.jvm.integration.common +package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.DocumentIndex -import solutions.bitbadger.documents.extensions.ensureDocumentIndex -import solutions.bitbadger.documents.extensions.ensureFieldIndex -import solutions.bitbadger.documents.extensions.ensureTable -import solutions.bitbadger.documents.support.TEST_TABLE -import solutions.bitbadger.documents.support.ThrowawayDatabase +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 Definition { +object DefinitionFunctions { fun ensureTable(db: ThrowawayDatabase) { assertFalse(db.dbObjectExists("ensured"), "The 'ensured' table should not exist") diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/Delete.kt b/src/core/src/test/kotlin/integration/DeleteFunctions.kt similarity index 90% rename from src/jvm/src/test/kotlin/jvm/integration/common/Delete.kt rename to src/core/src/test/kotlin/integration/DeleteFunctions.kt index e0ceada..919947a 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Delete.kt +++ b/src/core/src/test/kotlin/integration/DeleteFunctions.kt @@ -1,16 +1,14 @@ -package solutions.bitbadger.documents.jvm.integration.common +package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.extensions.* -import solutions.bitbadger.documents.support.JsonDocument -import solutions.bitbadger.documents.support.TEST_TABLE -import solutions.bitbadger.documents.support.ThrowawayDatabase +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* import kotlin.test.assertEquals /** * Integration tests for the `Delete` object */ -object Delete { +object DeleteFunctions { fun byIdMatch(db: ThrowawayDatabase) { JsonDocument.load(db) diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/Document.kt b/src/core/src/test/kotlin/integration/DocumentFunctions.kt similarity index 86% rename from src/jvm/src/test/kotlin/jvm/integration/common/Document.kt rename to src/core/src/test/kotlin/integration/DocumentFunctions.kt index 5b8e7ec..dee2043 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Document.kt +++ b/src/core/src/test/kotlin/integration/DocumentFunctions.kt @@ -1,16 +1,18 @@ -package solutions.bitbadger.documents.jvm.integration.common +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.extensions.* -import solutions.bitbadger.documents.support.* +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 Document { +object DocumentFunctions { fun insertDefault(db: ThrowawayDatabase) { assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") @@ -23,11 +25,8 @@ object Document { fun insertDupe(db: ThrowawayDatabase) { db.conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) - try { + assertThrows("Inserting a document with a duplicate key should have thrown an exception") { db.conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) - fail("Inserting a document with a duplicate key should have thrown an exception") - } catch (_: Exception) { - // yay } } @@ -92,8 +91,9 @@ object Document { fun saveMatch(db: ThrowawayDatabase) { JsonDocument.load(db) db.conn.save(TEST_TABLE, JsonDocument("two", numValue = 44)) - val doc = db.conn.findById(TEST_TABLE, "two", JsonDocument::class.java) - assertNotNull(doc, "There should have been a document returned") + 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") @@ -103,8 +103,8 @@ object Document { 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", JsonDocument::class.java), + assertTrue( + db.conn.findById(TEST_TABLE, "test", JsonDocument::class.java).isPresent, "The test document should have been saved" ) } @@ -112,8 +112,9 @@ object Document { 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", JsonDocument::class.java) - assertNotNull(doc, "There should have been a document returned") + 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") diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/Exists.kt b/src/core/src/test/kotlin/integration/ExistsFunctions.kt similarity index 91% rename from src/jvm/src/test/kotlin/jvm/integration/common/Exists.kt rename to src/core/src/test/kotlin/integration/ExistsFunctions.kt index a981f81..194aefb 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Exists.kt +++ b/src/core/src/test/kotlin/integration/ExistsFunctions.kt @@ -1,15 +1,15 @@ -package solutions.bitbadger.documents.jvm.integration.common +package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.extensions.* -import solutions.bitbadger.documents.support.* +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 Exists { +object ExistsFunctions { fun byIdMatch(db: ThrowawayDatabase) { JsonDocument.load(db) diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/Find.kt b/src/core/src/test/kotlin/integration/FindFunctions.kt similarity index 79% rename from src/jvm/src/test/kotlin/jvm/integration/common/Find.kt rename to src/core/src/test/kotlin/integration/FindFunctions.kt index 03aaa94..23e14a1 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Find.kt +++ b/src/core/src/test/kotlin/integration/FindFunctions.kt @@ -1,19 +1,16 @@ -package solutions.bitbadger.documents.jvm.integration.common +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.extensions.* -import solutions.bitbadger.documents.support.* -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue +import solutions.bitbadger.documents.core.tests.* +import solutions.bitbadger.documents.java.extensions.* +import kotlin.test.* /** * Integration tests for the `Find` object */ -object Find { +object FindFunctions { fun allDefault(db: ThrowawayDatabase) { JsonDocument.load(db) @@ -71,8 +68,8 @@ object Find { fun byIdString(db: ThrowawayDatabase) { JsonDocument.load(db) val doc = db.conn.findById(TEST_TABLE, "two", JsonDocument::class.java) - assertNotNull(doc, "The document should have been returned") - assertEquals("two", doc.id, "An incorrect document was returned") + assertTrue(doc.isPresent, "The document should have been returned") + assertEquals("two", doc.get().id, "An incorrect document was returned") } fun byIdNumber(db: ThrowawayDatabase) { @@ -80,7 +77,7 @@ object Find { try { db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) val doc = db.conn.findById(TEST_TABLE, 18, NumIdDocument::class.java) - assertNotNull(doc, "The document should have been returned") + assertTrue(doc.isPresent, "The document should have been returned") } finally { Configuration.idField = "id" } @@ -88,8 +85,8 @@ object Find { fun byIdNotFound(db: ThrowawayDatabase) { JsonDocument.load(db) - assertNull( - db.conn.findById(TEST_TABLE, "x", JsonDocument::class.java), + assertFalse( + db.conn.findById(TEST_TABLE, "x", JsonDocument::class.java).isPresent, "There should have been no document returned" ) } @@ -147,8 +144,8 @@ object Find { 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}") + 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) { @@ -168,8 +165,8 @@ object Find { 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}") + 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) { @@ -197,8 +194,8 @@ object Find { 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}") + 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) { @@ -226,16 +223,16 @@ object Find { JsonDocument.load(db) val doc = db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another")), JsonDocument::class.java) - assertNotNull(doc, "There should have been a document returned") - assertEquals("two", doc.id, "The incorrect document was returned") + 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) - assertNotNull(doc, "There should have been a document returned") - assertTrue(listOf("two", "four").contains(doc.id), "An incorrect document was returned (${doc.id}") + 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) { @@ -245,14 +242,18 @@ object Find { Field.named("n:numValue DESC") ) ) - assertNotNull(doc, "There should have been a document returned") - assertEquals("four", doc.id, "An incorrect document was returned") + 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) - assertNull( - db.conn.findFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent")), JsonDocument::class.java), + assertFalse( + db.conn.findFirstByFields( + TEST_TABLE, + listOf(Field.equal("value", "absent")), + JsonDocument::class.java + ).isPresent, "There should have been no document returned" ) } @@ -260,15 +261,18 @@ object Find { fun firstByContainsMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) val doc = db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!"), JsonDocument::class.java) - assertNotNull(doc, "There should have been a document returned") - assertEquals("one", doc.id, "An incorrect document was returned") + 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) - assertNotNull(doc, "There should have been a document returned") - assertTrue(listOf("four", "five").contains(doc.id), "An incorrect document was returned (${doc.id}") + 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) { @@ -279,14 +283,14 @@ object Find { JsonDocument::class.java, 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") + 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) - assertNull( - db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "indigo"), JsonDocument::class.java), + assertFalse( + db.conn.findFirstByContains(TEST_TABLE, mapOf("value" to "indigo"), JsonDocument::class.java).isPresent, "There should have been no document returned" ) } @@ -294,15 +298,18 @@ object Find { fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) val doc = db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)", JsonDocument::class.java) - assertNotNull(doc, "There should have been a document returned") - assertEquals("two", doc.id, "An incorrect document was returned") + 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) - assertNotNull(doc, "There should have been a document returned") - assertTrue(listOf("four", "five").contains(doc.id), "An incorrect document was returned (${doc.id}") + 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) { @@ -313,14 +320,14 @@ object Find { JsonDocument::class.java, listOf(Field.named("id DESC")) ) - assertNotNull(doc, "There should have been a document returned") - assertEquals("four", doc.id, "An incorrect document was returned") + 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) - assertNull( - db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument::class.java), + assertFalse( + db.conn.findFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument::class.java).isPresent, "There should have been no document returned" ) } diff --git a/src/jvm/src/test/kotlin/support/JacksonDocumentSerializer.kt b/src/core/src/test/kotlin/integration/JacksonDocumentSerializer.kt similarity index 68% rename from src/jvm/src/test/kotlin/support/JacksonDocumentSerializer.kt rename to src/core/src/test/kotlin/integration/JacksonDocumentSerializer.kt index fb211c2..c3d15db 100644 --- a/src/jvm/src/test/kotlin/support/JacksonDocumentSerializer.kt +++ b/src/core/src/test/kotlin/integration/JacksonDocumentSerializer.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.support +package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.DocumentSerializer import com.fasterxml.jackson.databind.ObjectMapper @@ -8,11 +8,11 @@ import com.fasterxml.jackson.databind.ObjectMapper */ class JacksonDocumentSerializer : DocumentSerializer { - val mapper = ObjectMapper() + private val mapper = ObjectMapper() - override fun serialize(document: TDoc) = + override fun serialize(document: TDoc): String = mapper.writeValueAsString(document) - override fun deserialize(json: String, clazz: Class) = + override fun deserialize(json: String, clazz: Class): TDoc = mapper.readValue(json, clazz) } 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/jvm/src/test/kotlin/jvm/integration/postgresql/PgDB.kt b/src/core/src/test/kotlin/integration/PgDB.kt similarity index 82% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/PgDB.kt rename to src/core/src/test/kotlin/integration/PgDB.kt index 6f60f71..4586d5f 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/PgDB.kt +++ b/src/core/src/test/kotlin/integration/PgDB.kt @@ -1,11 +1,10 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.extensions.customNonQuery -import solutions.bitbadger.documents.extensions.customScalar -import solutions.bitbadger.documents.extensions.ensureTable -import solutions.bitbadger.documents.jvm.* -import solutions.bitbadger.documents.support.* +import solutions.bitbadger.documents.core.tests.TEST_TABLE +import solutions.bitbadger.documents.java.DocumentConfig +import solutions.bitbadger.documents.java.Results +import solutions.bitbadger.documents.java.extensions.* /** * A wrapper for a throwaway PostgreSQL database 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..4fa7fb6 --- /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("Java | 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..8e943db --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -0,0 +1,47 @@ +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("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("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/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt similarity index 58% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt rename to src/core/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt index bb01c6c..e9f71ec 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DefinitionIT.kt +++ b/src/core/src/test/kotlin/integration/PostgreSQLDefinitionIT.kt @@ -1,32 +1,31 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.core.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Definition import kotlin.test.Test /** * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: Definition") -class DefinitionIT { +@DisplayName("Core | Kotlin | PostgreSQL: Definition") +class PostgreSQLDefinitionIT { @Test @DisplayName("ensureTable creates table and index") fun ensureTable() = - PgDB().use(Definition::ensureTable) + PgDB().use(DefinitionFunctions::ensureTable) @Test @DisplayName("ensureFieldIndex creates an index") fun ensureFieldIndex() = - PgDB().use(Definition::ensureFieldIndex) + PgDB().use(DefinitionFunctions::ensureFieldIndex) @Test @DisplayName("ensureDocumentIndex creates a full index") fun ensureDocumentIndexFull() = - PgDB().use(Definition::ensureDocumentIndexFull) + PgDB().use(DefinitionFunctions::ensureDocumentIndexFull) @Test @DisplayName("ensureDocumentIndex creates an optimized index") fun ensureDocumentIndexOptimized() = - PgDB().use(Definition::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/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/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt b/src/core/src/test/kotlin/integration/SQLiteCountIT.kt similarity index 59% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt rename to src/core/src/test/kotlin/integration/SQLiteCountIT.kt index 566cb4b..7e2aece 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/CountIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteCountIT.kt @@ -1,41 +1,40 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +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 solutions.bitbadger.documents.jvm.integration.common.Count import kotlin.test.Test /** * SQLite integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: Count") -class CountIT { +@DisplayName("Java | Kotlin | SQLite: Count") +class SQLiteCountIT { @Test @DisplayName("all counts all documents") fun all() = - SQLiteDB().use(Count::all) + SQLiteDB().use(CountFunctions::all) @Test @DisplayName("byFields counts documents by a numeric value") fun byFieldsNumeric() = - SQLiteDB().use(Count::byFieldsNumeric) + SQLiteDB().use(CountFunctions::byFieldsNumeric) @Test @DisplayName("byFields counts documents by a alphanumeric value") fun byFieldsAlpha() = - SQLiteDB().use(Count::byFieldsAlpha) + SQLiteDB().use(CountFunctions::byFieldsAlpha) @Test @DisplayName("byContains fails") fun byContainsMatch() { - assertThrows { SQLiteDB().use(Count::byContainsMatch) } + assertThrows { SQLiteDB().use(CountFunctions::byContainsMatch) } } @Test @DisplayName("byJsonPath fails") fun byJsonPathMatch() { - assertThrows { SQLiteDB().use(Count::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..470b040 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -0,0 +1,46 @@ +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("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("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/jvm/src/test/kotlin/jvm/integration/sqlite/SQLiteDB.kt b/src/core/src/test/kotlin/integration/SQLiteDB.kt similarity index 74% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/SQLiteDB.kt rename to src/core/src/test/kotlin/integration/SQLiteDB.kt index 609124e..1c930a0 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/SQLiteDB.kt +++ b/src/core/src/test/kotlin/integration/SQLiteDB.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.extensions.customScalar -import solutions.bitbadger.documents.extensions.ensureTable -import solutions.bitbadger.documents.jvm.* -import solutions.bitbadger.documents.support.* +import solutions.bitbadger.documents.core.tests.TEST_TABLE +import solutions.bitbadger.documents.java.DocumentConfig +import solutions.bitbadger.documents.java.Results +import solutions.bitbadger.documents.java.extensions.* import java.io.File /** diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt b/src/core/src/test/kotlin/integration/SQLiteDefinitionIT.kt similarity index 68% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt rename to src/core/src/test/kotlin/integration/SQLiteDefinitionIT.kt index bc47dc0..9f0ff14 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DefinitionIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteDefinitionIT.kt @@ -1,36 +1,35 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +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 solutions.bitbadger.documents.jvm.integration.common.Definition import kotlin.test.Test /** * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: Definition") -class DefinitionIT { +@DisplayName("Core | Kotlin | SQLite: Definition") +class SQLiteDefinitionIT { @Test @DisplayName("ensureTable creates table and index") fun ensureTable() = - SQLiteDB().use(Definition::ensureTable) + SQLiteDB().use(DefinitionFunctions::ensureTable) @Test @DisplayName("ensureFieldIndex creates an index") fun ensureFieldIndex() = - SQLiteDB().use(Definition::ensureFieldIndex) + SQLiteDB().use(DefinitionFunctions::ensureFieldIndex) @Test @DisplayName("ensureDocumentIndex fails for full index") fun ensureDocumentIndexFull() { - assertThrows { SQLiteDB().use(Definition::ensureDocumentIndexFull) } + assertThrows { SQLiteDB().use(DefinitionFunctions::ensureDocumentIndexFull) } } @Test @DisplayName("ensureDocumentIndex fails for optimized index") fun ensureDocumentIndexOptimized() { - assertThrows { SQLiteDB().use(Definition::ensureDocumentIndexOptimized) } + assertThrows { SQLiteDB().use(DefinitionFunctions::ensureDocumentIndexOptimized) } } } diff --git a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt b/src/core/src/test/kotlin/integration/SQLiteDeleteIT.kt similarity index 59% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt rename to src/core/src/test/kotlin/integration/SQLiteDeleteIT.kt index bbdda99..fa1acad 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DeleteIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteDeleteIT.kt @@ -1,46 +1,45 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +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 solutions.bitbadger.documents.jvm.integration.common.Delete import kotlin.test.Test /** * SQLite integration tests for the `Delete` object / `deleteBy*` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: Delete") -class DeleteIT { +@DisplayName("Core | Kotlin | SQLite: Delete") +class SQLiteDeleteIT { @Test @DisplayName("byId deletes a matching ID") fun byIdMatch() = - SQLiteDB().use(Delete::byIdMatch) + SQLiteDB().use(DeleteFunctions::byIdMatch) @Test @DisplayName("byId succeeds when no ID matches") fun byIdNoMatch() = - SQLiteDB().use(Delete::byIdNoMatch) + SQLiteDB().use(DeleteFunctions::byIdNoMatch) @Test @DisplayName("byFields deletes matching documents") fun byFieldsMatch() = - SQLiteDB().use(Delete::byFieldsMatch) + SQLiteDB().use(DeleteFunctions::byFieldsMatch) @Test @DisplayName("byFields succeeds when no documents match") fun byFieldsNoMatch() = - SQLiteDB().use(Delete::byFieldsNoMatch) + SQLiteDB().use(DeleteFunctions::byFieldsNoMatch) @Test @DisplayName("byContains fails") fun byContainsFails() { - assertThrows { SQLiteDB().use(Delete::byContainsMatch) } + assertThrows { SQLiteDB().use(DeleteFunctions::byContainsMatch) } } @Test @DisplayName("byJsonPath fails") fun byJsonPathFails() { - assertThrows { SQLiteDB().use(Delete::byJsonPathMatch) } + 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/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt b/src/core/src/test/kotlin/integration/SQLiteExistsIT.kt similarity index 61% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt rename to src/core/src/test/kotlin/integration/SQLiteExistsIT.kt index b4b65f2..e4ad1a9 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/ExistsIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteExistsIT.kt @@ -1,46 +1,45 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +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 solutions.bitbadger.documents.jvm.integration.common.Exists import kotlin.test.Test /** * SQLite integration tests for the `Exists` object / `existsBy*` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: Exists") -class ExistsIT { +@DisplayName("Core | Kotlin | SQLite: Exists") +class SQLiteExistsIT { @Test @DisplayName("byId returns true when a document matches the ID") fun byIdMatch() = - SQLiteDB().use(Exists::byIdMatch) + SQLiteDB().use(ExistsFunctions::byIdMatch) @Test @DisplayName("byId returns false when no document matches the ID") fun byIdNoMatch() = - SQLiteDB().use(Exists::byIdNoMatch) + SQLiteDB().use(ExistsFunctions::byIdNoMatch) @Test @DisplayName("byFields returns true when documents match") fun byFieldsMatch() = - SQLiteDB().use(Exists::byFieldsMatch) + SQLiteDB().use(ExistsFunctions::byFieldsMatch) @Test @DisplayName("byFields returns false when no documents match") fun byFieldsNoMatch() = - SQLiteDB().use(Exists::byFieldsNoMatch) + SQLiteDB().use(ExistsFunctions::byFieldsNoMatch) @Test @DisplayName("byContains fails") fun byContainsFails() { - assertThrows { SQLiteDB().use(Exists::byContainsMatch) } + assertThrows { SQLiteDB().use(ExistsFunctions::byContainsMatch) } } @Test @DisplayName("byJsonPath fails") fun byJsonPathFails() { - assertThrows { SQLiteDB().use(Exists::byJsonPathMatch) } + 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/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt b/src/core/src/test/kotlin/integration/SQLitePatchIT.kt similarity index 60% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt rename to src/core/src/test/kotlin/integration/SQLitePatchIT.kt index 7918bef..abbdcb0 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/PatchIT.kt +++ b/src/core/src/test/kotlin/integration/SQLitePatchIT.kt @@ -1,46 +1,45 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +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 solutions.bitbadger.documents.jvm.integration.common.Patch import kotlin.test.Test /** * SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: Patch") -class PatchIT { +@DisplayName("Core | Kotlin | SQLite: Patch") +class SQLitePatchIT { @Test @DisplayName("byId patches an existing document") fun byIdMatch() = - SQLiteDB().use(Patch::byIdMatch) + SQLiteDB().use(PatchFunctions::byIdMatch) @Test @DisplayName("byId succeeds for a non-existent document") fun byIdNoMatch() = - SQLiteDB().use(Patch::byIdNoMatch) + SQLiteDB().use(PatchFunctions::byIdNoMatch) @Test @DisplayName("byFields patches matching document") fun byFieldsMatch() = - SQLiteDB().use(Patch::byFieldsMatch) + SQLiteDB().use(PatchFunctions::byFieldsMatch) @Test @DisplayName("byFields succeeds when no documents match") fun byFieldsNoMatch() = - SQLiteDB().use(Patch::byFieldsNoMatch) + SQLiteDB().use(PatchFunctions::byFieldsNoMatch) @Test @DisplayName("byContains fails") fun byContainsFails() { - assertThrows { SQLiteDB().use(Patch::byContainsMatch) } + assertThrows { SQLiteDB().use(PatchFunctions::byContainsMatch) } } @Test @DisplayName("byJsonPath fails") fun byJsonPathFails() { - assertThrows { SQLiteDB().use(Patch::byJsonPathMatch) } + 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/jvm/src/test/kotlin/support/ThrowawayDatabase.kt b/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt similarity index 88% rename from src/jvm/src/test/kotlin/support/ThrowawayDatabase.kt rename to src/core/src/test/kotlin/integration/ThrowawayDatabase.kt index 9c63c30..4f454d1 100644 --- a/src/jvm/src/test/kotlin/support/ThrowawayDatabase.kt +++ b/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.support +package solutions.bitbadger.documents.core.tests.integration import java.sql.Connection 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..22d1d7c --- /dev/null +++ b/src/groovy/pom.xml @@ -0,0 +1,109 @@ + + + 4.0.0 + + solutions.bitbadger + documents + 4.0.0-alpha1-SNAPSHOT + ../../pom.xml + + + solutions.bitbadger.documents + groovy + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (Groovy Library) + https://bitbadger.solutions/open-source/relational-documents/jvm/ + + + + + 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} + + + + + + + + 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..4bba727 --- /dev/null +++ b/src/groovy/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module solutions.bitbadger.documents.groovy { + requires solutions.bitbadger.documents.core; +} 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/AutoIdTest.groovy similarity index 97% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/AutoIdTest.groovy index 1cecfa8..f4501e7 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/AutoIdTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/AutoIdTest.groovy @@ -1,17 +1,16 @@ -package solutions.bitbadger.documents.groovy +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 solutions.bitbadger.documents.groovy.support.* import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `AutoId` enum */ -@DisplayName('JVM | Groovy | AutoId') +@DisplayName('Groovy | AutoId') class AutoIdTest { @Test diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ByteIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ByteIdClass.groovy similarity index 62% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ByteIdClass.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ByteIdClass.groovy index 62442f6..f8506c7 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ByteIdClass.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ByteIdClass.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.support +package solutions.bitbadger.documents.groovy.tests class ByteIdClass { byte id diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ConfigurationTest.groovy similarity index 93% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ConfigurationTest.groovy index 87d7383..6dc8f3a 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ConfigurationTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ConfigurationTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -12,7 +12,7 @@ import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Configuration` object */ -@DisplayName('JVM | Groovy | Configuration') +@DisplayName('Groovy | Configuration') class ConfigurationTest { @Test diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/CountQueryTest.groovy similarity index 91% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/CountQueryTest.groovy index 95d62b5..10f69f4 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/CountQueryTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/CountQueryTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,15 +6,14 @@ import org.junit.jupiter.api.Test import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.query.CountQuery -import solutions.bitbadger.documents.support.ForceDialect -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static Types.TEST_TABLE import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Count` object */ -@DisplayName('JVM | Groovy | Query | CountQuery') +@DisplayName('Groovy | Query | CountQuery') class CountQueryTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DefinitionQueryTest.groovy similarity index 95% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DefinitionQueryTest.groovy index 6ce06fb..347d90c 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DefinitionQueryTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DefinitionQueryTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -7,15 +7,14 @@ import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.DocumentIndex import solutions.bitbadger.documents.query.DefinitionQuery -import solutions.bitbadger.documents.support.ForceDialect -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static Types.TEST_TABLE import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Definition` object */ -@DisplayName('JVM | Groovy | Query | DefinitionQuery') +@DisplayName('Groovy | Query | DefinitionQuery') class DefinitionQueryTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DeleteQueryTest.groovy similarity index 92% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DeleteQueryTest.groovy index 4b16942..c2a4128 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DeleteQueryTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DeleteQueryTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,15 +6,14 @@ import org.junit.jupiter.api.Test import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.query.DeleteQuery -import solutions.bitbadger.documents.support.ForceDialect -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static Types.TEST_TABLE import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Delete` object */ -@DisplayName('JVM | Groovy | Query | DeleteQuery') +@DisplayName('Groovy | Query | DeleteQuery') class DeleteQueryTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DialectTest.groovy similarity index 94% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DialectTest.groovy index 2aae920..f1905ba 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DialectTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DialectTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -10,7 +10,7 @@ import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Dialect` enum */ -@DisplayName('JVM | Groovy | Dialect') +@DisplayName('Groovy | Dialect') class DialectTest { @Test diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentIndexTest.groovy similarity index 87% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentIndexTest.groovy index a79aa5e..26f54d7 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/DocumentIndexTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentIndexTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals /** * Unit tests for the `DocumentIndex` enum */ -@DisplayName('JVM | Groovy | DocumentIndex') +@DisplayName('Groovy | DocumentIndex') class DocumentIndexTest { @Test diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentQueryTest.groovy similarity index 95% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentQueryTest.groovy index 29edebb..d912547 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/DocumentQueryTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/DocumentQueryTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -7,15 +7,14 @@ import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.query.DocumentQuery -import solutions.bitbadger.documents.support.ForceDialect -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static Types.TEST_TABLE import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Document` object */ -@DisplayName('JVM | Groovy | Query | DocumentQuery') +@DisplayName('Groovy | Query | DocumentQuery') class DocumentQueryTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldMatchTest.groovy similarity index 86% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldMatchTest.groovy index f1078e7..2be46d8 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldMatchTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldMatchTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals /** * Unit tests for the `FieldMatch` enum */ -@DisplayName('JVM | Groovy | FieldMatch') +@DisplayName('Groovy | FieldMatch') class FieldMatchTest { @Test diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldTest.groovy similarity index 99% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldTest.groovy index da2214e..82abe77 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/FieldTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FieldTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -8,14 +8,13 @@ import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldFormat import solutions.bitbadger.documents.Op -import solutions.bitbadger.documents.support.ForceDialect import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Field` class */ -@DisplayName('JVM | Groovy | Field') +@DisplayName('Groovy | Field') class FieldTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FindQueryTest.groovy similarity index 93% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FindQueryTest.groovy index 7d557c2..bec1d7b 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/FindQueryTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/FindQueryTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,15 +6,14 @@ import org.junit.jupiter.api.Test import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.query.FindQuery -import solutions.bitbadger.documents.support.ForceDialect -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static Types.TEST_TABLE import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Find` object */ -@DisplayName('JVM | Groovy | Query | FindQuery') +@DisplayName('Groovy | Query | FindQuery') class FindQueryTest { /** 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/IntIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/IntIdClass.groovy similarity index 61% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/IntIdClass.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/IntIdClass.groovy index 63d7f75..867b0ff 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/IntIdClass.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/IntIdClass.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.support +package solutions.bitbadger.documents.groovy.tests class IntIdClass { int id diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/LongIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/LongIdClass.groovy similarity index 62% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/LongIdClass.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/LongIdClass.groovy index 377fa1d..a6b1f07 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/LongIdClass.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/LongIdClass.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.support +package solutions.bitbadger.documents.groovy.tests class LongIdClass { long id diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/OpTest.groovy similarity index 96% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/OpTest.groovy index f4bd6e3..84c2d9e 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/OpTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/OpTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals /** * Unit tests for the `Op` enum */ -@DisplayName('JVM | Groovy | Op') +@DisplayName('Groovy | Op') class OpTest { @Test diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterNameTest.groovy similarity index 92% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterNameTest.groovy index 0acc5d0..947dac9 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterNameTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterNameTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -9,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals /** * Unit tests for the `ParameterName` class */ -@DisplayName('JVM | Groovy | ParameterName') +@DisplayName('Groovy | ParameterName') class ParameterNameTest { @Test diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterTest.groovy similarity index 93% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterTest.groovy index 76564b1..83d373a 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/ParameterTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParameterTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Parameter` class */ -@DisplayName('JVM | Groovy | Parameter') +@DisplayName('Groovy | Parameter') class ParameterTest { @Test diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParametersTest.groovy similarity index 96% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParametersTest.groovy index a8581a7..499a2d1 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/ParametersTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ParametersTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.jvm +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -7,15 +7,14 @@ import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.Parameter import solutions.bitbadger.documents.ParameterType -import solutions.bitbadger.documents.jvm.Parameters -import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.java.Parameters import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Parameters` object */ -@DisplayName('JVM | Groovy | Parameters') +@DisplayName('Groovy | Parameters') class ParametersTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/PatchQueryTest.groovy similarity index 92% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/PatchQueryTest.groovy index c362109..0a00a6f 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/PatchQueryTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/PatchQueryTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -6,15 +6,14 @@ import org.junit.jupiter.api.Test import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.query.PatchQuery -import solutions.bitbadger.documents.support.ForceDialect -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static Types.TEST_TABLE import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Patch` object */ -@DisplayName('JVM | Groovy | Query | PatchQuery') +@DisplayName('Groovy | Query | PatchQuery') class PatchQueryTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/QueryUtilsTest.groovy similarity index 97% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/QueryUtilsTest.groovy index 0643482..300eccf 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/QueryUtilsTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/QueryUtilsTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -7,14 +7,13 @@ import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch import solutions.bitbadger.documents.query.QueryUtils -import solutions.bitbadger.documents.support.ForceDialect import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `QueryUtils` class */ -@DisplayName('JVM | Groovy | Query | QueryUtils') +@DisplayName('Groovy | Query | QueryUtils') class QueryUtilsTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/RemoveFieldsQueryTest.groovy similarity index 94% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/RemoveFieldsQueryTest.groovy index 546b06c..0c4c5e2 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/RemoveFieldsQueryTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/RemoveFieldsQueryTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -8,15 +8,14 @@ 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.support.ForceDialect -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static Types.TEST_TABLE import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `RemoveFields` object */ -@DisplayName('JVM | Groovy | Query | RemoveFieldsQuery') +@DisplayName('Groovy | Query | RemoveFieldsQuery') class RemoveFieldsQueryTest { /** diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ShortIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ShortIdClass.groovy similarity index 63% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ShortIdClass.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ShortIdClass.groovy index fca6ea0..bf03a6a 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/ShortIdClass.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/ShortIdClass.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.support +package solutions.bitbadger.documents.groovy.tests class ShortIdClass { short id diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/StringIdClass.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/StringIdClass.groovy similarity index 64% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/StringIdClass.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/StringIdClass.groovy index 90e68e9..544cf37 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/StringIdClass.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/StringIdClass.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.support +package solutions.bitbadger.documents.groovy.tests class StringIdClass { String 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/WhereTest.groovy similarity index 97% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/WhereTest.groovy index e8fea87..170b6a6 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/query/WhereTest.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/WhereTest.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.query +package solutions.bitbadger.documents.groovy.tests import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.DisplayName @@ -7,14 +7,13 @@ 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.support.ForceDialect import static org.junit.jupiter.api.Assertions.* /** * Unit tests for the `Where` object */ -@DisplayName('JVM | Groovy | Query | Where') +@DisplayName('Groovy | Query | Where') class WhereTest { /** 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..72208e0 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/CustomFunctions.groovy @@ -0,0 +1,69 @@ +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 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 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 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy similarity index 71% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy index 78ba92e..71594e8 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/JsonDocument.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonDocument.groovy @@ -1,9 +1,6 @@ -package solutions.bitbadger.documents.groovy.support +package solutions.bitbadger.documents.groovy.tests.integration -import solutions.bitbadger.documents.jvm.Document -import solutions.bitbadger.documents.support.ThrowawayDatabase - -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +import static solutions.bitbadger.documents.groovy.tests.Types.TEST_TABLE class JsonDocument { String id @@ -26,6 +23,6 @@ class JsonDocument { new JsonDocument("five", "purple", 18)) static void load(ThrowawayDatabase db, String tableName = TEST_TABLE) { - testDocuments.forEach { Document.insert(tableName, it, db.conn) } + testDocuments.forEach { db.conn.insert(tableName, it) } } } 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..a980ef2 --- /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 value + + NumIdDocument(int key = 0, String value = "") { + this.key = key + this.value = value + } +} 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..cd7570d --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCustomIT.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 `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('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('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/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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CountIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCountIT.groovy similarity index 61% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CountIT.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCountIT.groovy index 0dd4dad..8afab08 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CountIT.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCountIT.groovy @@ -1,50 +1,48 @@ -package solutions.bitbadger.documents.groovy.jvm.integration.sqlite +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 solutions.bitbadger.documents.groovy.jvm.integration.common.CountFunctions -import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB import static org.junit.jupiter.api.Assertions.assertThrows /** * SQLite integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("JVM | Groovy | SQLite: Count") -class CountIT { +@DisplayName("Groovy | SQLite: Count") +final class SQLiteCountIT { @Test @DisplayName("all counts all documents") void all() { - new SQLiteDB().withCloseable { CountFunctions.all(it) } + new SQLiteDB().withCloseable CountFunctions.&all } @Test @DisplayName("byFields counts documents by a numeric value") void byFieldsNumeric() { - new SQLiteDB().withCloseable { CountFunctions.byFieldsNumeric(it) } + new SQLiteDB().withCloseable CountFunctions.&byFieldsNumeric } @Test @DisplayName("byFields counts documents by a alphanumeric value") void byFieldsAlpha() { - new SQLiteDB().withCloseable { CountFunctions.byFieldsAlpha(it) } + new SQLiteDB().withCloseable CountFunctions.&byFieldsAlpha } @Test @DisplayName("byContains fails") - void byContainsMatch() { + void byContainsFails() { new SQLiteDB().withCloseable { db -> - assertThrows(DocumentException) { CountFunctions.byContainsMatch(db) } + assertThrows(DocumentException) { CountFunctions.byContainsMatch db } } } @Test @DisplayName("byJsonPath fails") - void byJsonPathMatch() { + void byJsonPathFails() { new SQLiteDB().withCloseable { db -> - assertThrows(DocumentException) { CountFunctions.byJsonPathMatch(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..05d9f24 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCustomIT.groovy @@ -0,0 +1,53 @@ +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('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/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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SubDocument.groovy similarity index 71% rename from src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy rename to src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SubDocument.groovy index 744e498..a35722c 100644 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/support/SubDocument.groovy +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SubDocument.groovy @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.groovy.support +package solutions.bitbadger.documents.groovy.tests.integration class SubDocument { String foo 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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy deleted file mode 100644 index 673a7e4..0000000 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CountFunctions.groovy +++ /dev/null @@ -1,53 +0,0 @@ -package solutions.bitbadger.documents.groovy.jvm.integration.common - -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.groovy.support.JsonDocument -import solutions.bitbadger.documents.support.ThrowawayDatabase - -import static solutions.bitbadger.documents.extensions.ConnExt.* -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -import static org.junit.jupiter.api.Assertions.* - -class CountFunctions { - - static void all(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals 5L, countAll(db.conn, TEST_TABLE), 'There should have been 5 documents in the table' - } - - static void byFieldsNumeric(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals(3L, countByFields(db.conn, 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, countByFields(db.conn, 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, countByContains(db.conn, TEST_TABLE, Map.of('value', 'purple')), - 'There should have been 2 matching documents') - } - - static void byContainsNoMatch(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals(0L, countByContains(db.conn, TEST_TABLE, Map.of('value', 'magenta')), - 'There should have been no matching documents') - } - - static void byJsonPathMatch(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals(2L, countByJsonPath(db.conn, TEST_TABLE, '$.numValue ? (@ < 5)'), - 'There should have been 2 matching documents') - } - - static void byJsonPathNoMatch(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals(0L, countByJsonPath(db.conn, TEST_TABLE, '$.numValue ? (@ > 100)'), - 'There should have been no matching documents') - } -} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CustomFunctions.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CustomFunctions.groovy deleted file mode 100644 index 7147a8c..0000000 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/common/CustomFunctions.groovy +++ /dev/null @@ -1,72 +0,0 @@ -package solutions.bitbadger.documents.groovy.jvm.integration.common - -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.Parameter -import solutions.bitbadger.documents.ParameterType -import solutions.bitbadger.documents.groovy.support.JsonDocument -import solutions.bitbadger.documents.jvm.Results -import solutions.bitbadger.documents.query.CountQuery -import solutions.bitbadger.documents.query.DeleteQuery -import solutions.bitbadger.documents.query.FindQuery -import solutions.bitbadger.documents.support.ThrowawayDatabase - -import static org.junit.jupiter.api.Assertions.* -import static solutions.bitbadger.documents.extensions.ConnExt.* -import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -class CustomFunctions { - - static void listEmpty(ThrowawayDatabase db) { - JsonDocument.load(db) - deleteByFields(db.conn, TEST_TABLE, List.of(Field.exists(Configuration.idField))) - def result = customList(db.conn, FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results.&fromData) - assertEquals(0, result.size(), "There should have been no results") - } - - static void listAll(ThrowawayDatabase db) { - JsonDocument.load(db) - def result = customList(db.conn, FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results.&fromData) - assertEquals(5, result.size(), "There should have been 5 results") - } - - static void singleNone(ThrowawayDatabase db) { - assertNull(customSingle(db.conn, FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results.&fromData), - "There should not have been a document returned") - - } - - static void singleOne(ThrowawayDatabase db) { - JsonDocument.load(db) - assertNotNull( - customSingle(db.conn, FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results.&fromData), - "There should not have been a document returned") - } - - static void nonQueryChanges(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals(5L, customScalar(db.conn, CountQuery.all(TEST_TABLE), List.of(), Long.class, Results.&toCount), - "There should have been 5 documents in the table") - customNonQuery(db.conn, "DELETE FROM $TEST_TABLE") - assertEquals(0L, customScalar(db.conn, CountQuery.all(TEST_TABLE), List.of(), Long.class, Results.&toCount), - "There should have been no documents in the table") - } - - static void nonQueryNoChanges(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals(5L, customScalar(db.conn, CountQuery.all(TEST_TABLE), List.of(), Long.class, Results.&toCount), - "There should have been 5 documents in the table") - customNonQuery(db.conn, DeleteQuery.byId(TEST_TABLE, "eighty-two"), - List.of(new Parameter(":id", ParameterType.STRING, "eighty-two"))) - assertEquals(5L, customScalar(db.conn, CountQuery.all(TEST_TABLE), List.of(), Long.class, Results.&toCount), - "There should still have been 5 documents in the table") - } - - static void scalar(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals(3L, - customScalar(db.conn, "SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", List.of(), Long.class, - Results.&toCount), - "The number 3 should have been returned") - } -} diff --git a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy deleted file mode 100644 index 0763644..0000000 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CountIT.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package solutions.bitbadger.documents.groovy.jvm.integration.postgresql - -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.groovy.jvm.integration.common.CountFunctions -import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB - -/** - * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions - */ -@DisplayName("JVM | Groovy | PostgreSQL: Count") -class CountIT { - - @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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CustomIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CustomIT.groovy deleted file mode 100644 index 2250f01..0000000 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/postgresql/CustomIT.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package solutions.bitbadger.documents.groovy.jvm.integration.postgresql - -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.groovy.jvm.integration.common.CustomFunctions -import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB - -/** - * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions - */ -@DisplayName("JVM | Groovy | PostgreSQL: Custom") -class CustomIT { - - @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("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("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/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CustomIT.groovy b/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CustomIT.groovy deleted file mode 100644 index 8c3d155..0000000 --- a/src/jvm/src/test/groovy/solutions/bitbadger/documents/groovy/jvm/integration/sqlite/CustomIT.groovy +++ /dev/null @@ -1,55 +0,0 @@ -package solutions.bitbadger.documents.groovy.jvm.integration.sqlite - -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import solutions.bitbadger.documents.groovy.jvm.integration.common.CustomFunctions -import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB - -/** - * SQLite integration tests for the `Custom` object / `custom*` connection extension functions - */ -@DisplayName("JVM | Groovy | SQLite: Custom") -class CustomIT { - - @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("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/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CountFunctions.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CountFunctions.scala deleted file mode 100644 index 553a425..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CountFunctions.scala +++ /dev/null @@ -1,47 +0,0 @@ -package solutions.bitbadger.documents.scala.jvm.integration.common - -import org.junit.jupiter.api.Assertions.* -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.extensions.ConnExt.* -import solutions.bitbadger.documents.scala.support.JsonDocument -import solutions.bitbadger.documents.support.ThrowawayDatabase -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -import scala.jdk.CollectionConverters.* - -object CountFunctions { - - def all(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(5L, countAll(db.getConn, TEST_TABLE), "There should have been 5 documents in the table") - - def byFieldsNumeric(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(3L, countByFields(db.getConn, TEST_TABLE, (Field.between("numValue", 10, 20) :: Nil).asJava), - "There should have been 3 matching documents") - - def byFieldsAlpha(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(1L, countByFields(db.getConn, TEST_TABLE, (Field.between("value", "aardvark", "apple") :: Nil).asJava), - "There should have been 1 matching document") - - def byContainsMatch(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(2L, countByContains(db.getConn, TEST_TABLE, Map.Map1("value", "purple")), - "There should have been 2 matching documents") - - def byContainsNoMatch(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(0L, countByContains(db.getConn, TEST_TABLE, Map.Map1("value", "magenta")), - "There should have been no matching documents") - - def byJsonPathMatch(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(2L, countByJsonPath(db.getConn, TEST_TABLE, "$.numValue ? (@ < 5)"), - "There should have been 2 matching documents") - - def byJsonPathNoMatch(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(0L, countByJsonPath(db.getConn, TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no matching documents") -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CustomFunctions.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CustomFunctions.scala deleted file mode 100644 index 0d7e5a0..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/common/CustomFunctions.scala +++ /dev/null @@ -1,75 +0,0 @@ -package solutions.bitbadger.documents.scala.jvm.integration.common - -import org.junit.jupiter.api.Assertions.* -import solutions.bitbadger.documents.{Configuration, Field, Parameter, ParameterType} -import solutions.bitbadger.documents.extensions.ConnExt.* -import solutions.bitbadger.documents.jvm.Results -import solutions.bitbadger.documents.query.{CountQuery, DeleteQuery, FindQuery} -import solutions.bitbadger.documents.scala.support.JsonDocument -import solutions.bitbadger.documents.support.ThrowawayDatabase -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE - -import scala.annotation.nowarn -import scala.jdk.CollectionConverters.* - -object CustomFunctions { - - def listEmpty(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - deleteByFields(db.getConn, TEST_TABLE, (Field.exists(Configuration.idField) :: Nil).asJava) - @nowarn - val result = customList(db.getConn, FindQuery.all(TEST_TABLE), List().asJava, classOf[JsonDocument], - Results.fromData) - assertEquals(0, result.size, "There should have been no results") - - def listAll(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - @nowarn - val result = customList(db.getConn, FindQuery.all(TEST_TABLE), List().asJava, classOf[JsonDocument], - Results.fromData) - assertEquals(5, result.size, "There should have been 5 results") - - @nowarn - def singleNone(db: ThrowawayDatabase): Unit = - assertNull( - customSingle(db.getConn, FindQuery.all(TEST_TABLE), List().asJava, classOf[JsonDocument], Results.fromData), - "There should not have been a document returned") - - @nowarn - def singleOne(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertNotNull( - customSingle(db.getConn, FindQuery.all(TEST_TABLE), List().asJava, classOf[JsonDocument], Results.fromData), - "There should not have been a document returned") - - @nowarn - def nonQueryChanges(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(5L, - customScalar(db.getConn, CountQuery.all(TEST_TABLE), List().asJava, classOf[Long], Results.toCount), - "There should have been 5 documents in the table") - customNonQuery(db.getConn, s"DELETE FROM $TEST_TABLE") - assertEquals(0L, - customScalar(db.getConn, CountQuery.all(TEST_TABLE), List().asJava, classOf[Long], Results.toCount), - "There should have been no documents in the table") - - @nowarn - def nonQueryNoChanges(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(5L, - customScalar(db.getConn, CountQuery.all(TEST_TABLE), List().asJava, classOf[Long], Results.toCount), - "There should have been 5 documents in the table") - customNonQuery(db.getConn, DeleteQuery.byId(TEST_TABLE, "eighty-two"), - (Parameter(":id", ParameterType.STRING, "eighty-two") :: Nil).asJava) - assertEquals(5L, - customScalar(db.getConn, CountQuery.all(TEST_TABLE), List().asJava, classOf[Long], Results.toCount), - "There should still have been 5 documents in the table") - - @nowarn - def scalar(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals(3L, - customScalar(db.getConn, s"SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", List().asJava, classOf[Long], - Results.toCount), - "The number 3 should have been returned") -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ByteIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ByteIdClass.scala deleted file mode 100644 index 611fd75..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ByteIdClass.scala +++ /dev/null @@ -1,3 +0,0 @@ -package solutions.bitbadger.documents.scala.support - -class ByteIdClass(var id: Byte) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/IntIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/IntIdClass.scala deleted file mode 100644 index 2c4e6f1..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/IntIdClass.scala +++ /dev/null @@ -1,3 +0,0 @@ -package solutions.bitbadger.documents.scala.support - -class IntIdClass(var id: Int) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/LongIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/LongIdClass.scala deleted file mode 100644 index a66d738..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/LongIdClass.scala +++ /dev/null @@ -1,3 +0,0 @@ -package solutions.bitbadger.documents.scala.support - -class LongIdClass(var id: Long) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ShortIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ShortIdClass.scala deleted file mode 100644 index a81dfc4..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/ShortIdClass.scala +++ /dev/null @@ -1,3 +0,0 @@ -package solutions.bitbadger.documents.scala.support - -class ShortIdClass(var id: Short) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/StringIdClass.scala b/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/StringIdClass.scala deleted file mode 100644 index 11976a4..0000000 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/StringIdClass.scala +++ /dev/null @@ -1,3 +0,0 @@ -package solutions.bitbadger.documents.scala.support - -class StringIdClass(var id: String) diff --git a/src/kotlin/pom.xml b/src/kotlinx/pom.xml similarity index 73% rename from src/kotlin/pom.xml rename to src/kotlinx/pom.xml index 5549ee3..c0d6f73 100644 --- a/src/kotlin/pom.xml +++ b/src/kotlinx/pom.xml @@ -3,20 +3,18 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - solutions.bitbadger.documents - kotlin - 4.0.0-alpha1-SNAPSHOT - jar - solutions.bitbadger documents 4.0.0-alpha1-SNAPSHOT + ../../pom.xml + solutions.bitbadger.documents + kotlinx + ${project.groupId}:${project.artifactId} - Expose a document store interface for PostgreSQL and SQLite (Kotlin Library) + Expose a document store interface for PostgreSQL and SQLite (KotlinX Serialization Library) https://bitbadger.solutions/open-source/relational-documents/jvm/ @@ -28,15 +26,17 @@ solutions.bitbadger.documents - jvm - 4.0.0-alpha1-SNAPSHOT - jar + core + ${project.version} + + + org.jetbrains.kotlinx + kotlinx-serialization-json-jvm + ${serialization.version} - src/main/kotlin - src/test/kotlin org.jetbrains.kotlin @@ -49,6 +49,12 @@ compile + + + ${project.basedir}/src/main/java + ${project.basedir}/src/main/kotlin + + test-compile @@ -56,6 +62,12 @@ test-compile + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + @@ -73,11 +85,11 @@ maven-surefire-plugin - 2.22.2 + ${surefire.version} maven-failsafe-plugin - 2.22.2 + ${failsafe.version} @@ -90,6 +102,7 @@ org.apache.maven.plugins maven-compiler-plugin + 3.13.0 ${java.version} ${java.version} 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/kotlin/src/main/kotlin/Count.kt b/src/kotlinx/src/main/kotlin/Count.kt similarity index 93% rename from src/kotlin/src/main/kotlin/Count.kt rename to src/kotlinx/src/main/kotlin/Count.kt index c3b3ef5..0ebcfca 100644 --- a/src/kotlin/src/main/kotlin/Count.kt +++ b/src/kotlinx/src/main/kotlin/Count.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.kotlinx.extensions.* import solutions.bitbadger.documents.query.CountQuery import java.sql.Connection @@ -74,7 +74,11 @@ object Count { * @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) + conn.customScalar( + CountQuery.byContains(tableName), + listOf(Parameters.json(":criteria", criteria)), + Results::toCount + ) /** * Count documents using a JSON containment query (PostgreSQL only) diff --git a/src/kotlin/src/main/kotlin/Custom.kt b/src/kotlinx/src/main/kotlin/Custom.kt similarity index 97% rename from src/kotlin/src/main/kotlin/Custom.kt rename to src/kotlinx/src/main/kotlin/Custom.kt index 4d77ee5..c5d5e7e 100644 --- a/src/kotlin/src/main/kotlin/Custom.kt +++ b/src/kotlinx/src/main/kotlin/Custom.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.* import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.jvm.Custom as JvmCustom +import solutions.bitbadger.documents.java.Custom as JvmCustom import java.sql.Connection import java.sql.ResultSet diff --git a/src/kotlin/src/main/kotlin/Definition.kt b/src/kotlinx/src/main/kotlin/Definition.kt similarity index 95% rename from src/kotlin/src/main/kotlin/Definition.kt rename to src/kotlinx/src/main/kotlin/Definition.kt index 23fa87e..508b011 100644 --- a/src/kotlin/src/main/kotlin/Definition.kt +++ b/src/kotlinx/src/main/kotlin/Definition.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.DocumentException import solutions.bitbadger.documents.DocumentIndex -import solutions.bitbadger.documents.jvm.Definition as JvmDefinition +import solutions.bitbadger.documents.java.Definition as JvmDefinition import java.sql.Connection /** diff --git a/src/kotlin/src/main/kotlin/Delete.kt b/src/kotlinx/src/main/kotlin/Delete.kt similarity index 94% rename from src/kotlin/src/main/kotlin/Delete.kt rename to src/kotlinx/src/main/kotlin/Delete.kt index 9cdaa42..159a1a7 100644 --- a/src/kotlin/src/main/kotlin/Delete.kt +++ b/src/kotlinx/src/main/kotlin/Delete.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.jvm.Delete as JvmDelete -import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.java.Delete as JvmDelete +import solutions.bitbadger.documents.kotlinx.extensions.* import solutions.bitbadger.documents.query.DeleteQuery import java.sql.Connection @@ -60,7 +60,7 @@ object Delete { * @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))) + conn.customNonQuery(DeleteQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria))) /** * Delete documents using a JSON containment query (PostgreSQL only) diff --git a/src/kotlin/src/main/kotlin/Document.kt b/src/kotlinx/src/main/kotlin/Document.kt similarity index 97% rename from src/kotlin/src/main/kotlin/Document.kt rename to src/kotlinx/src/main/kotlin/Document.kt index 4b391b3..1667a49 100644 --- a/src/kotlin/src/main/kotlin/Document.kt +++ b/src/kotlinx/src/main/kotlin/Document.kt @@ -1,10 +1,10 @@ -package solutions.bitbadger.documents.kotlin +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.extensions.customNonQuery +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 diff --git a/src/kotlin/src/main/kotlin/DocumentConfig.kt b/src/kotlinx/src/main/kotlin/DocumentConfig.kt similarity index 94% rename from src/kotlin/src/main/kotlin/DocumentConfig.kt rename to src/kotlinx/src/main/kotlin/DocumentConfig.kt index cb42341..f1b4de9 100644 --- a/src/kotlin/src/main/kotlin/DocumentConfig.kt +++ b/src/kotlinx/src/main/kotlin/DocumentConfig.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import kotlinx.serialization.json.Json diff --git a/src/kotlin/src/main/kotlin/Exists.kt b/src/kotlinx/src/main/kotlin/Exists.kt similarity index 96% rename from src/kotlin/src/main/kotlin/Exists.kt rename to src/kotlinx/src/main/kotlin/Exists.kt index 4cbc05b..2bb12dc 100644 --- a/src/kotlin/src/main/kotlin/Exists.kt +++ b/src/kotlinx/src/main/kotlin/Exists.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.jvm.Exists as JvmExists -import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.java.Exists as JvmExists +import solutions.bitbadger.documents.kotlinx.extensions.* import solutions.bitbadger.documents.query.ExistsQuery import java.sql.Connection diff --git a/src/kotlin/src/main/kotlin/Find.kt b/src/kotlinx/src/main/kotlin/Find.kt similarity index 99% rename from src/kotlin/src/main/kotlin/Find.kt rename to src/kotlinx/src/main/kotlin/Find.kt index 37af42a..3e90428 100644 --- a/src/kotlin/src/main/kotlin/Find.kt +++ b/src/kotlinx/src/main/kotlin/Find.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.kotlinx.extensions.* import solutions.bitbadger.documents.query.FindQuery import solutions.bitbadger.documents.query.orderBy import java.sql.Connection diff --git a/src/kotlin/src/main/kotlin/Parameters.kt b/src/kotlinx/src/main/kotlin/Parameters.kt similarity index 91% rename from src/kotlin/src/main/kotlin/Parameters.kt rename to src/kotlinx/src/main/kotlin/Parameters.kt index 74bf62c..4e71c9d 100644 --- a/src/kotlin/src/main/kotlin/Parameters.kt +++ b/src/kotlinx/src/main/kotlin/Parameters.kt @@ -1,7 +1,9 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx -import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.jvm.Parameters as JvmParameters +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 /** @@ -28,7 +30,7 @@ object Parameters { * @return A parameter with the value encoded */ inline fun json(name: String, value: T) = - Parameter(name, ParameterType.JSON, DocumentConfig.serialize(value)) + Parameter(name, ParameterType.JSON, DocumentConfig.serialize(value)) /** * Add field parameters to the given set of parameters diff --git a/src/kotlin/src/main/kotlin/Patch.kt b/src/kotlinx/src/main/kotlin/Patch.kt similarity index 98% rename from src/kotlin/src/main/kotlin/Patch.kt rename to src/kotlinx/src/main/kotlin/Patch.kt index e4e4041..730d069 100644 --- a/src/kotlin/src/main/kotlin/Patch.kt +++ b/src/kotlinx/src/main/kotlin/Patch.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.kotlinx.extensions.* import solutions.bitbadger.documents.query.PatchQuery import java.sql.Connection diff --git a/src/kotlin/src/main/kotlin/RemoveFields.kt b/src/kotlinx/src/main/kotlin/RemoveFields.kt similarity index 96% rename from src/kotlin/src/main/kotlin/RemoveFields.kt rename to src/kotlinx/src/main/kotlin/RemoveFields.kt index f24d855..b5f8b0f 100644 --- a/src/kotlin/src/main/kotlin/RemoveFields.kt +++ b/src/kotlinx/src/main/kotlin/RemoveFields.kt @@ -1,8 +1,8 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.jvm.RemoveFields as JvmRemoveFields -import solutions.bitbadger.documents.kotlin.extensions.* +import solutions.bitbadger.documents.java.RemoveFields as JvmRemoveFields +import solutions.bitbadger.documents.kotlinx.extensions.* import solutions.bitbadger.documents.query.RemoveFieldsQuery import java.sql.Connection diff --git a/src/kotlin/src/main/kotlin/Results.kt b/src/kotlinx/src/main/kotlin/Results.kt similarity index 95% rename from src/kotlin/src/main/kotlin/Results.kt rename to src/kotlinx/src/main/kotlin/Results.kt index 791fb60..d63a363 100644 --- a/src/kotlin/src/main/kotlin/Results.kt +++ b/src/kotlinx/src/main/kotlin/Results.kt @@ -1,4 +1,4 @@ -package solutions.bitbadger.documents.kotlin +package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect @@ -19,7 +19,7 @@ object Results { * @return A function to create the constructed domain item */ inline fun fromDocument(field: String): (ResultSet) -> TDoc = - { rs -> DocumentConfig.deserialize(rs.getString(field)) } + { rs -> DocumentConfig.deserialize(rs.getString(field)) } /** * Create a domain item from a document diff --git a/src/kotlin/src/main/kotlin/extensions/Connection.kt b/src/kotlinx/src/main/kotlin/extensions/Connection.kt similarity index 99% rename from src/kotlin/src/main/kotlin/extensions/Connection.kt rename to src/kotlinx/src/main/kotlin/extensions/Connection.kt index d02e85b..784bb95 100644 --- a/src/kotlin/src/main/kotlin/extensions/Connection.kt +++ b/src/kotlinx/src/main/kotlin/extensions/Connection.kt @@ -1,7 +1,7 @@ -package solutions.bitbadger.documents.kotlin.extensions +package solutions.bitbadger.documents.kotlinx.extensions import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.kotlin.* +import solutions.bitbadger.documents.kotlinx.* import java.sql.Connection import java.sql.ResultSet 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/kotlin/src/test/kotlin/DocumentConfigTest.kt b/src/kotlinx/src/test/kotlin/DocumentConfigTest.kt similarity index 81% rename from src/kotlin/src/test/kotlin/DocumentConfigTest.kt rename to src/kotlinx/src/test/kotlin/DocumentConfigTest.kt index 9b90f19..882ed3b 100644 --- a/src/kotlin/src/test/kotlin/DocumentConfigTest.kt +++ b/src/kotlinx/src/test/kotlin/DocumentConfigTest.kt @@ -1,14 +1,15 @@ -package solutions.bitbadger.documents.kotlin +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("Kotlin | DocumentConfig") +@DisplayName("KotlinX | DocumentConfig") class DocumentConfigTest { @Test diff --git a/src/jvm/src/test/kotlin/support/Types.kt b/src/kotlinx/src/test/kotlin/Types.kt similarity index 81% rename from src/jvm/src/test/kotlin/support/Types.kt rename to src/kotlinx/src/test/kotlin/Types.kt index d56f23b..ab3754f 100644 --- a/src/jvm/src/test/kotlin/support/Types.kt +++ b/src/kotlinx/src/test/kotlin/Types.kt @@ -1,18 +1,23 @@ -package solutions.bitbadger.documents.support +package solutions.bitbadger.documents.kotlinx.tests -import solutions.bitbadger.documents.extensions.insert +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()) @@ -27,6 +32,7 @@ data class ArrayDocument(val id: String, val values: List) { } } +@Serializable data class JsonDocument(val id: String, val value: String = "", val numValue: Int = 0, val sub: SubDocument? = null) { constructor() : this("") @@ -45,11 +51,3 @@ data class JsonDocument(val id: String, val value: String = "", val numValue: In testDocuments.forEach { db.conn.insert(tableName, it) } } } - -// Test classes for AutoId generation - -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/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..d7ababc --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt @@ -0,0 +1,83 @@ +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.JsonDocument +import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE +import solutions.bitbadger.documents.query.* +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 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 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/jvm/src/test/kotlin/jvm/integration/common/Patch.kt b/src/kotlinx/src/test/kotlin/integration/PatchFunctions.kt similarity index 85% rename from src/jvm/src/test/kotlin/jvm/integration/common/Patch.kt rename to src/kotlinx/src/test/kotlin/integration/PatchFunctions.kt index 30c0731..d8e81ab 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/Patch.kt +++ b/src/kotlinx/src/test/kotlin/integration/PatchFunctions.kt @@ -1,8 +1,9 @@ -package solutions.bitbadger.documents.jvm.integration.common +package solutions.bitbadger.documents.kotlinx.tests.integration -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.extensions.* -import solutions.bitbadger.documents.support.* +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 @@ -11,12 +12,12 @@ import kotlin.test.assertTrue /** * Integration tests for the `Patch` object */ -object Patch { +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) + 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") @@ -53,7 +54,7 @@ object Patch { 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) + 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") @@ -70,7 +71,7 @@ object Patch { 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) + 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})") 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..1256d1e --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PgDB.kt @@ -0,0 +1,54 @@ +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 { + + private var dbName = "" + + init { + dbName = "throwaway_${AutoId.generateRandomString(8)}" + 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/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCountIT.kt similarity index 65% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt rename to src/kotlinx/src/test/kotlin/integration/PostgreSQLCountIT.kt index a80c2a7..93f56d7 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/CountIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCountIT.kt @@ -1,47 +1,46 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Count import kotlin.test.Test /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: Count") -class CountIT { +@DisplayName("KotlinX | PostgreSQL: Count") +class PostgreSQLCountIT { @Test @DisplayName("all counts all documents") fun all() = - PgDB().use(Count::all) + PgDB().use(CountFunctions::all) @Test @DisplayName("byFields counts documents by a numeric value") fun byFieldsNumeric() = - PgDB().use(Count::byFieldsNumeric) + PgDB().use(CountFunctions::byFieldsNumeric) @Test @DisplayName("byFields counts documents by a alphanumeric value") fun byFieldsAlpha() = - PgDB().use(Count::byFieldsAlpha) + PgDB().use(CountFunctions::byFieldsAlpha) @Test @DisplayName("byContains counts documents when matches are found") fun byContainsMatch() = - PgDB().use(Count::byContainsMatch) + PgDB().use(CountFunctions::byContainsMatch) @Test @DisplayName("byContains counts documents when no matches are found") fun byContainsNoMatch() = - PgDB().use(Count::byContainsNoMatch) + PgDB().use(CountFunctions::byContainsNoMatch) @Test @DisplayName("byJsonPath counts documents when matches are found") fun byJsonPathMatch() = - PgDB().use(Count::byJsonPathMatch) + PgDB().use(CountFunctions::byJsonPathMatch) @Test @DisplayName("byJsonPath counts documents when no matches are found") fun byJsonPathNoMatch() = - PgDB().use(Count::byJsonPathNoMatch) + PgDB().use(CountFunctions::byJsonPathNoMatch) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt similarity index 62% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt rename to src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt index 25272f1..e1ed60c 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/CustomIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -1,48 +1,47 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Custom import kotlin.test.Test /** * PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: Custom") -class CustomIT { +@DisplayName("KotlinX | PostgreSQL: Custom") +class PostgreSQLCustomIT { @Test @DisplayName("list succeeds with empty list") fun listEmpty() = - PgDB().use(Custom::listEmpty) + PgDB().use(CustomFunctions::listEmpty) @Test @DisplayName("list succeeds with a non-empty list") fun listAll() = - PgDB().use(Custom::listAll) + PgDB().use(CustomFunctions::listAll) @Test @DisplayName("single succeeds when document not found") fun singleNone() = - PgDB().use(Custom::singleNone) + PgDB().use(CustomFunctions::singleNone) @Test @DisplayName("single succeeds when a document is found") fun singleOne() = - PgDB().use(Custom::singleOne) + PgDB().use(CustomFunctions::singleOne) @Test @DisplayName("nonQuery makes changes") fun nonQueryChanges() = - PgDB().use(Custom::nonQueryChanges) + PgDB().use(CustomFunctions::nonQueryChanges) @Test @DisplayName("nonQuery makes no changes when where clause matches nothing") fun nonQueryNoChanges() = - PgDB().use(Custom::nonQueryNoChanges) + PgDB().use(CustomFunctions::nonQueryNoChanges) @Test @DisplayName("scalar succeeds") fun scalar() = - PgDB().use(Custom::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/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDeleteIT.kt similarity index 63% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt rename to src/kotlinx/src/test/kotlin/integration/PostgreSQLDeleteIT.kt index 8b322d0..280acbf 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DeleteIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDeleteIT.kt @@ -1,52 +1,51 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Delete import kotlin.test.Test /** * PostgreSQL integration tests for the `Delete` object / `deleteBy*` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: Delete") -class DeleteIT { +@DisplayName("KotlinX | PostgreSQL: Delete") +class PostgreSQLDeleteIT { @Test @DisplayName("byId deletes a matching ID") fun byIdMatch() = - PgDB().use(Delete::byIdMatch) + PgDB().use(DeleteFunctions::byIdMatch) @Test @DisplayName("byId succeeds when no ID matches") fun byIdNoMatch() = - PgDB().use(Delete::byIdNoMatch) + PgDB().use(DeleteFunctions::byIdNoMatch) @Test @DisplayName("byFields deletes matching documents") fun byFieldsMatch() = - PgDB().use(Delete::byFieldsMatch) + PgDB().use(DeleteFunctions::byFieldsMatch) @Test @DisplayName("byFields succeeds when no documents match") fun byFieldsNoMatch() = - PgDB().use(Delete::byFieldsNoMatch) + PgDB().use(DeleteFunctions::byFieldsNoMatch) @Test @DisplayName("byContains deletes matching documents") fun byContainsMatch() = - PgDB().use(Delete::byContainsMatch) + PgDB().use(DeleteFunctions::byContainsMatch) @Test @DisplayName("byContains succeeds when no documents match") fun byContainsNoMatch() = - PgDB().use(Delete::byContainsNoMatch) + PgDB().use(DeleteFunctions::byContainsNoMatch) @Test @DisplayName("byJsonPath deletes matching documents") fun byJsonPathMatch() = - PgDB().use(Delete::byJsonPathMatch) + PgDB().use(DeleteFunctions::byJsonPathMatch) @Test @DisplayName("byJsonPath succeeds when no documents match") fun byJsonPathNoMatch() = - PgDB().use(Delete::byJsonPathNoMatch) + PgDB().use(DeleteFunctions::byJsonPathNoMatch) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDocumentIT.kt similarity index 62% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt rename to src/kotlinx/src/test/kotlin/integration/PostgreSQLDocumentIT.kt index ab3955c..6292f7f 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/DocumentIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLDocumentIT.kt @@ -1,57 +1,56 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Document import kotlin.test.Test /** * PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: Document") -class DocumentIT { +@DisplayName("KotlinX | PostgreSQL: Document") +class PostgreSQLDocumentIT { @Test @DisplayName("insert works with default values") fun insertDefault() = - PgDB().use(Document::insertDefault) + PgDB().use(DocumentFunctions::insertDefault) @Test @DisplayName("insert fails with duplicate key") fun insertDupe() = - PgDB().use(Document::insertDupe) + PgDB().use(DocumentFunctions::insertDupe) @Test @DisplayName("insert succeeds with numeric auto IDs") fun insertNumAutoId() = - PgDB().use(Document::insertNumAutoId) + PgDB().use(DocumentFunctions::insertNumAutoId) @Test @DisplayName("insert succeeds with UUID auto ID") fun insertUUIDAutoId() = - PgDB().use(Document::insertUUIDAutoId) + PgDB().use(DocumentFunctions::insertUUIDAutoId) @Test @DisplayName("insert succeeds with random string auto ID") fun insertStringAutoId() = - PgDB().use(Document::insertStringAutoId) + PgDB().use(DocumentFunctions::insertStringAutoId) @Test @DisplayName("save updates an existing document") fun saveMatch() = - PgDB().use(Document::saveMatch) + PgDB().use(DocumentFunctions::saveMatch) @Test @DisplayName("save inserts a new document") fun saveNoMatch() = - PgDB().use(Document::saveNoMatch) + PgDB().use(DocumentFunctions::saveNoMatch) @Test @DisplayName("update replaces an existing document") fun updateMatch() = - PgDB().use(Document::updateMatch) + PgDB().use(DocumentFunctions::updateMatch) @Test @DisplayName("update succeeds when no document exists") fun updateNoMatch() = - PgDB().use(Document::updateNoMatch) + PgDB().use(DocumentFunctions::updateNoMatch) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLExistsIT.kt similarity index 64% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt rename to src/kotlinx/src/test/kotlin/integration/PostgreSQLExistsIT.kt index c41b14e..db868e0 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/ExistsIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLExistsIT.kt @@ -1,52 +1,51 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Exists import kotlin.test.Test /** * PostgreSQL integration tests for the `Exists` object / `existsBy*` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: Exists") -class ExistsIT { +@DisplayName("KotlinX | PostgreSQL: Exists") +class PostgreSQLExistsIT { @Test @DisplayName("byId returns true when a document matches the ID") fun byIdMatch() = - PgDB().use(Exists::byIdMatch) + PgDB().use(ExistsFunctions::byIdMatch) @Test @DisplayName("byId returns false when no document matches the ID") fun byIdNoMatch() = - PgDB().use(Exists::byIdNoMatch) + PgDB().use(ExistsFunctions::byIdNoMatch) @Test @DisplayName("byFields returns true when documents match") fun byFieldsMatch() = - PgDB().use(Exists::byFieldsMatch) + PgDB().use(ExistsFunctions::byFieldsMatch) @Test @DisplayName("byFields returns false when no documents match") fun byFieldsNoMatch() = - PgDB().use(Exists::byFieldsNoMatch) + PgDB().use(ExistsFunctions::byFieldsNoMatch) @Test @DisplayName("byContains returns true when documents match") fun byContainsMatch() = - PgDB().use(Exists::byContainsMatch) + PgDB().use(ExistsFunctions::byContainsMatch) @Test @DisplayName("byContains returns false when no documents match") fun byContainsNoMatch() = - PgDB().use(Exists::byContainsNoMatch) + PgDB().use(ExistsFunctions::byContainsNoMatch) @Test @DisplayName("byJsonPath returns true when documents match") fun byJsonPathMatch() = - PgDB().use(Exists::byJsonPathMatch) + PgDB().use(ExistsFunctions::byJsonPathMatch) @Test @DisplayName("byJsonPath returns false when no documents match") fun byJsonPathNoMatch() = - PgDB().use(Exists::byJsonPathNoMatch) + PgDB().use(ExistsFunctions::byJsonPathNoMatch) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLFindIT.kt similarity index 66% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt rename to src/kotlinx/src/test/kotlin/integration/PostgreSQLFindIT.kt index 32bf564..c6eedea 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/FindIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLFindIT.kt @@ -1,172 +1,171 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Find import kotlin.test.Test /** * PostgreSQL integration tests for the `Find` object / `find*` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: Find") -class FindIT { +@DisplayName("KotlinX | PostgreSQL: Find") +class PostgreSQLFindIT { @Test @DisplayName("all retrieves all documents") fun allDefault() = - PgDB().use(Find::allDefault) + PgDB().use(FindFunctions::allDefault) @Test @DisplayName("all sorts data ascending") fun allAscending() = - PgDB().use(Find::allAscending) + PgDB().use(FindFunctions::allAscending) @Test @DisplayName("all sorts data descending") fun allDescending() = - PgDB().use(Find::allDescending) + PgDB().use(FindFunctions::allDescending) @Test @DisplayName("all sorts data numerically") fun allNumOrder() = - PgDB().use(Find::allNumOrder) + PgDB().use(FindFunctions::allNumOrder) @Test @DisplayName("all succeeds with an empty table") fun allEmpty() = - PgDB().use(Find::allEmpty) + PgDB().use(FindFunctions::allEmpty) @Test @DisplayName("byId retrieves a document via a string ID") fun byIdString() = - PgDB().use(Find::byIdString) + PgDB().use(FindFunctions::byIdString) @Test @DisplayName("byId retrieves a document via a numeric ID") fun byIdNumber() = - PgDB().use(Find::byIdNumber) + PgDB().use(FindFunctions::byIdNumber) @Test @DisplayName("byId returns null when a matching ID is not found") fun byIdNotFound() = - PgDB().use(Find::byIdNotFound) + PgDB().use(FindFunctions::byIdNotFound) @Test @DisplayName("byFields retrieves matching documents") fun byFieldsMatch() = - PgDB().use(Find::byFieldsMatch) + PgDB().use(FindFunctions::byFieldsMatch) @Test @DisplayName("byFields retrieves ordered matching documents") fun byFieldsMatchOrdered() = - PgDB().use(Find::byFieldsMatchOrdered) + PgDB().use(FindFunctions::byFieldsMatchOrdered) @Test @DisplayName("byFields retrieves matching documents with a numeric IN clause") fun byFieldsMatchNumIn() = - PgDB().use(Find::byFieldsMatchNumIn) + PgDB().use(FindFunctions::byFieldsMatchNumIn) @Test @DisplayName("byFields succeeds when no documents match") fun byFieldsNoMatch() = - PgDB().use(Find::byFieldsNoMatch) + PgDB().use(FindFunctions::byFieldsNoMatch) @Test @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") fun byFieldsMatchInArray() = - PgDB().use(Find::byFieldsMatchInArray) + PgDB().use(FindFunctions::byFieldsMatchInArray) @Test @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") fun byFieldsNoMatchInArray() = - PgDB().use(Find::byFieldsNoMatchInArray) + PgDB().use(FindFunctions::byFieldsNoMatchInArray) @Test @DisplayName("byContains retrieves matching documents") fun byContainsMatch() = - PgDB().use(Find::byContainsMatch) + PgDB().use(FindFunctions::byContainsMatch) @Test @DisplayName("byContains retrieves ordered matching documents") fun byContainsMatchOrdered() = - PgDB().use(Find::byContainsMatchOrdered) + PgDB().use(FindFunctions::byContainsMatchOrdered) @Test @DisplayName("byContains succeeds when no documents match") fun byContainsNoMatch() = - PgDB().use(Find::byContainsNoMatch) + PgDB().use(FindFunctions::byContainsNoMatch) @Test @DisplayName("byJsonPath retrieves matching documents") fun byJsonPathMatch() = - PgDB().use(Find::byJsonPathMatch) + PgDB().use(FindFunctions::byJsonPathMatch) @Test @DisplayName("byJsonPath retrieves ordered matching documents") fun byJsonPathMatchOrdered() = - PgDB().use(Find::byJsonPathMatchOrdered) + PgDB().use(FindFunctions::byJsonPathMatchOrdered) @Test @DisplayName("byJsonPath succeeds when no documents match") fun byJsonPathNoMatch() = - PgDB().use(Find::byJsonPathNoMatch) + PgDB().use(FindFunctions::byJsonPathNoMatch) @Test @DisplayName("firstByFields retrieves a matching document") fun firstByFieldsMatchOne() = - PgDB().use(Find::firstByFieldsMatchOne) + PgDB().use(FindFunctions::firstByFieldsMatchOne) @Test @DisplayName("firstByFields retrieves a matching document among many") fun firstByFieldsMatchMany() = - PgDB().use(Find::firstByFieldsMatchMany) + PgDB().use(FindFunctions::firstByFieldsMatchMany) @Test @DisplayName("firstByFields retrieves a matching document among many (ordered)") fun firstByFieldsMatchOrdered() = - PgDB().use(Find::firstByFieldsMatchOrdered) + PgDB().use(FindFunctions::firstByFieldsMatchOrdered) @Test @DisplayName("firstByFields returns null when no document matches") fun firstByFieldsNoMatch() = - PgDB().use(Find::firstByFieldsNoMatch) + PgDB().use(FindFunctions::firstByFieldsNoMatch) @Test @DisplayName("firstByContains retrieves a matching document") fun firstByContainsMatchOne() = - PgDB().use(Find::firstByContainsMatchOne) + PgDB().use(FindFunctions::firstByContainsMatchOne) @Test @DisplayName("firstByContains retrieves a matching document among many") fun firstByContainsMatchMany() = - PgDB().use(Find::firstByContainsMatchMany) + PgDB().use(FindFunctions::firstByContainsMatchMany) @Test @DisplayName("firstByContains retrieves a matching document among many (ordered)") fun firstByContainsMatchOrdered() = - PgDB().use(Find::firstByContainsMatchOrdered) + PgDB().use(FindFunctions::firstByContainsMatchOrdered) @Test @DisplayName("firstByContains returns null when no document matches") fun firstByContainsNoMatch() = - PgDB().use(Find::firstByContainsNoMatch) + PgDB().use(FindFunctions::firstByContainsNoMatch) @Test @DisplayName("firstByJsonPath retrieves a matching document") fun firstByJsonPathMatchOne() = - PgDB().use(Find::firstByJsonPathMatchOne) + PgDB().use(FindFunctions::firstByJsonPathMatchOne) @Test @DisplayName("firstByJsonPath retrieves a matching document among many") fun firstByJsonPathMatchMany() = - PgDB().use(Find::firstByJsonPathMatchMany) + PgDB().use(FindFunctions::firstByJsonPathMatchMany) @Test @DisplayName("firstByJsonPath retrieves a matching document among many (ordered)") fun firstByJsonPathMatchOrdered() = - PgDB().use(Find::firstByJsonPathMatchOrdered) + PgDB().use(FindFunctions::firstByJsonPathMatchOrdered) @Test @DisplayName("firstByJsonPath returns null when no document matches") fun firstByJsonPathNoMatch() = - PgDB().use(Find::firstByJsonPathNoMatch) + PgDB().use(FindFunctions::firstByJsonPathNoMatch) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLPatchIT.kt similarity index 63% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt rename to src/kotlinx/src/test/kotlin/integration/PostgreSQLPatchIT.kt index 4a9c2ee..afb7066 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/PatchIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLPatchIT.kt @@ -1,52 +1,51 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Patch import kotlin.test.Test /** * PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: Patch") -class PatchIT { +@DisplayName("KotlinX | PostgreSQL: Patch") +class PostgreSQLPatchIT { @Test @DisplayName("byId patches an existing document") fun byIdMatch() = - PgDB().use(Patch::byIdMatch) + PgDB().use(PatchFunctions::byIdMatch) @Test @DisplayName("byId succeeds for a non-existent document") fun byIdNoMatch() = - PgDB().use(Patch::byIdNoMatch) + PgDB().use(PatchFunctions::byIdNoMatch) @Test @DisplayName("byFields patches matching document") fun byFieldsMatch() = - PgDB().use(Patch::byFieldsMatch) + PgDB().use(PatchFunctions::byFieldsMatch) @Test @DisplayName("byFields succeeds when no documents match") fun byFieldsNoMatch() = - PgDB().use(Patch::byFieldsNoMatch) + PgDB().use(PatchFunctions::byFieldsNoMatch) @Test @DisplayName("byContains patches matching document") fun byContainsMatch() = - PgDB().use(Patch::byContainsMatch) + PgDB().use(PatchFunctions::byContainsMatch) @Test @DisplayName("byContains succeeds when no documents match") fun byContainsNoMatch() = - PgDB().use(Patch::byContainsNoMatch) + PgDB().use(PatchFunctions::byContainsNoMatch) @Test @DisplayName("byJsonPath patches matching document") fun byJsonPathMatch() = - PgDB().use(Patch::byJsonPathMatch) + PgDB().use(PatchFunctions::byJsonPathMatch) @Test @DisplayName("byJsonPath succeeds when no documents match") fun byJsonPathNoMatch() = - PgDB().use(Patch::byJsonPathNoMatch) + PgDB().use(PatchFunctions::byJsonPathNoMatch) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt similarity index 63% rename from src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt rename to src/kotlinx/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt index 968106f..13f07ec 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/postgresql/RemoveFieldsIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLRemoveFieldsIT.kt @@ -1,72 +1,71 @@ -package solutions.bitbadger.documents.jvm.integration.postgresql +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.RemoveFields import kotlin.test.Test /** * PostgreSQL integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions */ -@DisplayName("JVM | Kotlin | PostgreSQL: RemoveFields") -class RemoveFieldsIT { +@DisplayName("KotlinX | PostgreSQL: RemoveFields") +class PostgreSQLRemoveFieldsIT { @Test @DisplayName("byId removes fields from an existing document") fun byIdMatchFields() = - PgDB().use(RemoveFields::byIdMatchFields) + PgDB().use(RemoveFieldsFunctions::byIdMatchFields) @Test @DisplayName("byId succeeds when fields do not exist on an existing document") fun byIdMatchNoFields() = - PgDB().use(RemoveFields::byIdMatchNoFields) + PgDB().use(RemoveFieldsFunctions::byIdMatchNoFields) @Test @DisplayName("byId succeeds when no document exists") fun byIdNoMatch() = - PgDB().use(RemoveFields::byIdNoMatch) + PgDB().use(RemoveFieldsFunctions::byIdNoMatch) @Test @DisplayName("byFields removes fields from matching documents") fun byFieldsMatchFields() = - PgDB().use(RemoveFields::byFieldsMatchFields) + PgDB().use(RemoveFieldsFunctions::byFieldsMatchFields) @Test @DisplayName("byFields succeeds when fields do not exist on matching documents") fun byFieldsMatchNoFields() = - PgDB().use(RemoveFields::byFieldsMatchNoFields) + PgDB().use(RemoveFieldsFunctions::byFieldsMatchNoFields) @Test @DisplayName("byFields succeeds when no matching documents exist") fun byFieldsNoMatch() = - PgDB().use(RemoveFields::byFieldsNoMatch) + PgDB().use(RemoveFieldsFunctions::byFieldsNoMatch) @Test @DisplayName("byContains removes fields from matching documents") fun byContainsMatchFields() = - PgDB().use(RemoveFields::byContainsMatchFields) + PgDB().use(RemoveFieldsFunctions::byContainsMatchFields) @Test @DisplayName("byContains succeeds when fields do not exist on matching documents") fun byContainsMatchNoFields() = - PgDB().use(RemoveFields::byContainsMatchNoFields) + PgDB().use(RemoveFieldsFunctions::byContainsMatchNoFields) @Test @DisplayName("byContains succeeds when no matching documents exist") fun byContainsNoMatch() = - PgDB().use(RemoveFields::byContainsNoMatch) + PgDB().use(RemoveFieldsFunctions::byContainsNoMatch) @Test @DisplayName("byJsonPath removes fields from matching documents") fun byJsonPathMatchFields() = - PgDB().use(RemoveFields::byJsonPathMatchFields) + PgDB().use(RemoveFieldsFunctions::byJsonPathMatchFields) @Test @DisplayName("byJsonPath succeeds when fields do not exist on matching documents") fun byJsonPathMatchNoFields() = - PgDB().use(RemoveFields::byJsonPathMatchNoFields) + PgDB().use(RemoveFieldsFunctions::byJsonPathMatchNoFields) @Test @DisplayName("byJsonPath succeeds when no matching documents exist") fun byJsonPathNoMatch() = - PgDB().use(RemoveFields::byJsonPathNoMatch) + PgDB().use(RemoveFieldsFunctions::byJsonPathNoMatch) } diff --git a/src/jvm/src/test/kotlin/jvm/integration/common/RemoveFields.kt b/src/kotlinx/src/test/kotlin/integration/RemoveFieldsFunctions.kt similarity index 86% rename from src/jvm/src/test/kotlin/jvm/integration/common/RemoveFields.kt rename to src/kotlinx/src/test/kotlin/integration/RemoveFieldsFunctions.kt index 2c90f1b..068ed52 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/common/RemoveFields.kt +++ b/src/kotlinx/src/test/kotlin/integration/RemoveFieldsFunctions.kt @@ -1,20 +1,20 @@ -package solutions.bitbadger.documents.jvm.integration.common +package solutions.bitbadger.documents.kotlinx.tests.integration -import solutions.bitbadger.documents.Field -import solutions.bitbadger.documents.extensions.* -import solutions.bitbadger.documents.support.* +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 RemoveFields { +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) + 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") @@ -36,7 +36,7 @@ object RemoveFields { 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) + 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") @@ -59,7 +59,7 @@ object RemoveFields { 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) + 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})") @@ -85,7 +85,7 @@ object RemoveFields { 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) + 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})") 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/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt similarity index 61% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt rename to src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt index 03ef8ee..ce06f4c 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/CustomIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -1,47 +1,46 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Custom import kotlin.test.Test /** * SQLite integration tests for the `Custom` object / `custom*` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: Custom") -class CustomIT { +@DisplayName("KotlinX | SQLite: Custom") +class SQLiteCustomIT { @Test @DisplayName("list succeeds with empty list") fun listEmpty() = - SQLiteDB().use(Custom::listEmpty) + SQLiteDB().use(CustomFunctions::listEmpty) @Test @DisplayName("list succeeds with a non-empty list") fun listAll() = - SQLiteDB().use(Custom::listAll) + SQLiteDB().use(CustomFunctions::listAll) @Test @DisplayName("single succeeds when document not found") fun singleNone() = - SQLiteDB().use(Custom::singleNone) + SQLiteDB().use(CustomFunctions::singleNone) @Test @DisplayName("single succeeds when a document is found") fun singleOne() = - SQLiteDB().use(Custom::singleOne) + SQLiteDB().use(CustomFunctions::singleOne) @Test @DisplayName("nonQuery makes changes") fun nonQueryChanges() = - SQLiteDB().use(Custom::nonQueryChanges) + SQLiteDB().use(CustomFunctions::nonQueryChanges) @Test @DisplayName("nonQuery makes no changes when where clause matches nothing") fun nonQueryNoChanges() = - SQLiteDB().use(Custom::nonQueryNoChanges) + SQLiteDB().use(CustomFunctions::nonQueryNoChanges) @Test @DisplayName("scalar succeeds") fun scalar() = - SQLiteDB().use(Custom::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..8ea554d --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt @@ -0,0 +1,35 @@ +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 { + + private var dbName = "" + + init { + dbName = "test-db-${AutoId.generateRandomString(8)}.db" + 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/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteDocumentIT.kt similarity index 61% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt rename to src/kotlinx/src/test/kotlin/integration/SQLiteDocumentIT.kt index 1531117..c220dc1 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/DocumentIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteDocumentIT.kt @@ -1,57 +1,56 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +package solutions.bitbadger.documents.kotlinx.tests.integration import org.junit.jupiter.api.DisplayName -import solutions.bitbadger.documents.jvm.integration.common.Document import kotlin.test.Test /** * SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: Document") -class DocumentIT { +@DisplayName("KotlinX | SQLite: Document") +class SQLiteDocumentIT { @Test @DisplayName("insert works with default values") fun insertDefault() = - SQLiteDB().use(Document::insertDefault) + SQLiteDB().use(DocumentFunctions::insertDefault) @Test @DisplayName("insert fails with duplicate key") fun insertDupe() = - SQLiteDB().use(Document::insertDupe) + SQLiteDB().use(DocumentFunctions::insertDupe) @Test @DisplayName("insert succeeds with numeric auto IDs") fun insertNumAutoId() = - SQLiteDB().use(Document::insertNumAutoId) + SQLiteDB().use(DocumentFunctions::insertNumAutoId) @Test @DisplayName("insert succeeds with UUID auto ID") fun insertUUIDAutoId() = - SQLiteDB().use(Document::insertUUIDAutoId) + SQLiteDB().use(DocumentFunctions::insertUUIDAutoId) @Test @DisplayName("insert succeeds with random string auto ID") fun insertStringAutoId() = - SQLiteDB().use(Document::insertStringAutoId) + SQLiteDB().use(DocumentFunctions::insertStringAutoId) @Test @DisplayName("save updates an existing document") fun saveMatch() = - SQLiteDB().use(Document::saveMatch) + SQLiteDB().use(DocumentFunctions::saveMatch) @Test @DisplayName("save inserts a new document") fun saveNoMatch() = - SQLiteDB().use(Document::saveNoMatch) + SQLiteDB().use(DocumentFunctions::saveNoMatch) @Test @DisplayName("update replaces an existing document") fun updateMatch() = - SQLiteDB().use(Document::updateMatch) + SQLiteDB().use(DocumentFunctions::updateMatch) @Test @DisplayName("update succeeds when no document exists") fun updateNoMatch() = - SQLiteDB().use(Document::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/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteFindIT.kt similarity index 62% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt rename to src/kotlinx/src/test/kotlin/integration/SQLiteFindIT.kt index 8dab546..9350620 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/FindIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteFindIT.kt @@ -1,128 +1,127 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +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 solutions.bitbadger.documents.jvm.integration.common.Find import kotlin.test.Test /** * SQLite integration tests for the `Find` object / `find*` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: Find") -class FindIT { +@DisplayName("KotlinX | SQLite: Find") +class SQLiteFindIT { @Test @DisplayName("all retrieves all documents") fun allDefault() = - SQLiteDB().use(Find::allDefault) + SQLiteDB().use(FindFunctions::allDefault) @Test @DisplayName("all sorts data ascending") fun allAscending() = - SQLiteDB().use(Find::allAscending) + SQLiteDB().use(FindFunctions::allAscending) @Test @DisplayName("all sorts data descending") fun allDescending() = - SQLiteDB().use(Find::allDescending) + SQLiteDB().use(FindFunctions::allDescending) @Test @DisplayName("all sorts data numerically") fun allNumOrder() = - SQLiteDB().use(Find::allNumOrder) + SQLiteDB().use(FindFunctions::allNumOrder) @Test @DisplayName("all succeeds with an empty table") fun allEmpty() = - SQLiteDB().use(Find::allEmpty) + SQLiteDB().use(FindFunctions::allEmpty) @Test @DisplayName("byId retrieves a document via a string ID") fun byIdString() = - SQLiteDB().use(Find::byIdString) + SQLiteDB().use(FindFunctions::byIdString) @Test @DisplayName("byId retrieves a document via a numeric ID") fun byIdNumber() = - SQLiteDB().use(Find::byIdNumber) + SQLiteDB().use(FindFunctions::byIdNumber) @Test @DisplayName("byId returns null when a matching ID is not found") fun byIdNotFound() = - SQLiteDB().use(Find::byIdNotFound) + SQLiteDB().use(FindFunctions::byIdNotFound) @Test @DisplayName("byFields retrieves matching documents") fun byFieldsMatch() = - SQLiteDB().use(Find::byFieldsMatch) + SQLiteDB().use(FindFunctions::byFieldsMatch) @Test @DisplayName("byFields retrieves ordered matching documents") fun byFieldsMatchOrdered() = - SQLiteDB().use(Find::byFieldsMatchOrdered) + SQLiteDB().use(FindFunctions::byFieldsMatchOrdered) @Test @DisplayName("byFields retrieves matching documents with a numeric IN clause") fun byFieldsMatchNumIn() = - SQLiteDB().use(Find::byFieldsMatchNumIn) + SQLiteDB().use(FindFunctions::byFieldsMatchNumIn) @Test @DisplayName("byFields succeeds when no documents match") fun byFieldsNoMatch() = - SQLiteDB().use(Find::byFieldsNoMatch) + SQLiteDB().use(FindFunctions::byFieldsNoMatch) @Test @DisplayName("byFields retrieves matching documents with an IN_ARRAY comparison") fun byFieldsMatchInArray() = - SQLiteDB().use(Find::byFieldsMatchInArray) + SQLiteDB().use(FindFunctions::byFieldsMatchInArray) @Test @DisplayName("byFields succeeds when no documents match an IN_ARRAY comparison") fun byFieldsNoMatchInArray() = - SQLiteDB().use(Find::byFieldsNoMatchInArray) + SQLiteDB().use(FindFunctions::byFieldsNoMatchInArray) @Test @DisplayName("byContains fails") fun byContainsFails() { - assertThrows { SQLiteDB().use(Find::byContainsMatch) } + assertThrows { SQLiteDB().use(FindFunctions::byContainsMatch) } } @Test @DisplayName("byJsonPath fails") fun byJsonPathFails() { - assertThrows { SQLiteDB().use(Find::byJsonPathMatch) } + assertThrows { SQLiteDB().use(FindFunctions::byJsonPathMatch) } } @Test @DisplayName("firstByFields retrieves a matching document") fun firstByFieldsMatchOne() = - SQLiteDB().use(Find::firstByFieldsMatchOne) + SQLiteDB().use(FindFunctions::firstByFieldsMatchOne) @Test @DisplayName("firstByFields retrieves a matching document among many") fun firstByFieldsMatchMany() = - SQLiteDB().use(Find::firstByFieldsMatchMany) + SQLiteDB().use(FindFunctions::firstByFieldsMatchMany) @Test @DisplayName("firstByFields retrieves a matching document among many (ordered)") fun firstByFieldsMatchOrdered() = - SQLiteDB().use(Find::firstByFieldsMatchOrdered) + SQLiteDB().use(FindFunctions::firstByFieldsMatchOrdered) @Test @DisplayName("firstByFields returns null when no document matches") fun firstByFieldsNoMatch() = - SQLiteDB().use(Find::firstByFieldsNoMatch) + SQLiteDB().use(FindFunctions::firstByFieldsNoMatch) @Test @DisplayName("firstByContains fails") fun firstByContainsFails() { - assertThrows { SQLiteDB().use(Find::firstByContainsMatchOne) } + assertThrows { SQLiteDB().use(FindFunctions::firstByContainsMatchOne) } } @Test @DisplayName("firstByJsonPath fails") fun firstByJsonPathFails() { - assertThrows { SQLiteDB().use(Find::firstByJsonPathMatchOne) } + assertThrows { SQLiteDB().use(FindFunctions::firstByJsonPathMatchOne) } } } 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/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt similarity index 67% rename from src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt rename to src/kotlinx/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt index 26e51b6..3bea0b4 100644 --- a/src/jvm/src/test/kotlin/jvm/integration/sqlite/RemoveFieldsIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteRemoveFieldsIT.kt @@ -1,56 +1,55 @@ -package solutions.bitbadger.documents.jvm.integration.sqlite +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 solutions.bitbadger.documents.jvm.integration.common.RemoveFields import kotlin.test.Test /** * SQLite integration tests for the `RemoveFields` object / `removeFieldsBy*` connection extension functions */ -@DisplayName("JVM | Kotlin | SQLite: RemoveFields") -class RemoveFieldsIT { +@DisplayName("KotlinX | SQLite: RemoveFields") +class SQLiteRemoveFieldsIT { @Test @DisplayName("byId removes fields from an existing document") fun byIdMatchFields() = - SQLiteDB().use(RemoveFields::byIdMatchFields) + SQLiteDB().use(RemoveFieldsFunctions::byIdMatchFields) @Test @DisplayName("byId succeeds when fields do not exist on an existing document") fun byIdMatchNoFields() = - SQLiteDB().use(RemoveFields::byIdMatchNoFields) + SQLiteDB().use(RemoveFieldsFunctions::byIdMatchNoFields) @Test @DisplayName("byId succeeds when no document exists") fun byIdNoMatch() = - SQLiteDB().use(RemoveFields::byIdNoMatch) + SQLiteDB().use(RemoveFieldsFunctions::byIdNoMatch) @Test @DisplayName("byFields removes fields from matching documents") fun byFieldsMatchFields() = - SQLiteDB().use(RemoveFields::byFieldsMatchFields) + SQLiteDB().use(RemoveFieldsFunctions::byFieldsMatchFields) @Test @DisplayName("byFields succeeds when fields do not exist on matching documents") fun byFieldsMatchNoFields() = - SQLiteDB().use(RemoveFields::byFieldsMatchNoFields) + SQLiteDB().use(RemoveFieldsFunctions::byFieldsMatchNoFields) @Test @DisplayName("byFields succeeds when no matching documents exist") fun byFieldsNoMatch() = - SQLiteDB().use(RemoveFields::byFieldsNoMatch) + SQLiteDB().use(RemoveFieldsFunctions::byFieldsNoMatch) @Test @DisplayName("byContains fails") fun byContainsFails() { - assertThrows { SQLiteDB().use(RemoveFields::byContainsMatchFields) } + assertThrows { SQLiteDB().use(RemoveFieldsFunctions::byContainsMatchFields) } } @Test @DisplayName("byJsonPath fails") fun byJsonPathFails() { - assertThrows { SQLiteDB().use(RemoveFields::byJsonPathMatchFields) } + 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..858ccc5 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import java.sql.Connection + +/** + * Common interface for PostgreSQL and SQLite throwaway databases + */ +interface ThrowawayDatabase : AutoCloseable { + + /** The database connection for the throwaway database */ + 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 + */ + fun dbObjectExists(name: String): Boolean +} diff --git a/src/pom.xml b/src/pom.xml deleted file mode 100644 index 06c5830..0000000 --- a/src/pom.xml +++ /dev/null @@ -1,98 +0,0 @@ - - - 4.0.0 - - solutions.bitbadger - documents - 4.0.0-alpha1-SNAPSHOT - pom - - ${project.groupId}:${project.artifactId} - Expose a document store interface for PostgreSQL and SQLite - https://bitbadger.solutions/open-source/solutions.bitbadger.documents - - - - 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 - - - - 11 - UTF-8 - official - ${java.version} - 2.1.10 - 1.8.0 - 3.5.2 - 4.0.26 - 3.46.1.2 - 42.7.5 - - - - jvm - kotlin - - - - - org.junit.jupiter - junit-jupiter - 5.11.1 - test - - - org.jetbrains.kotlin - kotlin-test-junit5 - ${kotlin.version} - test - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - org.jetbrains.kotlin - kotlin-reflect - ${kotlin.version} - - - org.jetbrains.kotlinx - kotlinx-serialization-json - ${serialization.version} - - - org.xerial - sqlite-jdbc - ${sqlite.version} - test - - - org.postgresql - postgresql - ${postgresql.version} - test - - - - diff --git a/src/scala/pom.xml b/src/scala/pom.xml new file mode 100644 index 0000000..566eadb --- /dev/null +++ b/src/scala/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + solutions.bitbadger + documents + 4.0.0-alpha1-SNAPSHOT + ../../pom.xml + + + solutions.bitbadger.documents + scala + + ${project.groupId}:${project.artifactId} + Expose a document store interface for PostgreSQL and SQLite (Scala Library) + https://bitbadger.solutions/open-source/relational-documents/jvm/ + + + UTF-8 + official + 1.8 + + + + ${project.basedir}/src/main/scala + ${project.basedir}/src/test/scala + + + net.alchim31.maven + scala-maven-plugin + 4.9.2 + + + + compile + testCompile + + + + + ${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} + + + + + + + + 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/jvm/jvm.iml b/src/scala/scala.iml similarity index 76% rename from src/jvm/jvm.iml rename to src/scala/scala.iml index d6f3c7a..6ad0dbf 100644 --- a/src/jvm/jvm.iml +++ b/src/scala/scala.iml @@ -2,7 +2,7 @@ - + diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Count.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Count.scala new file mode 100644 index 0000000..a2a1d3c --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/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[TContains](tableName: String, criteria: TContains, 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[TContains](tableName: String, criteria: TContains): 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/solutions/bitbadger/documents/scala/Custom.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Custom.scala new file mode 100644 index 0000000..b59bee2 --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Custom.scala @@ -0,0 +1,210 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Configuration, Parameter} + +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[TDoc](query: String, parameters: Seq[Parameter[?]], conn: Connection, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + Using(Parameters.apply(conn, query, parameters)) { stmt => Results.toCustomList[TDoc](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[TDoc](query: String, conn: Connection, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + list(query, List(), 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[TDoc](query: String, parameters: Seq[Parameter[?]], + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + Using(Configuration.dbConn()) { conn => list[TDoc](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[TDoc](query: String, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + list(query, List(), 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[TDoc](query: String, parameters: Seq[Parameter[?]], conn: Connection, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + list[TDoc](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[TDoc](query: String, conn: Connection, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + list[TDoc](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[TDoc](query: String, parameters: Seq[Parameter[?]], + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Using(Configuration.dbConn()) { conn => single[TDoc](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[TDoc](query: String, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + single[TDoc](query, List(), 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[T](query: String, parameters: Seq[Parameter[?]], conn: Connection, + mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = + 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[T](query: String, conn: Connection, + mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = + scalar[T](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[T](query: String, parameters: Seq[Parameter[?]], + mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = + Using(Configuration.dbConn()) { conn => scalar[T](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[T](query: String, + mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = + scalar[T](query, List(), mapFunc) diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Definition.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Definition.scala new file mode 100644 index 0000000..4e0c2cc --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/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/solutions/bitbadger/documents/scala/Delete.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Delete.scala new file mode 100644 index 0000000..19729af --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/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[TKey](tableName: String, docId: TKey, 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[TKey](tableName: String, docId: TKey): 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[TContains](tableName: String, criteria: TContains, 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[TContains](tableName: String, criteria: TContains): 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/solutions/bitbadger/documents/scala/Document.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Document.scala new file mode 100644 index 0000000..ca23662 --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/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[TDoc](tableName: String, document: TDoc, 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[TDoc](tableName: String, document: TDoc): 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[TDoc](tableName: String, document: TDoc, 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[TDoc](tableName: String, document: TDoc): 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[TKey, TDoc](tableName: String, docId: TKey, document: TDoc, 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[TKey, TDoc](tableName: String, docId: TKey, document: TDoc): Unit = + CoreDocument.update(tableName, docId, document) diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Exists.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Exists.scala new file mode 100644 index 0000000..90883f0 --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/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[TKey](tableName: String, docId: TKey, 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[TKey](tableName: String, docId: TKey): 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[TContains](tableName: String, criteria: TContains, 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[TContains](tableName: String, criteria: TContains): 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/solutions/bitbadger/documents/scala/Find.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Find.scala new file mode 100644 index 0000000..eb6534f --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Find.scala @@ -0,0 +1,372 @@ +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[TDoc](tableName: String, orderBy: Seq[Field[?]], conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = + Custom.list[TDoc](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[TDoc](tableName: String, conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = + all[TDoc](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[TDoc](tableName: String, orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): List[TDoc] = + Using(Configuration.dbConn()) { conn => all[TDoc](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[TKey, TDoc](tableName: String, docId: TKey, conn: Connection)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Custom.single[TDoc](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[TKey, TDoc](tableName: String, docId: TKey)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Using(Configuration.dbConn()) { conn => byId[TKey, TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], orderBy: Seq[Field[?]], + conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = + val named = Parameters.nameFields(fields) + Custom.list[TDoc]( + 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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection) + (implicit tag: ClassTag[TDoc]): List[TDoc] = + byFields[TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], orderBy: Seq[Field[?]], conn: Connection) + (implicit tag: ClassTag[TDoc]): List[TDoc] = + byFields[TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): List[TDoc] = + Using(Configuration.dbConn()) { conn => byFields[TDoc](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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]], + conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = + Custom.list[TDoc](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[TDoc, TContains](tableName: String, criteria: TContains, conn: Connection) + (implicit tag: ClassTag[TDoc]): List[TDoc] = + byContains[TDoc, TContains](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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]] = List()) + (implicit tag: ClassTag[TDoc]): List[TDoc] = + Using(Configuration.dbConn()) { conn => byContains[TDoc, TContains](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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]], conn: Connection) + (implicit tag: ClassTag[TDoc]): List[TDoc] = + Custom.list[TDoc](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[TDoc](tableName: String, path: String, conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = + byJsonPath[TDoc](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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) + (implicit tag: ClassTag[TDoc]): List[TDoc] = + Using(Configuration.dbConn()) { conn => byJsonPath[TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], + orderBy: Seq[Field[?]], conn: Connection)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + val named = Parameters.nameFields(fields) + Custom.single[TDoc]( + 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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + firstByFields[TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], orderBy: Seq[Field[?]], conn: Connection) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + firstByFields[TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], conn: Connection) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + firstByFields[TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Using(Configuration.dbConn()) { conn => firstByFields[TDoc](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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]], + conn: Connection)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Custom.single[TDoc](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[TDoc, TContains](tableName: String, criteria: TContains, conn: Connection) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + firstByContains[TDoc, TContains](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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]] = List()) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + Using(Configuration.dbConn()) { conn => firstByContains[TDoc, TContains](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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]], conn: Connection) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + Custom.single[TDoc](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[TDoc](tableName: String, path: String, conn: Connection) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + firstByJsonPath[TDoc](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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + Using(Configuration.dbConn()) { conn => firstByJsonPath[TDoc](tableName, path, orderBy, conn) }.get diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Parameters.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Parameters.scala new file mode 100644 index 0000000..02b2d5d --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Parameters.scala @@ -0,0 +1,87 @@ +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)) { + 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[T](name: String, value: T): 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/solutions/bitbadger/documents/scala/Patch.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Patch.scala new file mode 100644 index 0000000..b53bb83 --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/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[TKey, TPatch](tableName: String, docId: TKey, patch: TPatch, 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[TKey, TPatch](tableName: String, docId: TKey, patch: TPatch) = + 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[TPatch](tableName: String, fields: Seq[Field[?]], patch: TPatch, 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[TPatch](tableName: String, fields: Seq[Field[?]], patch: TPatch, 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[TPatch](tableName: String, fields: Seq[Field[?]], patch: TPatch, + 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[TContains, TPatch](tableName: String, criteria: TContains, patch: TPatch, 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[TContains, TPatch](tableName: String, criteria: TContains, patch: TPatch): 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[TPatch](tableName: String, path: String, patch: TPatch, 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[TPatch](tableName: String, path: String, patch: TPatch): Unit = + CorePatch.byJsonPath(tableName, path, patch) diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/RemoveFields.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/RemoveFields.scala new file mode 100644 index 0000000..2701553 --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/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[TKey](tableName: String, docId: TKey, 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[TKey](tableName: String, docId: TKey, 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[TContains](tableName: String, criteria: TContains, 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[TContains](tableName: String, criteria: TContains, 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/solutions/bitbadger/documents/scala/Results.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Results.scala new file mode 100644 index 0000000..bab76e4 --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Results.scala @@ -0,0 +1,77 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.DocumentException +import solutions.bitbadger.documents.java.Results as CoreResults + +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[TDoc](field: String, rs: ResultSet, ignored: ClassTag[TDoc])(implicit tag: ClassTag[TDoc]): TDoc = + CoreResults.fromDocument(field, rs, tag.runtimeClass.asInstanceOf[Class[TDoc]]) + + /** + * 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[TDoc](rs: ResultSet, ignored: ClassTag[TDoc])(implicit tag: ClassTag[TDoc]): TDoc = + fromDocument[TDoc]("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[TDoc](stmt: PreparedStatement, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + try + val buffer = ListBuffer[TDoc]() + Using(stmt.executeQuery()) { rs => + while (rs.next()) { + 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) diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/extensions/package.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/extensions/package.scala new file mode 100644 index 0000000..142fd26 --- /dev/null +++ b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/extensions/package.scala @@ -0,0 +1,499 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{DocumentIndex, Field, FieldMatch, Parameter} + +import java.sql.{Connection, ResultSet} +import scala.reflect.ClassTag + +package object extensions: + + 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[TDoc](query: String, parameters: Seq[Parameter[?]], + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + Custom.list[TDoc](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[TDoc](query: String, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + Custom.list[TDoc](query, conn, 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[TDoc](query: String, parameters: Seq[Parameter[?]], + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Custom.single[TDoc](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[TDoc](query: String, + mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Custom.single[TDoc](query, conn, 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[?]] = List()): 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[T](query: String, parameters: Seq[Parameter[?]], + mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = + Custom.scalar[T](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[T](query: String, + mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = + Custom.scalar[T](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[TDoc](tableName: String, document: TDoc): 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[TDoc](tableName: String, document: TDoc): 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[TKey, TDoc](tableName: String, docId: TKey, document: TDoc): 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[TContains](tableName: String, criteria: TContains): 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[TKey](tableName: String, docId: TKey): 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[TContains](tableName: String, criteria: TContains): 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 ~~~ + + /** + * 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[TDoc](tableName: String, orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): List[TDoc] = + Find.all[TDoc](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[TKey, TDoc](tableName: String, docId: TKey)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Find.byId[TKey, TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): List[TDoc] = + Find.byFields[TDoc](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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]] = List()) + (implicit tag: ClassTag[TDoc]): List[TDoc] = + Find.byContains[TDoc, TContains](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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) + (implicit tag: ClassTag[TDoc]): List[TDoc] = + Find.byJsonPath[TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): Option[TDoc] = + Find.firstByFields[TDoc](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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]] = List()) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + Find.firstByContains[TDoc, TContains](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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) + (implicit tag: ClassTag[TDoc]): Option[TDoc] = + Find.firstByJsonPath[TDoc](tableName, 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[TKey, TPatch](tableName: String, docId: TKey, patch: TPatch): 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[TPatch](tableName: String, fields: Seq[Field[?]], patch: TPatch, + 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[TContains, TPatch](tableName: String, criteria: TContains, patch: TPatch): 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[TPatch](tableName: String, path: String, patch: TPatch): 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[TKey](tableName: String, docId: TKey, 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[TContains](tableName: String, criteria: TContains, 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[TKey](tableName: String, docId: TKey): 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[TContains](tableName: String, criteria: TContains): 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/AutoIdTest.scala similarity index 95% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/AutoIdTest.scala index 6c41e8f..36bbc46 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/AutoIdTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/AutoIdTest.scala @@ -1,12 +1,11 @@ -package solutions.bitbadger.documents.scala +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.Assertions._ +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.{DisplayName, Test} -import solutions.bitbadger.documents.scala.support.{ByteIdClass, IntIdClass, LongIdClass, ShortIdClass, StringIdClass} import solutions.bitbadger.documents.{AutoId, DocumentException} -@DisplayName("JVM | Scala | AutoId") -class AutoIdTest { +@DisplayName("Scala | AutoId") +class AutoIdTest: @Test @DisplayName("Generates a UUID string") @@ -125,4 +124,3 @@ class AutoIdTest { @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/solutions/bitbadger/documents/scala/tests/ByteIdClass.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ByteIdClass.scala new file mode 100644 index 0000000..130f16a --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ByteIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +class ByteIdClass(var id: Byte) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ConfigurationTest.scala similarity index 87% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ConfigurationTest.scala index 1864216..f325ff9 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ConfigurationTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ConfigurationTest.scala @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.scala +package solutions.bitbadger.documents.scala.tests +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.{DisplayName, Test} -import org.junit.jupiter.api.Assertions._ import solutions.bitbadger.documents.{AutoId, Configuration, Dialect, DocumentException} -@DisplayName("JVM | Scala | Configuration") -class ConfigurationTest { +@DisplayName("Scala | Configuration") +class ConfigurationTest: @Test @DisplayName("Default ID field is `id`") @@ -32,4 +32,3 @@ class ConfigurationTest { } finally { Configuration.setConnectionString(null) } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQueryTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/CountQueryTest.scala similarity index 90% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQueryTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/CountQueryTest.scala index 1b601b0..00879e6 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/CountQueryTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/CountQueryTest.scala @@ -1,16 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.{AfterEach, DisplayName, Test} 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 solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | CountQuery") -class CountQueryTest { +@DisplayName("Scala | Query | CountQuery") +class CountQueryTest: /** * Clear the connection string (resets Dialect) @@ -66,4 +64,3 @@ class CountQueryTest { def byJsonPathSQLite(): Unit = ForceDialect.sqlite() assertThrows(classOf[DocumentException], () => CountQuery.byJsonPath(TEST_TABLE)) -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQueryTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DefinitionQueryTest.scala similarity index 94% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQueryTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DefinitionQueryTest.scala index d963970..62e4263 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DefinitionQueryTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DefinitionQueryTest.scala @@ -1,16 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.{AfterEach, DisplayName, Test} 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 solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | DefinitionQuery") -class DefinitionQueryTest { +@DisplayName("Scala | Query | DefinitionQuery") +class DefinitionQueryTest: /** * Clear the connection string (resets Dialect) @@ -104,4 +102,3 @@ class DefinitionQueryTest { ForceDialect.sqlite() assertThrows(classOf[DocumentException], () => DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL)) -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQueryTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DeleteQueryTest.scala similarity index 91% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQueryTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DeleteQueryTest.scala index 1106867..47ba9f5 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DeleteQueryTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DeleteQueryTest.scala @@ -1,16 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.{AfterEach, DisplayName, Test} 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 solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | DeleteQuery") -class DeleteQueryTest { +@DisplayName("Scala | Query | DeleteQuery") +class DeleteQueryTest: /** * Clear the connection string (resets Dialect) @@ -74,4 +72,3 @@ class DeleteQueryTest { def byJsonPathSQLite(): Unit = ForceDialect.sqlite() assertThrows(classOf[DocumentException], () => DeleteQuery.byJsonPath(TEST_TABLE)) -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DialectTest.scala similarity index 91% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DialectTest.scala index 5089914..27f86da 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DialectTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DialectTest.scala @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.scala +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.{DisplayName, Test} import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.{DisplayName, Test} import solutions.bitbadger.documents.{Dialect, DocumentException} -@DisplayName("JVM | Scala | Dialect") -class DialectTest { +@DisplayName("Scala | Dialect") +class DialectTest: @Test @DisplayName("deriveFromConnectionString derives PostgreSQL correctly") @@ -31,4 +31,4 @@ class DialectTest { assertTrue(ex.getMessage.contains("[SQL Server]"), "The connection string should have been in the exception message") } -} + diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentIndexTest.scala similarity index 80% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentIndexTest.scala index fd372bf..0f2f1b0 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/DocumentIndexTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentIndexTest.scala @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.scala +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("JVM | Scala | DocumentIndex") -class DocumentIndexTest { +@DisplayName("Scala | DocumentIndex") +class DocumentIndexTest: @Test @DisplayName("FULL uses proper SQL") @@ -16,4 +16,3 @@ class DocumentIndexTest { @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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQueryTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentQueryTest.scala similarity index 93% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQueryTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentQueryTest.scala index 2410773..57a7cb8 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/DocumentQueryTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentQueryTest.scala @@ -1,14 +1,12 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.{AfterEach, DisplayName, Test} -import org.junit.jupiter.api.Assertions._ import solutions.bitbadger.documents.{AutoId, Configuration, DocumentException} import solutions.bitbadger.documents.query.DocumentQuery -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE -@DisplayName("JVM | Scala | Query | DocumentQuery") -class DocumentQueryTest { +@DisplayName("Scala | Query | DocumentQuery") +class DocumentQueryTest: /** * Clear the connection string (resets Dialect) @@ -110,4 +108,3 @@ class DocumentQueryTest { def update(): Unit = assertEquals(s"UPDATE $TEST_TABLE SET data = :data", DocumentQuery.update(TEST_TABLE), "Update query not constructed correctly") -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQueryTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ExistsQueryTest.scala similarity index 90% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQueryTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ExistsQueryTest.scala index 3ab3992..c198d7a 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/ExistsQueryTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ExistsQueryTest.scala @@ -1,16 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.{AfterEach, DisplayName, Test} -import org.junit.jupiter.api.Assertions._ import solutions.bitbadger.documents.{DocumentException, Field} import solutions.bitbadger.documents.query.ExistsQuery -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | ExistsQuery") -class ExistsQueryTest { +@DisplayName("Scala | Query | ExistsQuery") +class ExistsQueryTest: /** * Clear the connection string (resets Dialect) @@ -74,4 +72,3 @@ class ExistsQueryTest { def byJsonPathSQLite(): Unit = ForceDialect.sqlite() assertThrows(classOf[DocumentException], () => ExistsQuery.byJsonPath(TEST_TABLE)) -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldMatchTest.scala similarity index 73% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldMatchTest.scala index 4b2440b..596e6f8 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldMatchTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldMatchTest.scala @@ -1,14 +1,14 @@ -package solutions.bitbadger.documents.scala +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.Assertions._ +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("JVM | Scala | FieldMatch") -class FieldMatchTest { +@DisplayName("Scala | FieldMatch") +class FieldMatchTest: @Test @DisplayName("ANY uses proper SQL") @@ -19,4 +19,3 @@ class FieldMatchTest { @DisplayName("ALL uses proper SQL") def all(): Unit = assertEquals("AND", FieldMatch.ALL.getSql, "ALL should use AND") -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldTest.scala similarity index 98% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldTest.scala index 3923561..0b60727 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/FieldTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldTest.scala @@ -1,14 +1,13 @@ -package solutions.bitbadger.documents.scala +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.{AfterEach, DisplayName, Test} import org.junit.jupiter.api.Assertions.* -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.{Dialect, DocumentException, Field, FieldFormat, Op} +import org.junit.jupiter.api.{AfterEach, DisplayName, Test} +import solutions.bitbadger.documents.* -import scala.jdk.CollectionConverters.* +import _root_.scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Field") -class FieldTest { +@DisplayName("Scala | Field") +class FieldTest: /** * Clear the connection string (resets Dialect) @@ -536,4 +535,3 @@ class FieldTest { 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQueryTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FindQueryTest.scala similarity index 92% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQueryTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FindQueryTest.scala index 06e1586..a44b825 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/FindQueryTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FindQueryTest.scala @@ -1,16 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.{AfterEach, DisplayName, Test} 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 solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | FindQuery") -class FindQueryTest { +@DisplayName("Scala | Query | FindQuery") +class FindQueryTest: /** * Clear the connection string (resets Dialect) @@ -79,4 +77,3 @@ class FindQueryTest { def byJsonPathSQLite(): Unit = ForceDialect.sqlite() assertThrows(classOf[DocumentException], () => FindQuery.byJsonPath(TEST_TABLE)) -} diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ForceDialect.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ForceDialect.scala new file mode 100644 index 0000000..7665d26 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/IntIdClass.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/IntIdClass.scala new file mode 100644 index 0000000..c04bb11 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/LongIdClass.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/LongIdClass.scala new file mode 100644 index 0000000..1f03ffa --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/LongIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +class LongIdClass(var id: Long) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/OpTest.scala similarity index 95% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/OpTest.scala index 62cb683..b28d6fc 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/OpTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/OpTest.scala @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.scala +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("JVM | Kotlin | Op") -class OpTest { +@DisplayName("Scala | Op") +class OpTest: @Test @DisplayName("EQUAL uses proper SQL") @@ -61,4 +61,3 @@ class OpTest { @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/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterNameTest.scala similarity index 89% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterNameTest.scala index d58c4fa..6231060 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterNameTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterNameTest.scala @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.scala +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("JVM | Scala | ParameterName") -class ParameterNameTest { +@DisplayName("Scala | ParameterName") +class ParameterNameTest: @Test @DisplayName("derive works when given existing names") @@ -22,4 +22,3 @@ class ParameterNameTest { 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterTest.scala similarity index 91% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterTest.scala index 0c3c689..11524cf 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/ParameterTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterTest.scala @@ -1,11 +1,11 @@ -package solutions.bitbadger.documents.scala +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("JVM | Scala | Parameter") -class ParameterTest { +@DisplayName("Scala | Parameter") +class ParameterTest: @Test @DisplayName("Construction with colon-prefixed name") @@ -27,4 +27,3 @@ class ParameterTest { @DisplayName("Construction fails with incorrect prefix") def ctorFailsForPrefix(): Unit = assertThrows(classOf[DocumentException], () => Parameter("it", ParameterType.JSON, "")) -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/ParametersTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParametersTest.scala similarity index 83% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/ParametersTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParametersTest.scala index 1420bdc..66e1093 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/ParametersTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParametersTest.scala @@ -1,15 +1,13 @@ -package solutions.bitbadger.documents.scala.jvm +package solutions.bitbadger.documents.scala.tests -import org.junit.jupiter.api.{AfterEach, DisplayName, Test} 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.jvm.Parameters -import solutions.bitbadger.documents.support.ForceDialect +import solutions.bitbadger.documents.scala.Parameters -import scala.jdk.CollectionConverters.* +@DisplayName("Scala | Parameters") +class ParametersTest: -@DisplayName("JVM | Scala | Parameters") -class ParametersTest { /** * Reset the dialect */ @@ -21,7 +19,7 @@ class ParametersTest { @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.asJava).asScala.toList + 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") @@ -32,7 +30,7 @@ class ParametersTest { def nameFieldsChange(): Unit = val fields = Field.equal("a", "") :: Field.equal("e", "", ":hi") :: Field.equal("b", "") :: Field.notExists("z") :: Nil - val named = Parameters.nameFields(fields.asJava).asScala.toList + 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") @@ -44,8 +42,8 @@ class ParametersTest { @Test @DisplayName("replaceNamesInQuery replaces successfully") def replaceNamesInQuery(): Unit = - val parameters = - (Parameter(":data", ParameterType.JSON, "{}") :: Parameter(":data_ext", ParameterType.STRING, "") :: Nil).asJava + 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") @@ -54,7 +52,7 @@ class ParametersTest { @DisplayName("fieldNames generates a single parameter (PostgreSQL)") def fieldNamesSinglePostgres(): Unit = ForceDialect.postgres() - val nameParams = Parameters.fieldNames(("test" :: Nil).asJava).asScala.toList + 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") @@ -64,7 +62,7 @@ class ParametersTest { @DisplayName("fieldNames generates multiple parameters (PostgreSQL)") def fieldNamesMultiplePostgres(): Unit = ForceDialect.postgres() - val nameParams = Parameters.fieldNames(("test" :: "this" :: "today" :: Nil).asJava).asScala.toList + 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") @@ -74,7 +72,7 @@ class ParametersTest { @DisplayName("fieldNames generates a single parameter (SQLite)") def fieldNamesSingleSQLite(): Unit = ForceDialect.sqlite() - val nameParams = Parameters.fieldNames(("test" :: Nil).asJava).asScala.toList + 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") @@ -84,7 +82,7 @@ class ParametersTest { @DisplayName("fieldNames generates multiple parameters (SQLite)") def fieldNamesMultipleSQLite(): Unit = ForceDialect.sqlite() - val nameParams = Parameters.fieldNames(("test" :: "this" :: "today" :: Nil).asJava).asScala.toList + 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") @@ -99,5 +97,4 @@ class ParametersTest { @Test @DisplayName("fieldNames fails if dialect not set") def fieldNamesFails(): Unit = - assertThrows(classOf[DocumentException], () => Parameters.fieldNames(List().asJava)) -} + assertThrows(classOf[DocumentException], () => Parameters.fieldNames(List())) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQueryTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/PatchQueryTest.scala similarity index 90% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQueryTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/PatchQueryTest.scala index 2741f2d..92787dc 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/PatchQueryTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/PatchQueryTest.scala @@ -1,16 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.{AfterEach, DisplayName, Test} -import org.junit.jupiter.api.Assertions._ import solutions.bitbadger.documents.{DocumentException, Field} import solutions.bitbadger.documents.query.PatchQuery -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | PatchQuery") -class PatchQueryTest { +@DisplayName("Scala | Query | PatchQuery") +class PatchQueryTest: /** * Reset the dialect @@ -72,4 +70,3 @@ class PatchQueryTest { def byJsonPathSQLite(): Unit = ForceDialect.sqlite() assertThrows(classOf[DocumentException], () => PatchQuery.byJsonPath(TEST_TABLE)) -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/QueryUtilsTest.scala similarity index 96% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/QueryUtilsTest.scala index 3076708..77ca9fd 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/QueryUtilsTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/QueryUtilsTest.scala @@ -1,15 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.{AfterEach, DisplayName, Test} -import org.junit.jupiter.api.Assertions._ import solutions.bitbadger.documents.{Dialect, Field, FieldMatch} import solutions.bitbadger.documents.query.QueryUtils -import solutions.bitbadger.documents.support.ForceDialect import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | Package Functions") -class QueryUtilsTest { +@DisplayName("Scala | Query | Package Functions") +class QueryUtilsTest: /** * Clear the connection string (resets Dialect) @@ -135,4 +134,3 @@ class QueryUtilsTest { 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/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/RemoveFieldsQueryTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/RemoveFieldsQueryTest.scala similarity index 91% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/RemoveFieldsQueryTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/RemoveFieldsQueryTest.scala index e5e93e0..59296dc 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/RemoveFieldsQueryTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/RemoveFieldsQueryTest.scala @@ -1,16 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.{AfterEach, DisplayName, Test} -import org.junit.jupiter.api.Assertions._ import solutions.bitbadger.documents.{DocumentException, Field, Parameter, ParameterType} import solutions.bitbadger.documents.query.RemoveFieldsQuery -import solutions.bitbadger.documents.support.ForceDialect -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | RemoveFieldsQuery") -class RemoveFieldsQueryTest { +@DisplayName("Scala | Query | RemoveFieldsQuery") +class RemoveFieldsQueryTest: /** * Reset the dialect @@ -82,4 +80,3 @@ class RemoveFieldsQueryTest { def byJsonPathSQLite(): Unit = ForceDialect.sqlite() assertThrows(classOf[DocumentException], () => RemoveFieldsQuery.byJsonPath(TEST_TABLE, List().asJava)) -} diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ShortIdClass.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ShortIdClass.scala new file mode 100644 index 0000000..35f2026 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/StringIdClass.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/StringIdClass.scala new file mode 100644 index 0000000..2a97774 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/StringIdClass.scala @@ -0,0 +1,3 @@ +package solutions.bitbadger.documents.scala.tests + +class StringIdClass(var id: String) diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/WhereTest.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/WhereTest.scala similarity index 95% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/WhereTest.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/WhereTest.scala index 26acf2c..87e05e1 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/query/WhereTest.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/WhereTest.scala @@ -1,15 +1,14 @@ -package solutions.bitbadger.documents.scala.query +package solutions.bitbadger.documents.scala.tests +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.{AfterEach, DisplayName, Test} -import org.junit.jupiter.api.Assertions._ import solutions.bitbadger.documents.{DocumentException, Field, FieldMatch} import solutions.bitbadger.documents.query.Where -import solutions.bitbadger.documents.support.ForceDialect import scala.jdk.CollectionConverters.* -@DisplayName("JVM | Scala | Query | Where") -class WhereTest { +@DisplayName("Scala | Query | Where") +class WhereTest: /** * Clear the connection string (resets Dialect) @@ -140,4 +139,3 @@ class WhereTest { def jsonPathFailsSQLite(): Unit = ForceDialect.sqlite() assertThrows(classOf[DocumentException], () => Where.jsonPathMatches()) -} diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ArrayDocument.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ArrayDocument.scala new file mode 100644 index 0000000..83d1081 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/CountFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CountFunctions.scala new file mode 100644 index 0000000..0fce19b --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/CustomFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CustomFunctions.scala new file mode 100644 index 0000000..0e6f992 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CustomFunctions.scala @@ -0,0 +1,52 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import org.junit.jupiter.api.Assertions.* +import solutions.bitbadger.documents.query.{CountQuery, DeleteQuery, FindQuery} +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} + +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 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 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/solutions/bitbadger/documents/scala/tests/integration/DefinitionFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DefinitionFunctions.scala new file mode 100644 index 0000000..80edff0 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/DeleteFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DeleteFunctions.scala new file mode 100644 index 0000000..faf1281 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/DocumentFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DocumentFunctions.scala new file mode 100644 index 0000000..210d6de --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DocumentFunctions.scala @@ -0,0 +1,111 @@ +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/solutions/bitbadger/documents/scala/tests/integration/ExistsFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ExistsFunctions.scala new file mode 100644 index 0000000..3f18932 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/FindFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/FindFunctions.scala new file mode 100644 index 0000000..a5680a1 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/FindFunctions.scala @@ -0,0 +1,217 @@ +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/solutions/bitbadger/documents/scala/tests/integration/JacksonDocumentSerializer.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JacksonDocumentSerializer.scala new file mode 100644 index 0000000..6cac9df --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/JsonDocument.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JsonDocument.scala similarity index 64% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/JsonDocument.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JsonDocument.scala index 861eeab..899409d 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/JsonDocument.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JsonDocument.scala @@ -1,8 +1,7 @@ -package solutions.bitbadger.documents.scala.support +package solutions.bitbadger.documents.scala.tests.integration -import solutions.bitbadger.documents.jvm.Document -import solutions.bitbadger.documents.support.ThrowawayDatabase -import solutions.bitbadger.documents.support.TypesKt.TEST_TABLE +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) @@ -17,4 +16,4 @@ object JsonDocument: JsonDocument("five", "purple", 18, null)) def load(db: ThrowawayDatabase, tableName: String = TEST_TABLE): Unit = - testDocuments.foreach { it => Document.insert(tableName, it, db.getConn) } + testDocuments.foreach { it => db.conn.insert(tableName, it) } diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/NumIdDocument.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/NumIdDocument.scala new file mode 100644 index 0000000..e426246 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/PatchFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PatchFunctions.scala new file mode 100644 index 0000000..b999c30 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/PgDB.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PgDB.scala new file mode 100644 index 0000000..27c1e41 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CountIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCountIT.scala similarity index 81% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CountIT.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCountIT.scala index 1533bde..fcb84e1 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CountIT.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCountIT.scala @@ -1,13 +1,11 @@ -package solutions.bitbadger.documents.scala.jvm.integration.postgresql +package solutions.bitbadger.documents.scala.tests.integration import org.junit.jupiter.api.{DisplayName, Test} -import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB -import solutions.bitbadger.documents.scala.jvm.integration.common.CountFunctions import scala.util.Using -@DisplayName("JVM | Scala | PostgreSQL: Count") -class CountIT { +@DisplayName("Scala | PostgreSQL: Count") +class PostgreSQLCountIT: @Test @DisplayName("all counts all documents") @@ -43,4 +41,3 @@ class CountIT { @DisplayName("byJsonPath counts documents when no matches are found") def byJsonPathNoMatch(): Unit = Using(PgDB()) { db => CountFunctions.byJsonPathNoMatch(db) } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CustomIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCustomIT.scala similarity index 79% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CustomIT.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCustomIT.scala index 4e74f7d..8ad437b 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/postgresql/CustomIT.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCustomIT.scala @@ -1,13 +1,11 @@ -package solutions.bitbadger.documents.scala.jvm.integration.postgresql +package solutions.bitbadger.documents.scala.tests.integration import org.junit.jupiter.api.{DisplayName, Test} -import solutions.bitbadger.documents.jvm.integration.postgresql.PgDB -import solutions.bitbadger.documents.scala.jvm.integration.common.CustomFunctions import scala.util.Using -@DisplayName("JVM | Scala | PostgreSQL: Custom") -class CustomIT { +@DisplayName("Scala | PostgreSQL: Custom") +class PostgreSQLCustomIT: @Test @DisplayName("list succeeds with empty list") @@ -43,4 +41,3 @@ class CustomIT { @DisplayName("scalar succeeds") def scalar(): Unit = Using(PgDB()) { db => CustomFunctions.scalar(db) } -} diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDefinitionIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDefinitionIT.scala new file mode 100644 index 0000000..abdf325 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDeleteIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDeleteIT.scala new file mode 100644 index 0000000..df349b0 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDocumentIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDocumentIT.scala new file mode 100644 index 0000000..77ebe77 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLExistsIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLExistsIT.scala new file mode 100644 index 0000000..ea04640 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLFindIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLFindIT.scala new file mode 100644 index 0000000..7074ada --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLFindIT.scala @@ -0,0 +1,172 @@ +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/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLPatchIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLPatchIT.scala new file mode 100644 index 0000000..06e9410 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLRemoveFieldsIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLRemoveFieldsIT.scala new file mode 100644 index 0000000..ffbc6c2 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/RemoveFieldsFunctions.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/RemoveFieldsFunctions.scala new file mode 100644 index 0000000..1531355 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CountIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCountIT.scala similarity index 78% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CountIT.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCountIT.scala index c77f453..eba322d 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CountIT.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCountIT.scala @@ -1,15 +1,13 @@ -package solutions.bitbadger.documents.scala.jvm.integration.sqlite +package solutions.bitbadger.documents.scala.tests.integration -import org.junit.jupiter.api.{DisplayName, Test} -import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB -import solutions.bitbadger.documents.scala.jvm.integration.common.CountFunctions import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.{DisplayName, Test} import solutions.bitbadger.documents.DocumentException import scala.util.Using -@DisplayName("JVM | Scala | SQLite: Count") -class CountIT { +@DisplayName("Scala | SQLite: Count") +class SQLiteCountIT: @Test @DisplayName("all counts all documents") @@ -35,4 +33,3 @@ class CountIT { @DisplayName("byJsonPath fails") def byJsonPathMatch(): Unit = Using(SQLiteDB()) { db => assertThrows(classOf[DocumentException], () => CountFunctions.byJsonPathMatch(db)) } -} diff --git a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CustomIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCustomIT.scala similarity index 80% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CustomIT.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCustomIT.scala index 36c08ab..f393a1e 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/jvm/integration/sqlite/CustomIT.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCustomIT.scala @@ -1,13 +1,11 @@ -package solutions.bitbadger.documents.scala.jvm.integration.sqlite +package solutions.bitbadger.documents.scala.tests.integration import org.junit.jupiter.api.{DisplayName, Test} -import solutions.bitbadger.documents.jvm.integration.sqlite.SQLiteDB -import solutions.bitbadger.documents.scala.jvm.integration.common.CustomFunctions import scala.util.Using -@DisplayName("JVM | Scala | SQLite: Custom") -class CustomIT { +@DisplayName("Scala | SQLite: Custom") +class SQLiteCustomIT: @Test @DisplayName("list succeeds with empty list") @@ -43,4 +41,3 @@ class CustomIT { @DisplayName("scalar succeeds") def scalar(): Unit = Using(SQLiteDB()) { db => CustomFunctions.scalar(db) } -} diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDB.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDB.scala new file mode 100644 index 0000000..5a90a0e --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/SQLiteDefinitionIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDefinitionIT.scala new file mode 100644 index 0000000..051d820 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/SQLiteDeleteIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDeleteIT.scala new file mode 100644 index 0000000..35c9c91 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/SQLiteDocumentIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDocumentIT.scala new file mode 100644 index 0000000..9c3040a --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/SQLiteExistsIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteExistsIT.scala new file mode 100644 index 0000000..86636b4 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/SQLiteFindIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteFindIT.scala new file mode 100644 index 0000000..c99bcdf --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/SQLitePatchIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLitePatchIT.scala new file mode 100644 index 0000000..fb77238 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/integration/SQLiteRemoveFieldsIT.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteRemoveFieldsIT.scala new file mode 100644 index 0000000..0d8a679 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/SubDocument.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SubDocument.scala similarity index 50% rename from src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/SubDocument.scala rename to src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SubDocument.scala index 9ec17d5..66f65b5 100644 --- a/src/jvm/src/test/scala/solutions/bitbadger/documents/scala/support/SubDocument.scala +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SubDocument.scala @@ -1,3 +1,3 @@ -package solutions.bitbadger.documents.scala.support +package solutions.bitbadger.documents.scala.tests.integration class SubDocument(val foo: String = "", val bar: String = "") diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ThrowawayDatabase.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ThrowawayDatabase.scala new file mode 100644 index 0000000..4d12442 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/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/solutions/bitbadger/documents/scala/tests/package.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/package.scala new file mode 100644 index 0000000..8992837 --- /dev/null +++ b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/package.scala @@ -0,0 +1,6 @@ +package solutions.bitbadger.documents.scala + +package object tests { + + def TEST_TABLE = "test_table" +} \ No newline at end of file -- 2.47.2 From db720515fab4cf1899a56c5dbf874d0ee46bb9b9 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 25 Mar 2025 08:19:20 -0400 Subject: [PATCH 61/88] First cut of README; tweak kotlinx pom --- .idea/compiler.xml | 2 +- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++- src/kotlinx/pom.xml | 2 +- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index ea58415..0d99751 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -6,6 +6,7 @@ + @@ -19,7 +20,6 @@ - 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/src/kotlinx/pom.xml b/src/kotlinx/pom.xml index c0d6f73..272ec63 100644 --- a/src/kotlinx/pom.xml +++ b/src/kotlinx/pom.xml @@ -58,7 +58,7 @@ test-compile - test-compile + process-test-sources test-compile -- 2.47.2 From 85b60a649c5827bc77ad5e4d4a8fddfc3a2d35ac Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 25 Mar 2025 15:40:55 -0400 Subject: [PATCH 62/88] Update version; begin package prep --- pom.xml | 4 +++- src/core/pom.xml | 28 +++++++++++++++++++++++++++- src/groovy/pom.xml | 2 +- src/kotlinx/pom.xml | 2 +- src/scala/pom.xml | 8 +------- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 7dfbc5a..cdb1d1f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-alpha1-SNAPSHOT + 4.0.0-RC1 pom ${project.groupId}:${project.artifactId} @@ -49,6 +49,8 @@ 2.18.2 3.46.1.2 42.7.5 + 3.3.1 + 3.11.2 diff --git a/src/core/pom.xml b/src/core/pom.xml index fd79794..2da4e57 100644 --- a/src/core/pom.xml +++ b/src/core/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-alpha1-SNAPSHOT + 4.0.0-RC1 ../../pom.xml @@ -83,6 +83,32 @@ ${java.version} + + 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 + + + + diff --git a/src/groovy/pom.xml b/src/groovy/pom.xml index 22d1d7c..41e5ece 100644 --- a/src/groovy/pom.xml +++ b/src/groovy/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-alpha1-SNAPSHOT + 4.0.0-RC1 ../../pom.xml diff --git a/src/kotlinx/pom.xml b/src/kotlinx/pom.xml index 272ec63..10eb8a8 100644 --- a/src/kotlinx/pom.xml +++ b/src/kotlinx/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-alpha1-SNAPSHOT + 4.0.0-RC1 ../../pom.xml diff --git a/src/scala/pom.xml b/src/scala/pom.xml index 566eadb..7ed1b27 100644 --- a/src/scala/pom.xml +++ b/src/scala/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-alpha1-SNAPSHOT + 4.0.0-RC1 ../../pom.xml @@ -17,12 +17,6 @@ Expose a document store interface for PostgreSQL and SQLite (Scala Library) https://bitbadger.solutions/open-source/relational-documents/jvm/ - - UTF-8 - official - 1.8 - - ${project.basedir}/src/main/scala ${project.basedir}/src/test/scala -- 2.47.2 From f1385b6d55c418bbd2ec1dd130bd929f865ed96f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 25 Mar 2025 16:57:24 -0400 Subject: [PATCH 63/88] Flatten Scala source trees --- src/core/pom.xml | 3 + .../documents/scala => }/Count.scala | 8 +- .../documents/scala => }/Custom.scala | 62 +-- .../documents/scala => }/Definition.scala | 0 .../documents/scala => }/Delete.scala | 10 +- .../documents/scala => }/Document.scala | 28 +- .../documents/scala => }/Exists.scala | 10 +- .../bitbadger/documents/scala => }/Find.scala | 159 +++--- .../documents/scala => }/Parameters.scala | 12 +- .../documents/scala => }/Patch.scala | 22 +- .../documents/scala => }/RemoveFields.scala | 8 +- .../documents/scala => }/Results.scala | 16 +- .../src/main/scala/extensions/package.scala | 497 +++++++++++++++++ .../documents/scala/extensions/package.scala | 499 ------------------ .../scala/tests => }/AutoIdTest.scala | 0 .../scala/tests => }/ByteIdClass.scala | 0 .../scala/tests => }/ConfigurationTest.scala | 0 .../scala/tests => }/CountQueryTest.scala | 0 .../tests => }/DefinitionQueryTest.scala | 0 .../scala/tests => }/DeleteQueryTest.scala | 0 .../scala/tests => }/DialectTest.scala | 0 .../scala/tests => }/DocumentIndexTest.scala | 0 .../scala/tests => }/DocumentQueryTest.scala | 0 .../scala/tests => }/ExistsQueryTest.scala | 0 .../scala/tests => }/FieldMatchTest.scala | 0 .../scala/tests => }/FieldTest.scala | 0 .../scala/tests => }/FindQueryTest.scala | 0 .../scala/tests => }/ForceDialect.scala | 6 +- .../scala/tests => }/IntIdClass.scala | 0 .../scala/tests => }/LongIdClass.scala | 0 .../documents/scala/tests => }/OpTest.scala | 0 .../scala/tests => }/ParameterNameTest.scala | 0 .../scala/tests => }/ParameterTest.scala | 0 .../scala/tests => }/ParametersTest.scala | 0 .../scala/tests => }/PatchQueryTest.scala | 0 .../scala/tests => }/QueryUtilsTest.scala | 0 .../tests => }/RemoveFieldsQueryTest.scala | 0 .../scala/tests => }/ShortIdClass.scala | 0 .../scala/tests => }/StringIdClass.scala | 0 .../scala/tests => }/WhereTest.scala | 0 .../integration/ArrayDocument.scala | 0 .../integration/CountFunctions.scala | 0 .../integration/CustomFunctions.scala | 0 .../integration/DefinitionFunctions.scala | 0 .../integration/DeleteFunctions.scala | 0 .../integration/DocumentFunctions.scala | 0 .../integration/ExistsFunctions.scala | 0 .../integration/FindFunctions.scala | 0 .../JacksonDocumentSerializer.scala | 0 .../tests => }/integration/JsonDocument.scala | 0 .../integration/NumIdDocument.scala | 0 .../integration/PatchFunctions.scala | 0 .../scala/tests => }/integration/PgDB.scala | 0 .../integration/PostgreSQLCountIT.scala | 0 .../integration/PostgreSQLCustomIT.scala | 0 .../integration/PostgreSQLDefinitionIT.scala | 0 .../integration/PostgreSQLDeleteIT.scala | 0 .../integration/PostgreSQLDocumentIT.scala | 0 .../integration/PostgreSQLExistsIT.scala | 0 .../integration/PostgreSQLFindIT.scala | 0 .../integration/PostgreSQLPatchIT.scala | 0 .../PostgreSQLRemoveFieldsIT.scala | 0 .../integration/RemoveFieldsFunctions.scala | 0 .../integration/SQLiteCountIT.scala | 0 .../integration/SQLiteCustomIT.scala | 0 .../tests => }/integration/SQLiteDB.scala | 0 .../integration/SQLiteDefinitionIT.scala | 0 .../integration/SQLiteDeleteIT.scala | 0 .../integration/SQLiteDocumentIT.scala | 0 .../integration/SQLiteExistsIT.scala | 0 .../tests => }/integration/SQLiteFindIT.scala | 0 .../integration/SQLitePatchIT.scala | 0 .../integration/SQLiteRemoveFieldsIT.scala | 0 .../tests => }/integration/SubDocument.scala | 0 .../integration/ThrowawayDatabase.scala | 0 src/scala/src/test/scala/package.scala | 3 + .../documents/scala/tests/package.scala | 6 - 77 files changed, 670 insertions(+), 679 deletions(-) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Count.scala (92%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Custom.scala (73%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Definition.scala (100%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Delete.scala (91%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Document.scala (71%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Exists.scala (92%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Find.scala (67%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Parameters.scala (90%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Patch.scala (83%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/RemoveFields.scala (93%) rename src/scala/src/main/scala/{solutions/bitbadger/documents/scala => }/Results.scala (82%) create mode 100644 src/scala/src/main/scala/extensions/package.scala delete mode 100644 src/scala/src/main/scala/solutions/bitbadger/documents/scala/extensions/package.scala rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/AutoIdTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/ByteIdClass.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/ConfigurationTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/CountQueryTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/DefinitionQueryTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/DeleteQueryTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/DialectTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/DocumentIndexTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/DocumentQueryTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/ExistsQueryTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/FieldMatchTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/FieldTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/FindQueryTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/ForceDialect.scala (83%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/IntIdClass.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/LongIdClass.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/OpTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/ParameterNameTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/ParameterTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/ParametersTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/PatchQueryTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/QueryUtilsTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/RemoveFieldsQueryTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/ShortIdClass.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/StringIdClass.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/WhereTest.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/ArrayDocument.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/CountFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/CustomFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/DefinitionFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/DeleteFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/DocumentFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/ExistsFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/FindFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/JacksonDocumentSerializer.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/JsonDocument.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/NumIdDocument.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PatchFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PgDB.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLCountIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLCustomIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLDefinitionIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLDeleteIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLDocumentIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLExistsIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLFindIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLPatchIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/PostgreSQLRemoveFieldsIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/RemoveFieldsFunctions.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteCountIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteCustomIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteDB.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteDefinitionIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteDeleteIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteDocumentIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteExistsIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteFindIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLitePatchIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SQLiteRemoveFieldsIT.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/SubDocument.scala (100%) rename src/scala/src/test/scala/{solutions/bitbadger/documents/scala/tests => }/integration/ThrowawayDatabase.scala (100%) create mode 100644 src/scala/src/test/scala/package.scala delete mode 100644 src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/package.scala diff --git a/src/core/pom.xml b/src/core/pom.xml index 2da4e57..d54de26 100644 --- a/src/core/pom.xml +++ b/src/core/pom.xml @@ -106,6 +106,9 @@ jar + + ${project.basedir}/src/main/kotlin + diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Count.scala b/src/scala/src/main/scala/Count.scala similarity index 92% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Count.scala rename to src/scala/src/main/scala/Count.scala index a2a1d3c..df5a319 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Count.scala +++ b/src/scala/src/main/scala/Count.scala @@ -62,12 +62,12 @@ object Count: * 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 + * @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[TContains](tableName: String, criteria: TContains, conn: Connection): Long = + def byContains[A](tableName: String, criteria: A, conn: Connection): Long = CoreCount.byContains(tableName, criteria, conn) /** @@ -78,7 +78,7 @@ object Count: * @return A count of the matching documents in the table * @throws DocumentException If called on a SQLite connection */ - def byContains[TContains](tableName: String, criteria: TContains): Long = + def byContains[A](tableName: String, criteria: A): Long = CoreCount.byContains(tableName, criteria) /** diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Custom.scala b/src/scala/src/main/scala/Custom.scala similarity index 73% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Custom.scala rename to src/scala/src/main/scala/Custom.scala index b59bee2..d946b33 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Custom.scala +++ b/src/scala/src/main/scala/Custom.scala @@ -18,9 +18,9 @@ object Custom: * @return A list of results for the given query * @throws DocumentException If parameters are invalid */ - def list[TDoc](query: String, parameters: Seq[Parameter[?]], conn: Connection, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = - Using(Parameters.apply(conn, query, parameters)) { stmt => Results.toCustomList[TDoc](stmt, mapFunc) }.get + 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 @@ -31,8 +31,8 @@ object Custom: * @return A list of results for the given query * @throws DocumentException If parameters are invalid */ - def list[TDoc](query: String, conn: Connection, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + def list[Doc](query: String, conn: Connection, mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): List[Doc] = list(query, List(), conn, mapFunc) /** @@ -44,9 +44,9 @@ object Custom: * @return A list of results for the given query * @throws DocumentException If parameters are invalid */ - def list[TDoc](query: String, parameters: Seq[Parameter[?]], - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = - Using(Configuration.dbConn()) { conn => list[TDoc](query, parameters, conn, mapFunc) }.get + 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) @@ -56,8 +56,7 @@ object Custom: * @return A list of results for the given query * @throws DocumentException If parameters are invalid */ - def list[TDoc](query: String, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + def list[Doc](query: String, mapFunc: (ResultSet, ClassTag[Doc]) => Doc)(using tag: ClassTag[Doc]): List[Doc] = list(query, List(), mapFunc) /** @@ -70,9 +69,9 @@ object Custom: * @return An `Option` value, with the document if one matches the query * @throws DocumentException If parameters are invalid */ - def single[TDoc](query: String, parameters: Seq[Parameter[?]], conn: Connection, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - list[TDoc](s"$query LIMIT 1", parameters, conn, mapFunc).headOption + 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 @@ -83,9 +82,9 @@ object Custom: * @return An `Option` value, with the document if one matches the query * @throws DocumentException If parameters are invalid */ - def single[TDoc](query: String, conn: Connection, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - list[TDoc](s"$query LIMIT 1", List(), conn, mapFunc).headOption + 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) @@ -96,9 +95,9 @@ object Custom: * @return An `Option` value, with the document if one matches the query * @throws DocumentException If parameters are invalid */ - def single[TDoc](query: String, parameters: Seq[Parameter[?]], - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Using(Configuration.dbConn()) { conn => single[TDoc](query, parameters, conn, mapFunc) }.get + 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) @@ -108,9 +107,8 @@ object Custom: * @return An `Option` value, with the document if one matches the query * @throws DocumentException If parameters are invalid */ - def single[TDoc](query: String, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - single[TDoc](query, List(), mapFunc) + 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 no results @@ -162,8 +160,8 @@ object Custom: * @return The scalar value from the query * @throws DocumentException If parameters are invalid */ - def scalar[T](query: String, parameters: Seq[Parameter[?]], conn: Connection, - mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = + 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() @@ -180,9 +178,8 @@ object Custom: * @return The scalar value from the query * @throws DocumentException If parameters are invalid */ - def scalar[T](query: String, conn: Connection, - mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = - scalar[T](query, List(), conn, mapFunc) + 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) @@ -193,9 +190,9 @@ object Custom: * @return The scalar value from the query * @throws DocumentException If parameters are invalid */ - def scalar[T](query: String, parameters: Seq[Parameter[?]], - mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = - Using(Configuration.dbConn()) { conn => scalar[T](query, parameters, conn, mapFunc) }.get + 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) @@ -205,6 +202,5 @@ object Custom: * @return The scalar value from the query * @throws DocumentException If parameters are invalid */ - def scalar[T](query: String, - mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = - scalar[T](query, List(), mapFunc) + 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/solutions/bitbadger/documents/scala/Definition.scala b/src/scala/src/main/scala/Definition.scala similarity index 100% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Definition.scala rename to src/scala/src/main/scala/Definition.scala diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Delete.scala b/src/scala/src/main/scala/Delete.scala similarity index 91% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Delete.scala rename to src/scala/src/main/scala/Delete.scala index 19729af..bd67103 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Delete.scala +++ b/src/scala/src/main/scala/Delete.scala @@ -19,7 +19,7 @@ object Delete: * @param conn The connection on which the deletion should be executed * @throws DocumentException If no dialect has been configured */ - def byId[TKey](tableName: String, docId: TKey, conn: Connection): Unit = + def byId[Key](tableName: String, docId: Key, conn: Connection): Unit = CoreDelete.byId(tableName, docId, conn) /** @@ -29,7 +29,7 @@ object Delete: * @param docId The ID of the document to be deleted * @throws DocumentException If no connection string has been set */ - def byId[TKey](tableName: String, docId: TKey): Unit = + def byId[Key](tableName: String, docId: Key): Unit = CoreDelete.byId(tableName, docId) /** @@ -63,7 +63,7 @@ object Delete: * @param conn The connection on which the deletion should be executed * @throws DocumentException If called on a SQLite connection */ - def byContains[TContains](tableName: String, criteria: TContains, conn: Connection): Unit = + def byContains[A](tableName: String, criteria: A, conn: Connection): Unit = CoreDelete.byContains(tableName, criteria, conn) /** @@ -73,7 +73,7 @@ object Delete: * @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[TContains](tableName: String, criteria: TContains): Unit = + def byContains[A](tableName: String, criteria: A): Unit = CoreDelete.byContains(tableName, criteria) /** @@ -91,7 +91,7 @@ object Delete: * 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 + * @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 = diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Document.scala b/src/scala/src/main/scala/Document.scala similarity index 71% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Document.scala rename to src/scala/src/main/scala/Document.scala index ca23662..cab75fe 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Document.scala +++ b/src/scala/src/main/scala/Document.scala @@ -14,7 +14,7 @@ object Document: * @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[TDoc](tableName: String, document: TDoc, conn: Connection): Unit = + def insert[Doc](tableName: String, document: Doc, conn: Connection): Unit = CoreDocument.insert(tableName, document, conn) /** @@ -24,49 +24,49 @@ object Document: * @param document The document to be inserted * @throws DocumentException If IDs are misconfigured, or if the database command fails */ - def insert[TDoc](tableName: String, document: TDoc): Unit = + 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 + * @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[TDoc](tableName: String, document: TDoc, conn: Connection): Unit = + 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 + * @param document The document to be saved * @throws DocumentException If the database command fails */ - def save[TDoc](tableName: String, document: TDoc): Unit = + 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 + * @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[TKey, TDoc](tableName: String, docId: TKey, document: TDoc, conn: Connection): Unit = + 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 + * @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[TKey, TDoc](tableName: String, docId: TKey, document: TDoc): Unit = + def update[Key, Doc](tableName: String, docId: Key, document: Doc): Unit = CoreDocument.update(tableName, docId, document) diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Exists.scala b/src/scala/src/main/scala/Exists.scala similarity index 92% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Exists.scala rename to src/scala/src/main/scala/Exists.scala index 90883f0..ef1c4fb 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Exists.scala +++ b/src/scala/src/main/scala/Exists.scala @@ -20,7 +20,7 @@ object Exists: * @return True if the document exists, false if not * @throws DocumentException If no dialect has been configured */ - def byId[TKey](tableName: String, docId: TKey, conn: Connection): Boolean = + def byId[Key](tableName: String, docId: Key, conn: Connection): Boolean = CoreExists.byId(tableName, docId, conn) /** @@ -31,7 +31,7 @@ object Exists: * @return True if the document exists, false if not * @throws DocumentException If no connection string has been set */ - def byId[TKey](tableName: String, docId: TKey): Boolean = + def byId[Key](tableName: String, docId: Key): Boolean = CoreExists.byId(tableName, docId) /** @@ -68,7 +68,7 @@ object Exists: * @return True if any matching documents exist, false if not * @throws DocumentException If called on a SQLite connection */ - def byContains[TContains](tableName: String, criteria: TContains, conn: Connection): Boolean = + def byContains[A](tableName: String, criteria: A, conn: Connection): Boolean = CoreExists.byContains(tableName, criteria, conn) /** @@ -79,7 +79,7 @@ object Exists: * @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[TContains](tableName: String, criteria: TContains): Boolean = + def byContains[A](tableName: String, criteria: A): Boolean = CoreExists.byContains(tableName, criteria) /** @@ -98,7 +98,7 @@ object Exists: * 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 + * @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 */ diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Find.scala b/src/scala/src/main/scala/Find.scala similarity index 67% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Find.scala rename to src/scala/src/main/scala/Find.scala index eb6534f..4335be7 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Find.scala +++ b/src/scala/src/main/scala/Find.scala @@ -22,8 +22,8 @@ object Find: * @return A list of documents from the given table * @throws DocumentException If query execution fails */ - def all[TDoc](tableName: String, orderBy: Seq[Field[?]], conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = - Custom.list[TDoc](FindQuery.all(tableName) + QueryUtils.orderBy(orderBy.asJava), conn, Results.fromData) + 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 @@ -33,43 +33,43 @@ object Find: * @return A list of documents from the given table * @throws DocumentException If query execution fails */ - def all[TDoc](tableName: String, conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = - all[TDoc](tableName, List(), conn) + 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) + * @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[TDoc](tableName: String, orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): List[TDoc] = - Using(Configuration.dbConn()) { conn => all[TDoc](tableName, orderBy, conn) }.get + def all[Doc](tableName: String, orderBy: Seq[Field[?]] = List())(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 + * @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[TKey, TDoc](tableName: String, docId: TKey, conn: Connection)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Custom.single[TDoc](FindQuery.byId(tableName, docId), + 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 + * @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[TKey, TDoc](tableName: String, docId: TKey)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Using(Configuration.dbConn()) { conn => byId[TKey, TDoc](tableName, docId, conn) }.get + 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 @@ -82,10 +82,10 @@ object Find: * @return A list of documents matching the field comparison * @throws DocumentException If no dialect has been configured, or if parameters are invalid */ - def byFields[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], orderBy: Seq[Field[?]], - conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = + 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[TDoc]( + Custom.list[Doc]( FindQuery.byFields(tableName, named.asJava, howMatched.orNull) + QueryUtils.orderBy(orderBy.asJava), Parameters.addFields(named).toSeq, conn, Results.fromData) @@ -99,9 +99,9 @@ object Find: * @return A list of documents matching the field comparison * @throws DocumentException If no dialect has been configured, or if parameters are invalid */ - def byFields[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection) - (implicit tag: ClassTag[TDoc]): List[TDoc] = - byFields[TDoc](tableName, fields, howMatched, List(), conn) + 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 @@ -113,9 +113,9 @@ object Find: * @return A list of documents matching the field comparison * @throws DocumentException If no dialect has been configured, or if parameters are invalid */ - def byFields[TDoc](tableName: String, fields: Seq[Field[?]], orderBy: Seq[Field[?]], conn: Connection) - (implicit tag: ClassTag[TDoc]): List[TDoc] = - byFields[TDoc](tableName, fields, None, orderBy, conn) + 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) @@ -127,9 +127,9 @@ object Find: * @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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, - orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): List[TDoc] = - Using(Configuration.dbConn()) { conn => byFields[TDoc](tableName, fields, howMatched, orderBy, conn) }.get + def byFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = List())(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) @@ -141,9 +141,9 @@ object Find: * @return A list of documents matching the JSON containment query * @throws DocumentException If called on a SQLite connection */ - def byContains[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]], - conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = - Custom.list[TDoc](FindQuery.byContains(tableName) + QueryUtils.orderBy(orderBy.asJava), + 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) /** @@ -155,9 +155,8 @@ object Find: * @return A list of documents matching the JSON containment query * @throws DocumentException If called on a SQLite connection */ - def byContains[TDoc, TContains](tableName: String, criteria: TContains, conn: Connection) - (implicit tag: ClassTag[TDoc]): List[TDoc] = - byContains[TDoc, TContains](tableName, criteria, List(), conn) + 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 @@ -169,9 +168,9 @@ object Find: * @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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]] = List()) - (implicit tag: ClassTag[TDoc]): List[TDoc] = - Using(Configuration.dbConn()) { conn => byContains[TDoc, TContains](tableName, criteria, orderBy, conn) }.get + def byContains[Doc, A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = List()) + (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) @@ -183,9 +182,9 @@ object Find: * @return A list of documents matching the JSON Path match query * @throws DocumentException If called on a SQLite connection */ - def byJsonPath[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]], conn: Connection) - (implicit tag: ClassTag[TDoc]): List[TDoc] = - Custom.list[TDoc](FindQuery.byJsonPath(tableName) + QueryUtils.orderBy(orderBy.asJava), + 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) /** @@ -197,8 +196,8 @@ object Find: * @return A list of documents matching the JSON Path match query * @throws DocumentException If called on a SQLite connection */ - def byJsonPath[TDoc](tableName: String, path: String, conn: Connection)(implicit tag: ClassTag[TDoc]): List[TDoc] = - byJsonPath[TDoc](tableName, path, List(), conn) + 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 @@ -210,9 +209,9 @@ object Find: * @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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) - (implicit tag: ClassTag[TDoc]): List[TDoc] = - Using(Configuration.dbConn()) { conn => byJsonPath[TDoc](tableName, path, orderBy, conn) }.get + def byJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) + (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 @@ -225,10 +224,10 @@ object Find: * @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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], - orderBy: Seq[Field[?]], conn: Connection)(implicit tag: ClassTag[TDoc]): Option[TDoc] = + 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[TDoc]( + Custom.single[Doc]( FindQuery.byFields(tableName, named.asJava, howMatched.orNull) + QueryUtils.orderBy(orderBy.asJava), Parameters.addFields(named).toSeq, conn, Results.fromData) @@ -242,9 +241,9 @@ object Find: * @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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch], conn: Connection) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - firstByFields[TDoc](tableName, fields, howMatched, List(), conn) + 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 @@ -256,9 +255,9 @@ object Find: * @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[TDoc](tableName: String, fields: Seq[Field[?]], orderBy: Seq[Field[?]], conn: Connection) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - firstByFields[TDoc](tableName, fields, None, orderBy, conn) + 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 @@ -269,9 +268,9 @@ object Find: * @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[TDoc](tableName: String, fields: Seq[Field[?]], conn: Connection) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - firstByFields[TDoc](tableName, fields, None, List(), conn) + 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) @@ -283,9 +282,9 @@ object Find: * @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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, - orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Using(Configuration.dbConn()) { conn => firstByFields[TDoc](tableName, fields, howMatched, orderBy, conn) }.get + def firstByFields[Doc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, + orderBy: Seq[Field[?]] = List())(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) @@ -297,9 +296,9 @@ object Find: * @return An `Option` with the first document matching the JSON containment query if found * @throws DocumentException If called on a SQLite connection */ - def firstByContains[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]], - conn: Connection)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Custom.single[TDoc](FindQuery.byContains(tableName) + QueryUtils.orderBy(orderBy.asJava), + 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) /** @@ -311,9 +310,8 @@ object Find: * @return An `Option` with the first document matching the JSON containment query if found * @throws DocumentException If called on a SQLite connection */ - def firstByContains[TDoc, TContains](tableName: String, criteria: TContains, conn: Connection) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - firstByContains[TDoc, TContains](tableName, criteria, List(), conn) + 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 @@ -325,23 +323,23 @@ object Find: * @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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]] = List()) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - Using(Configuration.dbConn()) { conn => firstByContains[TDoc, TContains](tableName, criteria, orderBy, conn) }.get + def firstByContains[Doc, A](tableName: String, criteria: A, orderBy: Seq[Field[?]] = List()) + (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 + * @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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]], conn: Connection) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - Custom.single[TDoc](FindQuery.byJsonPath(tableName) + QueryUtils.orderBy(orderBy.asJava), + 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) /** @@ -353,20 +351,19 @@ object Find: * @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[TDoc](tableName: String, path: String, conn: Connection) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - firstByJsonPath[TDoc](tableName, path, List(), conn) + 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) + * @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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - Using(Configuration.dbConn()) { conn => firstByJsonPath[TDoc](tableName, path, orderBy, conn) }.get + def firstByJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) + (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/solutions/bitbadger/documents/scala/Parameters.scala b/src/scala/src/main/scala/Parameters.scala similarity index 90% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Parameters.scala rename to src/scala/src/main/scala/Parameters.scala index 02b2d5d..2499863 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Parameters.scala +++ b/src/scala/src/main/scala/Parameters.scala @@ -38,13 +38,13 @@ object Parameters: * @param value The object to be encoded as JSON * @return A parameter with the value encoded */ - def json[T](name: String, value: T): Parameter[String] = + 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 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 */ @@ -56,7 +56,7 @@ object Parameters: /** * Replace the parameter names in the query with question marks * - * @param query The query with named placeholders + * @param query The query with named placeholders * @param parameters The parameters for the query * @return The query, with name parameters changed to `?`s */ @@ -66,8 +66,8 @@ object Parameters: /** * Apply the given parameters to the given query, returning a prepared statement * - * @param conn The active JDBC connection - * @param query The query + * @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 @@ -78,7 +78,7 @@ object Parameters: /** * Create parameters for field names to be removed from a document * - * @param names The names of the fields to be removed + * @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 diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Patch.scala b/src/scala/src/main/scala/Patch.scala similarity index 83% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Patch.scala rename to src/scala/src/main/scala/Patch.scala index b53bb83..e90e150 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Patch.scala +++ b/src/scala/src/main/scala/Patch.scala @@ -20,7 +20,7 @@ object Patch: * @param conn The connection on which the update should be executed * @throws DocumentException If no dialect has been configured */ - def byId[TKey, TPatch](tableName: String, docId: TKey, patch: TPatch, conn: Connection): Unit = + def byId[Key, Patch](tableName: String, docId: Key, patch: Patch, conn: Connection): Unit = CorePatch.byId(tableName, docId, patch, conn) /** @@ -31,7 +31,7 @@ object Patch: * @param patch The object whose properties should be replaced in the document * @throws DocumentException If no connection string has been set */ - def byId[TKey, TPatch](tableName: String, docId: TKey, patch: TPatch) = + def byId[Key, Patch](tableName: String, docId: Key, patch: Patch): Unit = CorePatch.byId(tableName, docId, patch) /** @@ -44,8 +44,8 @@ object Patch: * @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[TPatch](tableName: String, fields: Seq[Field[?]], patch: TPatch, howMatched: Option[FieldMatch], - conn: Connection): Unit = + 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) /** @@ -57,7 +57,7 @@ object Patch: * @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[TPatch](tableName: String, fields: Seq[Field[?]], patch: TPatch, conn: Connection): Unit = + def byFields[Patch](tableName: String, fields: Seq[Field[?]], patch: Patch, conn: Connection): Unit = byFields(tableName, fields, patch, None, conn) /** @@ -69,8 +69,8 @@ object Patch: * @param howMatched How the fields should be matched * @throws DocumentException If no connection string has been set, or if parameters are invalid */ - def byFields[TPatch](tableName: String, fields: Seq[Field[?]], patch: TPatch, - howMatched: Option[FieldMatch] = None): Unit = + def byFields[Patch](tableName: String, fields: Seq[Field[?]], patch: Patch, + howMatched: Option[FieldMatch] = None): Unit = CorePatch.byFields(tableName, fields.asJava, patch, howMatched.orNull) /** @@ -82,7 +82,7 @@ object Patch: * @param conn The connection on which the update should be executed * @throws DocumentException If called on a SQLite connection */ - def byContains[TContains, TPatch](tableName: String, criteria: TContains, patch: TPatch, conn: Connection): Unit = + def byContains[A, Patch](tableName: String, criteria: A, patch: Patch, conn: Connection): Unit = CorePatch.byContains(tableName, criteria, patch, conn) /** @@ -93,7 +93,7 @@ object Patch: * @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[TContains, TPatch](tableName: String, criteria: TContains, patch: TPatch): Unit = + def byContains[A, Patch](tableName: String, criteria: A, patch: Patch): Unit = CorePatch.byContains(tableName, criteria, patch) /** @@ -105,7 +105,7 @@ object Patch: * @param conn The connection on which the update should be executed * @throws DocumentException If called on a SQLite connection */ - def byJsonPath[TPatch](tableName: String, path: String, patch: TPatch, conn: Connection): Unit = + def byJsonPath[Patch](tableName: String, path: String, patch: Patch, conn: Connection): Unit = CorePatch.byJsonPath(tableName, path, patch, conn) /** @@ -116,5 +116,5 @@ object Patch: * @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[TPatch](tableName: String, path: String, patch: TPatch): Unit = + def byJsonPath[Patch](tableName: String, path: String, patch: Patch): Unit = CorePatch.byJsonPath(tableName, path, patch) diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/RemoveFields.scala b/src/scala/src/main/scala/RemoveFields.scala similarity index 93% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/RemoveFields.scala rename to src/scala/src/main/scala/RemoveFields.scala index 2701553..a605b7f 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/RemoveFields.scala +++ b/src/scala/src/main/scala/RemoveFields.scala @@ -20,7 +20,7 @@ object RemoveFields: * @param conn The connection on which the update should be executed * @throws DocumentException If no dialect has been configured */ - def byId[TKey](tableName: String, docId: TKey, toRemove: Seq[String], conn: Connection): Unit = + def byId[Key](tableName: String, docId: Key, toRemove: Seq[String], conn: Connection): Unit = CoreRemoveFields.byId(tableName, docId, toRemove.asJava, conn) /** @@ -31,7 +31,7 @@ object RemoveFields: * @param toRemove The names of the fields to be removed * @throws DocumentException If no connection string has been set */ - def byId[TKey](tableName: String, docId: TKey, toRemove: Seq[String]): Unit = + def byId[Key](tableName: String, docId: Key, toRemove: Seq[String]): Unit = CoreRemoveFields.byId(tableName, docId, toRemove.asJava) /** @@ -82,7 +82,7 @@ object RemoveFields: * @param conn The connection on which the update should be executed * @throws DocumentException If called on a SQLite connection */ - def byContains[TContains](tableName: String, criteria: TContains, toRemove: Seq[String], conn: Connection): Unit = + def byContains[A](tableName: String, criteria: A, toRemove: Seq[String], conn: Connection): Unit = CoreRemoveFields.byContains(tableName, criteria, toRemove.asJava, conn) /** @@ -93,7 +93,7 @@ object RemoveFields: * @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[TContains](tableName: String, criteria: TContains, toRemove: Seq[String]): Unit = + def byContains[A](tableName: String, criteria: A, toRemove: Seq[String]): Unit = CoreRemoveFields.byContains(tableName, criteria, toRemove.asJava) /** diff --git a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Results.scala b/src/scala/src/main/scala/Results.scala similarity index 82% rename from src/scala/src/main/scala/solutions/bitbadger/documents/scala/Results.scala rename to src/scala/src/main/scala/Results.scala index bab76e4..18de0c1 100644 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/Results.scala +++ b/src/scala/src/main/scala/Results.scala @@ -21,8 +21,8 @@ object Results: * @param ignored The class tag (placeholder used for signature; implicit tag used for serialization) * @return The constructed domain item */ - def fromDocument[TDoc](field: String, rs: ResultSet, ignored: ClassTag[TDoc])(implicit tag: ClassTag[TDoc]): TDoc = - CoreResults.fromDocument(field, rs, tag.runtimeClass.asInstanceOf[Class[TDoc]]) + 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 @@ -31,21 +31,21 @@ object Results: * @param ignored The class tag (placeholder used for signature; implicit tag used for serialization) * @return The constructed domain item */ - def fromData[TDoc](rs: ResultSet, ignored: ClassTag[TDoc])(implicit tag: ClassTag[TDoc]): TDoc = - fromDocument[TDoc]("data", rs, tag) + 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 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[TDoc](stmt: PreparedStatement, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = + def toCustomList[Doc](stmt: PreparedStatement,mapFunc: (ResultSet, ClassTag[Doc]) => Doc) + (using tag: ClassTag[Doc]): List[Doc] = try - val buffer = ListBuffer[TDoc]() + val buffer = ListBuffer[Doc]() Using(stmt.executeQuery()) { rs => while (rs.next()) { buffer.append(mapFunc(rs, tag)) 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..c7c4dd6 --- /dev/null +++ b/src/scala/src/main/scala/extensions/package.scala @@ -0,0 +1,497 @@ +package solutions.bitbadger.documents.scala.extensions + +import solutions.bitbadger.documents.{DocumentIndex, Field, FieldMatch, Parameter} +import solutions.bitbadger.documents.scala.* + +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 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 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[?]] = List()): 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 ~~~ + + /** + * 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[?]] = List())(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[?]] = List())(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[?]] = List()) + (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[?]] = List()) + (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[?]] = List())(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[?]] = List()) + (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[?]] = List()) + (using tag: ClassTag[Doc]): Option[Doc] = + Find.firstByJsonPath[Doc](tableName, 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/main/scala/solutions/bitbadger/documents/scala/extensions/package.scala b/src/scala/src/main/scala/solutions/bitbadger/documents/scala/extensions/package.scala deleted file mode 100644 index 142fd26..0000000 --- a/src/scala/src/main/scala/solutions/bitbadger/documents/scala/extensions/package.scala +++ /dev/null @@ -1,499 +0,0 @@ -package solutions.bitbadger.documents.scala - -import solutions.bitbadger.documents.{DocumentIndex, Field, FieldMatch, Parameter} - -import java.sql.{Connection, ResultSet} -import scala.reflect.ClassTag - -package object extensions: - - 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[TDoc](query: String, parameters: Seq[Parameter[?]], - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = - Custom.list[TDoc](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[TDoc](query: String, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): List[TDoc] = - Custom.list[TDoc](query, conn, 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[TDoc](query: String, parameters: Seq[Parameter[?]], - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Custom.single[TDoc](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[TDoc](query: String, - mapFunc: (ResultSet, ClassTag[TDoc]) => TDoc)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Custom.single[TDoc](query, conn, 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[?]] = List()): 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[T](query: String, parameters: Seq[Parameter[?]], - mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = - Custom.scalar[T](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[T](query: String, - mapFunc: (ResultSet, ClassTag[T]) => T)(implicit tag: ClassTag[T]): T = - Custom.scalar[T](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[TDoc](tableName: String, document: TDoc): 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[TDoc](tableName: String, document: TDoc): 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[TKey, TDoc](tableName: String, docId: TKey, document: TDoc): 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[TContains](tableName: String, criteria: TContains): 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[TKey](tableName: String, docId: TKey): 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[TContains](tableName: String, criteria: TContains): 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 ~~~ - - /** - * 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[TDoc](tableName: String, orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): List[TDoc] = - Find.all[TDoc](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[TKey, TDoc](tableName: String, docId: TKey)(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Find.byId[TKey, TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, - orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): List[TDoc] = - Find.byFields[TDoc](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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]] = List()) - (implicit tag: ClassTag[TDoc]): List[TDoc] = - Find.byContains[TDoc, TContains](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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) - (implicit tag: ClassTag[TDoc]): List[TDoc] = - Find.byJsonPath[TDoc](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[TDoc](tableName: String, fields: Seq[Field[?]], howMatched: Option[FieldMatch] = None, - orderBy: Seq[Field[?]] = List())(implicit tag: ClassTag[TDoc]): Option[TDoc] = - Find.firstByFields[TDoc](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[TDoc, TContains](tableName: String, criteria: TContains, orderBy: Seq[Field[?]] = List()) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - Find.firstByContains[TDoc, TContains](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[TDoc](tableName: String, path: String, orderBy: Seq[Field[?]] = List()) - (implicit tag: ClassTag[TDoc]): Option[TDoc] = - Find.firstByJsonPath[TDoc](tableName, 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[TKey, TPatch](tableName: String, docId: TKey, patch: TPatch): 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[TPatch](tableName: String, fields: Seq[Field[?]], patch: TPatch, - 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[TContains, TPatch](tableName: String, criteria: TContains, patch: TPatch): 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[TPatch](tableName: String, path: String, patch: TPatch): 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[TKey](tableName: String, docId: TKey, 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[TContains](tableName: String, criteria: TContains, 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[TKey](tableName: String, docId: TKey): 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[TContains](tableName: String, criteria: TContains): 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/solutions/bitbadger/documents/scala/tests/AutoIdTest.scala b/src/scala/src/test/scala/AutoIdTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/AutoIdTest.scala rename to src/scala/src/test/scala/AutoIdTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ByteIdClass.scala b/src/scala/src/test/scala/ByteIdClass.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ByteIdClass.scala rename to src/scala/src/test/scala/ByteIdClass.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ConfigurationTest.scala b/src/scala/src/test/scala/ConfigurationTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ConfigurationTest.scala rename to src/scala/src/test/scala/ConfigurationTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/CountQueryTest.scala b/src/scala/src/test/scala/CountQueryTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/CountQueryTest.scala rename to src/scala/src/test/scala/CountQueryTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DefinitionQueryTest.scala b/src/scala/src/test/scala/DefinitionQueryTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DefinitionQueryTest.scala rename to src/scala/src/test/scala/DefinitionQueryTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DeleteQueryTest.scala b/src/scala/src/test/scala/DeleteQueryTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DeleteQueryTest.scala rename to src/scala/src/test/scala/DeleteQueryTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DialectTest.scala b/src/scala/src/test/scala/DialectTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DialectTest.scala rename to src/scala/src/test/scala/DialectTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentIndexTest.scala b/src/scala/src/test/scala/DocumentIndexTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentIndexTest.scala rename to src/scala/src/test/scala/DocumentIndexTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentQueryTest.scala b/src/scala/src/test/scala/DocumentQueryTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/DocumentQueryTest.scala rename to src/scala/src/test/scala/DocumentQueryTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ExistsQueryTest.scala b/src/scala/src/test/scala/ExistsQueryTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ExistsQueryTest.scala rename to src/scala/src/test/scala/ExistsQueryTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldMatchTest.scala b/src/scala/src/test/scala/FieldMatchTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldMatchTest.scala rename to src/scala/src/test/scala/FieldMatchTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldTest.scala b/src/scala/src/test/scala/FieldTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FieldTest.scala rename to src/scala/src/test/scala/FieldTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FindQueryTest.scala b/src/scala/src/test/scala/FindQueryTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/FindQueryTest.scala rename to src/scala/src/test/scala/FindQueryTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ForceDialect.scala b/src/scala/src/test/scala/ForceDialect.scala similarity index 83% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ForceDialect.scala rename to src/scala/src/test/scala/ForceDialect.scala index 7665d26..d233bd9 100644 --- a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ForceDialect.scala +++ b/src/scala/src/test/scala/ForceDialect.scala @@ -7,11 +7,11 @@ import solutions.bitbadger.documents.Configuration */ object ForceDialect: - def postgres (): Unit = + def postgres(): Unit = Configuration.setConnectionString(":postgresql:") - def sqlite (): Unit = + def sqlite(): Unit = Configuration.setConnectionString(":sqlite:") - def none (): Unit = + def none(): Unit = Configuration.setConnectionString(null) diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/IntIdClass.scala b/src/scala/src/test/scala/IntIdClass.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/IntIdClass.scala rename to src/scala/src/test/scala/IntIdClass.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/LongIdClass.scala b/src/scala/src/test/scala/LongIdClass.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/LongIdClass.scala rename to src/scala/src/test/scala/LongIdClass.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/OpTest.scala b/src/scala/src/test/scala/OpTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/OpTest.scala rename to src/scala/src/test/scala/OpTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterNameTest.scala b/src/scala/src/test/scala/ParameterNameTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterNameTest.scala rename to src/scala/src/test/scala/ParameterNameTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterTest.scala b/src/scala/src/test/scala/ParameterTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParameterTest.scala rename to src/scala/src/test/scala/ParameterTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParametersTest.scala b/src/scala/src/test/scala/ParametersTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ParametersTest.scala rename to src/scala/src/test/scala/ParametersTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/PatchQueryTest.scala b/src/scala/src/test/scala/PatchQueryTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/PatchQueryTest.scala rename to src/scala/src/test/scala/PatchQueryTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/QueryUtilsTest.scala b/src/scala/src/test/scala/QueryUtilsTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/QueryUtilsTest.scala rename to src/scala/src/test/scala/QueryUtilsTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/RemoveFieldsQueryTest.scala b/src/scala/src/test/scala/RemoveFieldsQueryTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/RemoveFieldsQueryTest.scala rename to src/scala/src/test/scala/RemoveFieldsQueryTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ShortIdClass.scala b/src/scala/src/test/scala/ShortIdClass.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/ShortIdClass.scala rename to src/scala/src/test/scala/ShortIdClass.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/StringIdClass.scala b/src/scala/src/test/scala/StringIdClass.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/StringIdClass.scala rename to src/scala/src/test/scala/StringIdClass.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/WhereTest.scala b/src/scala/src/test/scala/WhereTest.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/WhereTest.scala rename to src/scala/src/test/scala/WhereTest.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ArrayDocument.scala b/src/scala/src/test/scala/integration/ArrayDocument.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ArrayDocument.scala rename to src/scala/src/test/scala/integration/ArrayDocument.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CountFunctions.scala b/src/scala/src/test/scala/integration/CountFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CountFunctions.scala rename to src/scala/src/test/scala/integration/CountFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CustomFunctions.scala b/src/scala/src/test/scala/integration/CustomFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/CustomFunctions.scala rename to src/scala/src/test/scala/integration/CustomFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DefinitionFunctions.scala b/src/scala/src/test/scala/integration/DefinitionFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DefinitionFunctions.scala rename to src/scala/src/test/scala/integration/DefinitionFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DeleteFunctions.scala b/src/scala/src/test/scala/integration/DeleteFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DeleteFunctions.scala rename to src/scala/src/test/scala/integration/DeleteFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DocumentFunctions.scala b/src/scala/src/test/scala/integration/DocumentFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/DocumentFunctions.scala rename to src/scala/src/test/scala/integration/DocumentFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ExistsFunctions.scala b/src/scala/src/test/scala/integration/ExistsFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ExistsFunctions.scala rename to src/scala/src/test/scala/integration/ExistsFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/FindFunctions.scala b/src/scala/src/test/scala/integration/FindFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/FindFunctions.scala rename to src/scala/src/test/scala/integration/FindFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JacksonDocumentSerializer.scala b/src/scala/src/test/scala/integration/JacksonDocumentSerializer.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JacksonDocumentSerializer.scala rename to src/scala/src/test/scala/integration/JacksonDocumentSerializer.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JsonDocument.scala b/src/scala/src/test/scala/integration/JsonDocument.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/JsonDocument.scala rename to src/scala/src/test/scala/integration/JsonDocument.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/NumIdDocument.scala b/src/scala/src/test/scala/integration/NumIdDocument.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/NumIdDocument.scala rename to src/scala/src/test/scala/integration/NumIdDocument.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PatchFunctions.scala b/src/scala/src/test/scala/integration/PatchFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PatchFunctions.scala rename to src/scala/src/test/scala/integration/PatchFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PgDB.scala b/src/scala/src/test/scala/integration/PgDB.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PgDB.scala rename to src/scala/src/test/scala/integration/PgDB.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCountIT.scala b/src/scala/src/test/scala/integration/PostgreSQLCountIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCountIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLCountIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCustomIT.scala b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLCustomIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDefinitionIT.scala b/src/scala/src/test/scala/integration/PostgreSQLDefinitionIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDefinitionIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLDefinitionIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDeleteIT.scala b/src/scala/src/test/scala/integration/PostgreSQLDeleteIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDeleteIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLDeleteIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDocumentIT.scala b/src/scala/src/test/scala/integration/PostgreSQLDocumentIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLDocumentIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLDocumentIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLExistsIT.scala b/src/scala/src/test/scala/integration/PostgreSQLExistsIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLExistsIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLExistsIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLFindIT.scala b/src/scala/src/test/scala/integration/PostgreSQLFindIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLFindIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLFindIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLPatchIT.scala b/src/scala/src/test/scala/integration/PostgreSQLPatchIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLPatchIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLPatchIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLRemoveFieldsIT.scala b/src/scala/src/test/scala/integration/PostgreSQLRemoveFieldsIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/PostgreSQLRemoveFieldsIT.scala rename to src/scala/src/test/scala/integration/PostgreSQLRemoveFieldsIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/RemoveFieldsFunctions.scala b/src/scala/src/test/scala/integration/RemoveFieldsFunctions.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/RemoveFieldsFunctions.scala rename to src/scala/src/test/scala/integration/RemoveFieldsFunctions.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCountIT.scala b/src/scala/src/test/scala/integration/SQLiteCountIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCountIT.scala rename to src/scala/src/test/scala/integration/SQLiteCountIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCustomIT.scala b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteCustomIT.scala rename to src/scala/src/test/scala/integration/SQLiteCustomIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDB.scala b/src/scala/src/test/scala/integration/SQLiteDB.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDB.scala rename to src/scala/src/test/scala/integration/SQLiteDB.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDefinitionIT.scala b/src/scala/src/test/scala/integration/SQLiteDefinitionIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDefinitionIT.scala rename to src/scala/src/test/scala/integration/SQLiteDefinitionIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDeleteIT.scala b/src/scala/src/test/scala/integration/SQLiteDeleteIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDeleteIT.scala rename to src/scala/src/test/scala/integration/SQLiteDeleteIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDocumentIT.scala b/src/scala/src/test/scala/integration/SQLiteDocumentIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteDocumentIT.scala rename to src/scala/src/test/scala/integration/SQLiteDocumentIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteExistsIT.scala b/src/scala/src/test/scala/integration/SQLiteExistsIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteExistsIT.scala rename to src/scala/src/test/scala/integration/SQLiteExistsIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteFindIT.scala b/src/scala/src/test/scala/integration/SQLiteFindIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteFindIT.scala rename to src/scala/src/test/scala/integration/SQLiteFindIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLitePatchIT.scala b/src/scala/src/test/scala/integration/SQLitePatchIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLitePatchIT.scala rename to src/scala/src/test/scala/integration/SQLitePatchIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteRemoveFieldsIT.scala b/src/scala/src/test/scala/integration/SQLiteRemoveFieldsIT.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SQLiteRemoveFieldsIT.scala rename to src/scala/src/test/scala/integration/SQLiteRemoveFieldsIT.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SubDocument.scala b/src/scala/src/test/scala/integration/SubDocument.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/SubDocument.scala rename to src/scala/src/test/scala/integration/SubDocument.scala diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ThrowawayDatabase.scala b/src/scala/src/test/scala/integration/ThrowawayDatabase.scala similarity index 100% rename from src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/integration/ThrowawayDatabase.scala rename to src/scala/src/test/scala/integration/ThrowawayDatabase.scala 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" diff --git a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/package.scala b/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/package.scala deleted file mode 100644 index 8992837..0000000 --- a/src/scala/src/test/scala/solutions/bitbadger/documents/scala/tests/package.scala +++ /dev/null @@ -1,6 +0,0 @@ -package solutions.bitbadger.documents.scala - -package object tests { - - def TEST_TABLE = "test_table" -} \ No newline at end of file -- 2.47.2 From 54b46ffedb1062864ddceafefb72bb4dca83896f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 25 Mar 2025 17:10:08 -0400 Subject: [PATCH 64/88] Update display name for tests --- src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt | 2 +- src/core/src/test/kotlin/integration/SQLiteCountIT.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt index 4fa7fb6..6e2690a 100644 --- a/src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt +++ b/src/core/src/test/kotlin/integration/PostgreSQLCountIT.kt @@ -6,7 +6,7 @@ import kotlin.test.Test /** * PostgreSQL integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("Java | Kotlin | PostgreSQL: Count") +@DisplayName("Core | Kotlin | PostgreSQL: Count") class PostgreSQLCountIT { @Test diff --git a/src/core/src/test/kotlin/integration/SQLiteCountIT.kt b/src/core/src/test/kotlin/integration/SQLiteCountIT.kt index 7e2aece..08575e4 100644 --- a/src/core/src/test/kotlin/integration/SQLiteCountIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteCountIT.kt @@ -8,7 +8,7 @@ import kotlin.test.Test /** * SQLite integration tests for the `Count` object / `count*` connection extension functions */ -@DisplayName("Java | Kotlin | SQLite: Count") +@DisplayName("Core | Kotlin | SQLite: Count") class SQLiteCountIT { @Test -- 2.47.2 From cf704ee182a2bfb526c43d8c54e10716ae330aa0 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 25 Mar 2025 22:17:27 -0400 Subject: [PATCH 65/88] Attach sources for all projects --- pom.xml | 2 ++ src/core/pom.xml | 31 ++++++++++++++++++++----------- src/groovy/pom.xml | 31 +++++++++++++++++++++++++++++++ src/kotlinx/pom.xml | 31 +++++++++++++++++++++++++++++++ src/scala/pom.xml | 31 +++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index cdb1d1f..9eeda2c 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,7 @@ official 17 ${java.version} + true 2.1.20 1.8.0 3.5.2 @@ -49,6 +50,7 @@ 2.18.2 3.46.1.2 42.7.5 + 3.6.0 3.3.1 3.11.2 diff --git a/src/core/pom.xml b/src/core/pom.xml index d54de26..e364db0 100644 --- a/src/core/pom.xml +++ b/src/core/pom.xml @@ -17,12 +17,6 @@ Expose a document store interface for PostgreSQL and SQLite (Core Library) https://bitbadger.solutions/open-source/relational-documents/jvm/ - - UTF-8 - official - 1.8 - - @@ -83,6 +77,24 @@ ${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 @@ -96,7 +108,7 @@ - + diff --git a/src/groovy/pom.xml b/src/groovy/pom.xml index 41e5ece..0289ee4 100644 --- a/src/groovy/pom.xml +++ b/src/groovy/pom.xml @@ -72,6 +72,37 @@ ${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 + + + + diff --git a/src/kotlinx/pom.xml b/src/kotlinx/pom.xml index 10eb8a8..f2974dc 100644 --- a/src/kotlinx/pom.xml +++ b/src/kotlinx/pom.xml @@ -108,6 +108,37 @@ ${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 + + + + diff --git a/src/scala/pom.xml b/src/scala/pom.xml index 7ed1b27..8dc88a8 100644 --- a/src/scala/pom.xml +++ b/src/scala/pom.xml @@ -63,6 +63,37 @@ ${java.version} + + + org.apache.maven.plugins + maven-source-plugin + ${sourcePlugin.version} + + + attach-sources + + jar-no-fork + + + + -- 2.47.2 From 7cb7abd0e117e6a4bedab352255cf085ef48ee62 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 26 Mar 2025 23:43:18 -0400 Subject: [PATCH 66/88] Gen JavaDoc for all but scala --- src/core/pom.xml | 18 +++++++------ src/core/src/main/kotlin/Configuration.kt | 4 +++ src/core/src/main/kotlin/java/Custom.kt | 3 +++ src/core/src/main/kotlin/java/Results.kt | 3 +++ src/core/src/main/module-info.md | 19 ++++++++++++++ src/groovy/pom.xml | 13 ++++++++++ src/groovy/src/main/java/module-info.java | 4 +++ .../documents/groovy/NoClassesHere.java | 7 ++++++ .../documents/groovy/package-info.java | 11 ++++++++ src/kotlinx/pom.xml | 17 +++++++++++++ src/kotlinx/src/main/module-info.md | 11 ++++++++ src/scala/pom.xml | 25 ++++++------------- 12 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 src/core/src/main/module-info.md 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/kotlinx/src/main/module-info.md diff --git a/src/core/pom.xml b/src/core/pom.xml index e364db0..79623a0 100644 --- a/src/core/pom.xml +++ b/src/core/pom.xml @@ -108,19 +108,23 @@ - + diff --git a/src/core/src/main/kotlin/Configuration.kt b/src/core/src/main/kotlin/Configuration.kt index d40f653..2aa3228 100644 --- a/src/core/src/main/kotlin/Configuration.kt +++ b/src/core/src/main/kotlin/Configuration.kt @@ -26,6 +26,10 @@ object Configuration { /** 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) diff --git a/src/core/src/main/kotlin/java/Custom.kt b/src/core/src/main/kotlin/java/Custom.kt index f341153..b90d963 100644 --- a/src/core/src/main/kotlin/java/Custom.kt +++ b/src/core/src/main/kotlin/java/Custom.kt @@ -9,6 +9,9 @@ import java.sql.SQLException import java.util.* import kotlin.jvm.Throws +/** + * Functions to run custom queries + */ object Custom { /** diff --git a/src/core/src/main/kotlin/java/Results.kt b/src/core/src/main/kotlin/java/Results.kt index d79b533..553329a 100644 --- a/src/core/src/main/kotlin/java/Results.kt +++ b/src/core/src/main/kotlin/java/Results.kt @@ -7,6 +7,9 @@ import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.SQLException +/** + * Functions to create results from queries + */ object Results { /** 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/groovy/pom.xml b/src/groovy/pom.xml index 0289ee4..ef52157 100644 --- a/src/groovy/pom.xml +++ b/src/groovy/pom.xml @@ -103,6 +103,19 @@ + + org.apache.maven.plugins + maven-javadoc-plugin + ${javaDocPlugin.version} + + + attach-javadocs + + jar + + + + diff --git a/src/groovy/src/main/java/module-info.java b/src/groovy/src/main/java/module-info.java index 4bba727..80da45b 100644 --- a/src/groovy/src/main/java/module-info.java +++ b/src/groovy/src/main/java/module-info.java @@ -1,3 +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/kotlinx/pom.xml b/src/kotlinx/pom.xml index f2974dc..d19c981 100644 --- a/src/kotlinx/pom.xml +++ b/src/kotlinx/pom.xml @@ -139,6 +139,23 @@ + + org.jetbrains.dokka + dokka-maven-plugin + 2.0.0 + + true + ${project.basedir}/src/main/module-info.md + + + + package + + javadocJar + + + + 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/scala/pom.xml b/src/scala/pom.xml index 8dc88a8..6a8aa83 100644 --- a/src/scala/pom.xml +++ b/src/scala/pom.xml @@ -32,6 +32,13 @@ testCompile + ${java.version} @@ -63,24 +70,6 @@ ${java.version} - org.apache.maven.plugins maven-source-plugin -- 2.47.2 From 3990b40c381579020b31b1bc1acb2458926b1f0d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 27 Mar 2025 13:56:40 -0400 Subject: [PATCH 67/88] Gen JavaDoc for Scala --- .idea/compiler.xml | 1 - .idea/modules.xml | 10 ------- .idea/solutions.bitbadger.documents.iml | 13 --------- documents.iml | 15 ---------- java.iml | 8 ------ src/scala/pom.xml | 37 +++++++++++++++++++++---- 6 files changed, 32 insertions(+), 52 deletions(-) delete mode 100644 .idea/modules.xml delete mode 100644 .idea/solutions.bitbadger.documents.iml delete mode 100644 documents.iml delete mode 100644 java.iml diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 0d99751..8788ef7 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -16,7 +16,6 @@ - diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index a3d569e..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/solutions.bitbadger.documents.iml b/.idea/solutions.bitbadger.documents.iml deleted file mode 100644 index cd2501b..0000000 --- a/.idea/solutions.bitbadger.documents.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/documents.iml b/documents.iml deleted file mode 100644 index 115f078..0000000 --- a/documents.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/java.iml b/java.iml deleted file mode 100644 index 0ae9cfd..0000000 --- a/java.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/scala/pom.xml b/src/scala/pom.xml index 6a8aa83..2ffcc5d 100644 --- a/src/scala/pom.xml +++ b/src/scala/pom.xml @@ -21,6 +21,19 @@ ${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 @@ -32,13 +45,27 @@ testCompile - + + -nobootcp + dotty.tools.scaladoc.Main + ${project.build.outputDirectory} + + **/*.tasty + + + + org.scala-lang + scaladoc_3 + ${scala.version} + + + + ${java.version} @@ -70,7 +97,7 @@ ${java.version} - + -- 2.47.2 From 18ba1be191c72d2f5de835952d188e3da1af7785 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 27 Mar 2025 20:26:51 -0400 Subject: [PATCH 68/88] Add JSON string custom functions --- src/core/src/main/kotlin/java/Custom.kt | 68 ++++++++++++ src/core/src/main/kotlin/java/Results.kt | 44 ++++++++ .../main/kotlin/java/extensions/Connection.kt | 34 ++++++ .../java/integration/CustomFunctions.java | 36 +++++++ .../tests/java/integration/JsonFunctions.java | 22 ++++ .../java/integration/PostgreSQLCustomIT.java | 40 +++++++ .../java/integration/SQLiteCustomIT.java | 40 +++++++ .../kotlin/integration/CustomFunctions.kt | 44 ++++++++ .../test/kotlin/integration/JsonFunctions.kt | 21 ++++ .../kotlin/integration/PostgreSQLCustomIT.kt | 25 +++++ .../test/kotlin/integration/SQLiteCustomIT.kt | 25 +++++ .../tests/integration/CustomFunctions.groovy | 35 ++++++ .../tests/integration/JsonFunctions.groovy | 20 ++++ .../integration/PostgreSQLCustomIT.groovy | 30 ++++++ src/kotlinx/src/main/kotlin/Custom.kt | 62 ++++++++++- src/kotlinx/src/main/kotlin/Results.kt | 31 ++++++ .../src/main/kotlin/extensions/Connection.kt | 48 ++++++--- .../kotlin/integration/CustomFunctions.kt | 44 ++++++++ .../test/kotlin/integration/JsonFunctions.kt | 20 ++++ .../kotlin/integration/PostgreSQLCustomIT.kt | 25 +++++ .../test/kotlin/integration/SQLiteCustomIT.kt | 25 +++++ src/scala/src/main/scala/Custom.scala | 101 +++++++++++++++++- src/scala/src/main/scala/Results.scala | 41 +++++++ .../src/main/scala/extensions/package.scala | 46 ++++++++ .../scala/integration/CustomFunctions.scala | 34 +++++- .../scala/integration/JsonFunctions.scala | 17 +++ .../integration/PostgreSQLCustomIT.scala | 25 +++++ .../scala/integration/SQLiteCustomIT.scala | 25 +++++ 28 files changed, 1012 insertions(+), 16 deletions(-) create mode 100644 src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonFunctions.java create mode 100644 src/core/src/test/kotlin/integration/JsonFunctions.kt create mode 100644 src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy create mode 100644 src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt create mode 100644 src/scala/src/test/scala/integration/JsonFunctions.scala diff --git a/src/core/src/main/kotlin/java/Custom.kt b/src/core/src/main/kotlin/java/Custom.kt index b90d963..b1fc6be 100644 --- a/src/core/src/main/kotlin/java/Custom.kt +++ b/src/core/src/main/kotlin/java/Custom.kt @@ -54,6 +54,39 @@ object Custom { 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 that returns one or no results * @@ -94,6 +127,41 @@ object Custom { 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 * diff --git a/src/core/src/main/kotlin/java/Results.kt b/src/core/src/main/kotlin/java/Results.kt index 553329a..66fc15c 100644 --- a/src/core/src/main/kotlin/java/Results.kt +++ b/src/core/src/main/kotlin/java/Results.kt @@ -89,4 +89,48 @@ object Results { 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) = + 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) + } } diff --git a/src/core/src/main/kotlin/java/extensions/Connection.kt b/src/core/src/main/kotlin/java/extensions/Connection.kt index f28a103..b23ef02 100644 --- a/src/core/src/main/kotlin/java/extensions/Connection.kt +++ b/src/core/src/main/kotlin/java/extensions/Connection.kt @@ -29,6 +29,23 @@ fun Connection.customList( ) = 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 that returns one or no results * @@ -48,6 +65,23 @@ fun Connection.customSingle( ) = 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 +) = + Configuration.dbConn().use { Custom.jsonSingle(query, parameters, it, mapFunc) } + /** * Execute a query that returns no results * 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 index 7c6ee1b..a2a4d55 100644 --- 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 @@ -13,6 +13,7 @@ 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 { @@ -31,6 +32,29 @@ final public class CustomFunctions { 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 singleNone(ThrowawayDatabase db) throws DocumentException { assertFalse( customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData) @@ -46,6 +70,18 @@ final public class CustomFunctions { "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, 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..0d440d2 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/JsonFunctions.java @@ -0,0 +1,22 @@ +package solutions.bitbadger.documents.core.tests.java.integration; + +import solutions.bitbadger.documents.Configuration; +import solutions.bitbadger.documents.Dialect; +import solutions.bitbadger.documents.DocumentException; + +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 { + if (Configuration.dialect() == Dialect.SQLITE) { + return json; + } + return json.replace("\":\"", "\": \"").replace("\",\"", "\", \"").replace("\":[", "\": ["); + } +} 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 index d2c2b01..a2d8f5c 100644 --- 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 @@ -27,6 +27,30 @@ final public class PostgreSQLCustomIT { } } + @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("single succeeds when document not found") public void singleNone() throws DocumentException { @@ -43,6 +67,22 @@ final public class PostgreSQLCustomIT { } } + @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 { 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 index 6398554..66bc246 100644 --- 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 @@ -27,6 +27,30 @@ final public class SQLiteCustomIT { } } + @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("single succeeds when document not found") public void singleNone() throws DocumentException { @@ -43,6 +67,22 @@ final public class SQLiteCustomIT { } } + @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 { diff --git a/src/core/src/test/kotlin/integration/CustomFunctions.kt b/src/core/src/test/kotlin/integration/CustomFunctions.kt index e25fff4..dafce1c 100644 --- a/src/core/src/test/kotlin/integration/CustomFunctions.kt +++ b/src/core/src/test/kotlin/integration/CustomFunctions.kt @@ -7,6 +7,7 @@ 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 kotlin.test.* /** @@ -29,6 +30,36 @@ object CustomFunctions { 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 singleNone(db: ThrowawayDatabase) = assertFalse( db.conn.customSingle( @@ -53,6 +84,19 @@ object CustomFunctions { ) } + 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( 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..4fe6bea --- /dev/null +++ b/src/core/src/test/kotlin/integration/JsonFunctions.kt @@ -0,0 +1,21 @@ +package solutions.bitbadger.documents.core.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect + +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("\",\"", "\", \"").replace("\":[", "\": [") + } + +} \ No newline at end of file diff --git a/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt index 8e943db..ca28dd7 100644 --- a/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt +++ b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -20,6 +20,21 @@ class PostgreSQLCustomIT { 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("single succeeds when document not found") fun singleNone() = @@ -30,6 +45,16 @@ class PostgreSQLCustomIT { 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() = diff --git a/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt index 470b040..9eca13a 100644 --- a/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -19,6 +19,21 @@ class SQLiteCustomIT { 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("single succeeds when document not found") fun singleNone() = @@ -29,6 +44,16 @@ class SQLiteCustomIT { 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() = 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 index 72208e0..36a5bc9 100644 --- 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 @@ -8,6 +8,7 @@ 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 @@ -27,6 +28,28 @@ final class CustomFunctions { 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 singleNone(ThrowawayDatabase db) { assertFalse(db.conn.customSingle(FindQuery.all(TEST_TABLE), List.of(), JsonDocument, Results.&fromData) .isPresent(), @@ -41,6 +64,18 @@ final class CustomFunctions { '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), 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..f657d5e --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/JsonFunctions.groovy @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents.groovy.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect + +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) { + Configuration.dialect() == Dialect.SQLITE + ? json + : json.replace('":"', '": "').replace('","', '", "').replace('":[', '": [') + } +} 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 index cd7570d..e052725 100644 --- 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 @@ -21,6 +21,24 @@ final class PostgreSQLCustomIT { 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('single succeeds when document not found') void singleNone() { @@ -33,6 +51,18 @@ final class PostgreSQLCustomIT { 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() { diff --git a/src/kotlinx/src/main/kotlin/Custom.kt b/src/kotlinx/src/main/kotlin/Custom.kt index c5d5e7e..3b9a4be 100644 --- a/src/kotlinx/src/main/kotlin/Custom.kt +++ b/src/kotlinx/src/main/kotlin/Custom.kt @@ -2,7 +2,7 @@ package solutions.bitbadger.documents.kotlinx import solutions.bitbadger.documents.* import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.java.Custom as JvmCustom +import solutions.bitbadger.documents.java.Custom as CoreCustom import java.sql.Connection import java.sql.ResultSet @@ -41,6 +41,35 @@ object Custom { 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 that returns one or no results * @@ -71,6 +100,35 @@ object Custom { 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 * @@ -79,7 +137,7 @@ object Custom { * @param parameters Parameters to use for the query */ fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) = - JvmCustom.nonQuery(query, parameters, conn) + CoreCustom.nonQuery(query, parameters, conn) /** * Execute a query that returns no results diff --git a/src/kotlinx/src/main/kotlin/Results.kt b/src/kotlinx/src/main/kotlin/Results.kt index d63a363..e4b04ae 100644 --- a/src/kotlinx/src/main/kotlin/Results.kt +++ b/src/kotlinx/src/main/kotlin/Results.kt @@ -3,6 +3,7 @@ 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 @@ -73,4 +74,34 @@ object Results { 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 index 784bb95..2b15a1b 100644 --- a/src/kotlinx/src/main/kotlin/extensions/Connection.kt +++ b/src/kotlinx/src/main/kotlin/extensions/Connection.kt @@ -19,6 +19,21 @@ 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, mapFunc) + /** * Execute a query that returns one or no results * @@ -31,6 +46,21 @@ 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, mapFunc) + /** * Execute a query that returns no results * @@ -239,8 +269,7 @@ inline fun Connection.findByFields( fields: Collection>, howMatched: FieldMatch? = null, orderBy: Collection>? = null -) = - Find.byFields(tableName, fields, howMatched, orderBy, this) +) = Find.byFields(tableName, fields, howMatched, orderBy, this) /** * Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL only) @@ -255,8 +284,7 @@ inline fun Connection.findByContains( tableName: String, criteria: TContains, orderBy: Collection>? = null -) = - Find.byContains(tableName, criteria, orderBy, this) +) = Find.byContains(tableName, criteria, orderBy, this) /** * Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only) @@ -271,8 +299,7 @@ inline fun Connection.findByJsonPath( tableName: String, path: String, orderBy: Collection>? = null -) = - Find.byJsonPath(tableName, path, orderBy, this) +) = Find.byJsonPath(tableName, path, orderBy, this) /** * Retrieve the first document using a field comparison and optional ordering fields @@ -288,8 +315,7 @@ inline fun Connection.findFirstByFields( fields: Collection>, howMatched: FieldMatch? = null, orderBy: Collection>? = null -) = - Find.firstByFields(tableName, fields, howMatched, orderBy, this) +) = Find.firstByFields(tableName, fields, howMatched, orderBy, this) /** * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) @@ -304,8 +330,7 @@ inline fun Connection.findFirstByContain tableName: String, criteria: TContains, orderBy: Collection>? = null -) = - Find.firstByContains(tableName, criteria, orderBy, this) +) = Find.firstByContains(tableName, criteria, orderBy, this) /** * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) @@ -320,8 +345,7 @@ inline fun Connection.findFirstByJsonPath( tableName: String, path: String, orderBy: Collection>? = null -) = - Find.firstByJsonPath(tableName, path, orderBy, this) +) = Find.firstByJsonPath(tableName, path, orderBy, this) // ~~~ DOCUMENT PATCH (PARTIAL UPDATE) QUERIES ~~~ diff --git a/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt index d7ababc..c32d52f 100644 --- a/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt +++ b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt @@ -3,6 +3,7 @@ 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.* @@ -28,6 +29,36 @@ object CustomFunctions { 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 singleNone(db: ThrowawayDatabase) = assertNull( db.conn.customSingle(FindQuery.all(TEST_TABLE), mapFunc = Results::fromData), @@ -42,6 +73,19 @@ object CustomFunctions { ) } + 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( 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..f877497 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents.kotlinx.tests.integration + +import solutions.bitbadger.documents.Configuration +import solutions.bitbadger.documents.Dialect + +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("\",\"", "\", \"").replace("\":[", "\": [") + } +} \ No newline at end of file diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt index e1ed60c..7407c71 100644 --- a/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -20,6 +20,21 @@ class PostgreSQLCustomIT { 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("single succeeds when document not found") fun singleNone() = @@ -30,6 +45,16 @@ class PostgreSQLCustomIT { 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() = diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt index ce06f4c..af2e850 100644 --- a/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -19,6 +19,21 @@ class SQLiteCustomIT { 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("single succeeds when document not found") fun singleNone() = @@ -29,6 +44,16 @@ class SQLiteCustomIT { 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() = diff --git a/src/scala/src/main/scala/Custom.scala b/src/scala/src/main/scala/Custom.scala index d946b33..110562f 100644 --- a/src/scala/src/main/scala/Custom.scala +++ b/src/scala/src/main/scala/Custom.scala @@ -33,7 +33,7 @@ object Custom: */ def list[Doc](query: String, conn: Connection, mapFunc: (ResultSet, ClassTag[Doc]) => Doc) (using tag: ClassTag[Doc]): List[Doc] = - list(query, List(), conn, mapFunc) + list(query, Nil, conn, mapFunc) /** * Execute a query that returns a list of results (creates connection) @@ -59,6 +59,54 @@ object Custom: 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 returns one or no results * @@ -110,6 +158,57 @@ object Custom: 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 * diff --git a/src/scala/src/main/scala/Results.scala b/src/scala/src/main/scala/Results.scala index 18de0c1..6845076 100644 --- a/src/scala/src/main/scala/Results.scala +++ b/src/scala/src/main/scala/Results.scala @@ -75,3 +75,44 @@ object Results: */ 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()) { + 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) diff --git a/src/scala/src/main/scala/extensions/package.scala b/src/scala/src/main/scala/extensions/package.scala index c7c4dd6..75275f6 100644 --- a/src/scala/src/main/scala/extensions/package.scala +++ b/src/scala/src/main/scala/extensions/package.scala @@ -35,6 +35,29 @@ extension (conn: Connection) (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 returns one or no results * @@ -60,6 +83,29 @@ extension (conn: Connection) (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 * diff --git a/src/scala/src/test/scala/integration/CustomFunctions.scala b/src/scala/src/test/scala/integration/CustomFunctions.scala index 0e6f992..32748fe 100644 --- a/src/scala/src/test/scala/integration/CustomFunctions.scala +++ b/src/scala/src/test/scala/integration/CustomFunctions.scala @@ -1,12 +1,14 @@ package solutions.bitbadger.documents.scala.tests.integration import org.junit.jupiter.api.Assertions.* -import solutions.bitbadger.documents.query.{CountQuery, DeleteQuery, FindQuery} +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 scala.jdk.CollectionConverters.* + object CustomFunctions: def listEmpty(db: ThrowawayDatabase): Unit = @@ -20,6 +22,26 @@ object CustomFunctions: 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 singleNone(db: ThrowawayDatabase): Unit = assertTrue(db.conn.customSingle[JsonDocument](FindQuery.all(TEST_TABLE), Results.fromData).isEmpty, "There should not have been a document returned") @@ -29,6 +51,16 @@ object CustomFunctions: 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), 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..a8cfc97 --- /dev/null +++ b/src/scala/src/test/scala/integration/JsonFunctions.scala @@ -0,0 +1,17 @@ +package solutions.bitbadger.documents.scala.tests.integration + +import solutions.bitbadger.documents.{Configuration, Dialect} + +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("\",\"", "\", \"").replace("\":[", "\": [") diff --git a/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala index 8ad437b..9c0c131 100644 --- a/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala +++ b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala @@ -17,6 +17,21 @@ class PostgreSQLCustomIT: 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("single succeeds when document not found") def singleNone(): Unit = @@ -27,6 +42,16 @@ class PostgreSQLCustomIT: 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 = diff --git a/src/scala/src/test/scala/integration/SQLiteCustomIT.scala b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala index f393a1e..dfbc0ca 100644 --- a/src/scala/src/test/scala/integration/SQLiteCustomIT.scala +++ b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala @@ -17,6 +17,21 @@ class SQLiteCustomIT: 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("single succeeds when document not found") def singleNone(): Unit = @@ -27,6 +42,16 @@ class SQLiteCustomIT: 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 = -- 2.47.2 From 7bba852f982399dcbcfa73b6cc8381ee71727a7c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 27 Mar 2025 23:10:18 -0400 Subject: [PATCH 69/88] WIP on core Json tests --- src/core/src/main/kotlin/java/Json.kt | 426 ++++++++++++++++++ src/core/src/main/kotlin/java/Results.kt | 2 +- .../main/kotlin/java/extensions/Connection.kt | 155 ++++++- src/core/src/test/kotlin/Types.kt | 15 + .../test/kotlin/integration/JsonFunctions.kt | 298 +++++++++++- .../kotlin/integration/PostgreSQLJsonIT.kt | 156 +++++++ .../test/kotlin/integration/SQLiteJsonIT.kt | 112 +++++ 7 files changed, 1144 insertions(+), 20 deletions(-) create mode 100644 src/core/src/main/kotlin/java/Json.kt create mode 100644 src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt create mode 100644 src/core/src/test/kotlin/integration/SQLiteJsonIT.kt 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..cebc83e --- /dev/null +++ b/src/core/src/main/kotlin/java/Json.kt @@ -0,0 +1,426 @@ +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 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 + * + * @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) + + /** + * 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 + * + * @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) } + + /** + * 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 + * + * @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) + + /** + * 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) + * + * @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) + + /** + * 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) + * + * @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) + + /** + * 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 + * + * @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) + + /** + * 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) + * + * @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) } + + /** + * 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) + * + * @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) } +} diff --git a/src/core/src/main/kotlin/java/Results.kt b/src/core/src/main/kotlin/java/Results.kt index 66fc15c..c71557c 100644 --- a/src/core/src/main/kotlin/java/Results.kt +++ b/src/core/src/main/kotlin/java/Results.kt @@ -120,7 +120,7 @@ object Results { * @throws DocumentException If there is a problem executing the query (unchecked) */ @JvmStatic - fun toJsonArray(stmt: PreparedStatement, mapFunc: (ResultSet) -> String) = + fun toJsonArray(stmt: PreparedStatement, mapFunc: (ResultSet) -> String): String = try { val results = StringBuilder("[") stmt.executeQuery().use { diff --git a/src/core/src/main/kotlin/java/extensions/Connection.kt b/src/core/src/main/kotlin/java/extensions/Connection.kt index b23ef02..f5c9ac1 100644 --- a/src/core/src/main/kotlin/java/extensions/Connection.kt +++ b/src/core/src/main/kotlin/java/extensions/Connection.kt @@ -287,7 +287,7 @@ fun Connection.existsByContains(tableName: String, criteria: TContai fun Connection.existsByJsonPath(tableName: String, path: String) = Exists.byJsonPath(tableName, path, this) -// ~~~ DOCUMENT RETRIEVAL QUERIES ~~~ +// ~~~ DOCUMENT RETRIEVAL QUERIES (Domain Objects) ~~~ /** * Retrieve all documents in the given table, ordering results by the optional given fields @@ -309,7 +309,7 @@ fun Connection.findAll(tableName: String, clazz: Class, orderBy: Co * @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 The document if it is found, `null` otherwise + * @return An `Optional` item with the document if it is found * @throws DocumentException If no dialect has been configured */ @Throws(DocumentException::class) @@ -335,8 +335,7 @@ fun Connection.findByFields( clazz: Class, howMatched: FieldMatch? = null, orderBy: Collection>? = null -) = - Find.byFields(tableName, fields, clazz, howMatched, orderBy, this) +) = Find.byFields(tableName, fields, clazz, howMatched, orderBy, this) /** * Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL only) @@ -355,8 +354,7 @@ fun Connection.findByContains( criteria: TContains, clazz: Class, orderBy: Collection>? = null -) = - Find.byContains(tableName, criteria, clazz, orderBy, this) +) = Find.byContains(tableName, criteria, clazz, orderBy, this) /** * Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only) @@ -375,8 +373,7 @@ fun Connection.findByJsonPath( path: String, clazz: Class, orderBy: Collection>? = null -) = - Find.byJsonPath(tableName, path, clazz, orderBy, this) +) = Find.byJsonPath(tableName, path, clazz, orderBy, this) /** * Retrieve the first document using a field comparison and optional ordering fields @@ -386,7 +383,7 @@ fun Connection.findByJsonPath( * @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 The first document matching the field comparison, or `null` if no matches are found + * @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) @@ -397,8 +394,7 @@ fun Connection.findFirstByFields( clazz: Class, howMatched: FieldMatch? = null, orderBy: Collection>? = null -) = - Find.firstByFields(tableName, fields, clazz, howMatched, orderBy, this) +) = Find.firstByFields(tableName, fields, clazz, howMatched, orderBy, this) /** * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) @@ -407,7 +403,7 @@ fun Connection.findFirstByFields( * @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 The first document matching the JSON containment query, or `null` if no matches are found + * @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) @@ -417,8 +413,7 @@ fun Connection.findFirstByContains( criteria: TContains, clazz: Class, orderBy: Collection>? = null -) = - Find.firstByContains(tableName, criteria, clazz, orderBy, this) +) = Find.firstByContains(tableName, criteria, clazz, orderBy, this) /** * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) @@ -427,7 +422,7 @@ fun Connection.findFirstByContains( * @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 The first document matching the JSON Path match query, or `null` if no matches are found + * @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) @@ -437,8 +432,134 @@ fun Connection.findFirstByJsonPath( path: String, clazz: Class, orderBy: Collection>? = null -) = - Find.firstByJsonPath(tableName, path, clazz, orderBy, this) +) = 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 PATCH (PARTIAL UPDATE) QUERIES ~~~ diff --git a/src/core/src/test/kotlin/Types.kt b/src/core/src/test/kotlin/Types.kt index cc222fa..e074eaf 100644 --- a/src/core/src/test/kotlin/Types.kt +++ b/src/core/src/test/kotlin/Types.kt @@ -44,5 +44,20 @@ data class JsonDocument(val id: String, val value: String = "", val numValue: In 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/integration/JsonFunctions.kt b/src/core/src/test/kotlin/integration/JsonFunctions.kt index 4fe6bea..8b27176 100644 --- a/src/core/src/test/kotlin/integration/JsonFunctions.kt +++ b/src/core/src/test/kotlin/integration/JsonFunctions.kt @@ -2,7 +2,25 @@ 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 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 { /** @@ -15,7 +33,283 @@ object JsonFunctions { fun maybeJsonB(json: String) = when (Configuration.dialect()) { Dialect.SQLITE -> json - Dialect.POSTGRESQL -> json.replace("\":\"", "\": \"").replace("\",\"", "\", \"").replace("\":[", "\": [") + Dialect.POSTGRESQL -> json.replace("\":", "\": ").replace(",\"", ", \"") } -} \ No newline at end of file + /** + * 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\"") + + fun allDefault(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonAll(TEST_TABLE) + 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.indexOf(docId("one")) >= 0, "Document 'one' not found in JSON ($json)") + assertTrue(json.indexOf(docId("two")) >= 0, "Document 'two' not found in JSON ($json)") + assertTrue(json.indexOf(docId("three")) >= 0, "Document 'three' not found in JSON ($json)") + assertTrue(json.indexOf(docId("four")) >= 0, "Document 'four' not found in JSON ($json)") + assertTrue(json.indexOf(docId("five")) >= 0, "Document 'five' not found in JSON ($json)") + } + } + } + + fun allEmpty(db: ThrowawayDatabase) = + assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") + + fun byIdString(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonById(TEST_TABLE, "two") + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "An incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue( + json.indexOf(docId("two")) >= 0, + "An incorrect document was returned ($json)" + ) + } + } + + fun byIdNumber(db: ThrowawayDatabase) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + assertEquals( + maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), db.conn.jsonById(TEST_TABLE, 18), + "The document should have been found by numeric ID" + ) + } finally { + Configuration.idField = "id" + } + } + + fun byIdNotFound(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByFields( + TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL + ) + when (Configuration.dialect()) { + Dialect.SQLITE -> assertEquals(JsonDocument.four, json, "The incorrect document was returned") + Dialect.POSTGRESQL -> assertTrue( + json.contains(docId("four")), + "The incorrect document was returned ($json)" + ) + } + } + + // TODO: stopped here with Postgres/SQLite split + + fun byFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + maybeJsonB("[${JsonDocument.five},${JsonDocument.four}]"), db.conn.jsonByFields( + TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + ), "The documents were not ordered correctly" + ) + } + + fun byFieldsMatchNumIn(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + maybeJsonB("[${JsonDocument.three}]"), db.conn.jsonByFields( + TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8))) + ), "The incorrect document was returned" + ) + } + + fun byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "[]", db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))), + "There should have been no documents returned" + ) + } + + fun byFieldsMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + assertTrue(json.contains(maybeJsonB("{\"id\":\"first\"")), "The 'first' document was not found ($json)") + assertTrue(json.contains(maybeJsonB("{\"id\":\"second\"")), "The 'second' document was not found ($json)") + } + + fun byFieldsNoMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals( + "[]", db.conn.jsonByFields( + TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) + ), "There should have been no documents returned" + ) + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "purple")) + assertTrue(json.contains(maybeJsonB(JsonDocument.four)), "Document 'four' not found ($json)") + assertTrue(json.contains(maybeJsonB(JsonDocument.five)), "Document 'five' not found ($json)") + } + + fun byContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + maybeJsonB("[${JsonDocument.two},${JsonDocument.four}]"), db.conn.jsonByContains( + TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) + ), "The documents were not ordered correctly" + ) + } + + fun byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "[]", + db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "indigo")), + "There should have been no documents returned" + ) + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + assertTrue(json.contains(maybeJsonB(JsonDocument.four)), "Document 'four' not found ($json)") + assertTrue(json.contains(maybeJsonB(JsonDocument.five)), "Document 'five' not found ($json)") + } + + fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + val fiveIdx = json.indexOf(docId("five")) + val fourIdx = json.indexOf(docId("four")) + 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)") + } + + fun byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "[]", + db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no documents returned" + ) + } + + fun firstByFieldsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) + assertTrue(json.contains(docId("two")), "The incorrect document was returned") + } + + fun firstByFieldsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) + assertTrue( + json.contains(maybeJsonB(JsonDocument.two)) || json.contains(maybeJsonB(JsonDocument.four)), + "Expected document 'two' or 'four' ($json)" + ) + } + + fun firstByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + maybeJsonB(JsonDocument.four), db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) + ), "An incorrect document was returned" + ) + } + + fun firstByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), + "There should have been no document returned" + ) + } + + fun firstByContainsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + maybeJsonB(JsonDocument.one), + db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!")), + "An incorrect document was returned" + ) + } + + fun firstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "purple")) + assertTrue( + json.contains(maybeJsonB(JsonDocument.four)) || json.contains(maybeJsonB(JsonDocument.five)), + "Expected document 'four' or 'five' ($json)" + ) + } + + fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + maybeJsonB(JsonDocument.five), db.conn.jsonFirstByContains( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ), "An incorrect document was returned" + ) + } + + fun firstByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "indigo")), + "There should have been no document returned" + ) + } + + fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + maybeJsonB(JsonDocument.two), + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)"), + "An incorrect document was returned" + ) + } + + fun firstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + assertTrue( + json.contains(maybeJsonB(JsonDocument.four)) || json.contains(maybeJsonB(JsonDocument.five)), + "Expected document 'four' or 'five' ($json)" + ) + } + + fun firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + maybeJsonB(JsonDocument.four), + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))), + "An incorrect document was returned" + ) + } + + fun firstByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no document returned" + ) + } +} 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..4e98051 --- /dev/null +++ b/src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt @@ -0,0 +1,156 @@ +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 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) +} 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..6f20601 --- /dev/null +++ b/src/core/src/test/kotlin/integration/SQLiteJsonIT.kt @@ -0,0 +1,112 @@ +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) } + } +} -- 2.47.2 From 27a6cdfa3b6b1a4668206b8a4957ca492e0697f7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 28 Mar 2025 12:15:34 -0400 Subject: [PATCH 70/88] Fix JSON tests by implementation --- .../test/kotlin/integration/JsonFunctions.kt | 221 ++++++++++++------ 1 file changed, 154 insertions(+), 67 deletions(-) diff --git a/src/core/src/test/kotlin/integration/JsonFunctions.kt b/src/core/src/test/kotlin/integration/JsonFunctions.kt index 8b27176..77de5b8 100644 --- a/src/core/src/test/kotlin/integration/JsonFunctions.kt +++ b/src/core/src/test/kotlin/integration/JsonFunctions.kt @@ -48,6 +48,7 @@ object JsonFunctions { fun allDefault(db: ThrowawayDatabase) { JsonDocument.load(db) val json = db.conn.jsonAll(TEST_TABLE) + 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)") @@ -64,6 +65,7 @@ object JsonFunctions { assertTrue(json.indexOf(docId("five")) >= 0, "Document 'five' not found in JSON ($json)") } } + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } fun allEmpty(db: ThrowawayDatabase) = @@ -105,32 +107,48 @@ object JsonFunctions { TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL ) when (Configuration.dialect()) { - Dialect.SQLITE -> assertEquals(JsonDocument.four, json, "The incorrect document was returned") - Dialect.POSTGRESQL -> assertTrue( - json.contains(docId("four")), - "The incorrect document was returned ($json)" - ) + 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)") + } } } - // TODO: stopped here with Postgres/SQLite split - fun byFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - maybeJsonB("[${JsonDocument.five},${JsonDocument.four}]"), db.conn.jsonByFields( - TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) - ), "The documents were not ordered correctly" + val json = db.conn.jsonByFields( + TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) ) + 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 byFieldsMatchNumIn(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - maybeJsonB("[${JsonDocument.three}]"), db.conn.jsonByFields( - TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8))) - ), "The incorrect document was returned" - ) + val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + 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 byFieldsNoMatch(db: ThrowawayDatabase) { @@ -144,8 +162,10 @@ object JsonFunctions { fun byFieldsMatchInArray(db: ThrowawayDatabase) { ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) - assertTrue(json.contains(maybeJsonB("{\"id\":\"first\"")), "The 'first' document was not found ($json)") - assertTrue(json.contains(maybeJsonB("{\"id\":\"second\"")), "The 'second' document was not found ($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)") } fun byFieldsNoMatchInArray(db: ThrowawayDatabase) { @@ -160,17 +180,39 @@ object JsonFunctions { fun byContainsMatch(db: ThrowawayDatabase) { JsonDocument.load(db) val json = db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "purple")) - assertTrue(json.contains(maybeJsonB(JsonDocument.four)), "Document 'four' not found ($json)") - assertTrue(json.contains(maybeJsonB(JsonDocument.five)), "Document 'five' not found ($json)") + 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 byContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - maybeJsonB("[${JsonDocument.two},${JsonDocument.four}]"), db.conn.jsonByContains( - TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) - ), "The documents were not ordered correctly" + val json = db.conn.jsonByContains( + TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) ) + 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 byContainsNoMatch(db: ThrowawayDatabase) { @@ -185,18 +227,38 @@ object JsonFunctions { fun byJsonPathMatch(db: ThrowawayDatabase) { JsonDocument.load(db) val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") - assertTrue(json.contains(maybeJsonB(JsonDocument.four)), "Document 'four' not found ($json)") - assertTrue(json.contains(maybeJsonB(JsonDocument.five)), "Document 'five' not found ($json)") + 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 byJsonPathMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) - val fiveIdx = json.indexOf(docId("five")) - val fourIdx = json.indexOf(docId("four")) - 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)") + 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 byJsonPathNoMatch(db: ThrowawayDatabase) { @@ -211,25 +273,36 @@ object JsonFunctions { fun firstByFieldsMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) - assertTrue(json.contains(docId("two")), "The incorrect document was returned") + 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 firstByFieldsMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) - assertTrue( - json.contains(maybeJsonB(JsonDocument.two)) || json.contains(maybeJsonB(JsonDocument.four)), - "Expected document 'two' or 'four' ($json)" - ) + 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 firstByFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - maybeJsonB(JsonDocument.four), db.conn.jsonFirstByFields( - TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) - ), "An incorrect document was returned" + val json = db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) ) + 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 firstByFieldsNoMatch(db: ThrowawayDatabase) { @@ -243,29 +316,37 @@ object JsonFunctions { fun firstByContainsMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - maybeJsonB(JsonDocument.one), - db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!")), - "An incorrect document was returned" - ) + val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!")) + 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 firstByContainsMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "purple")) - assertTrue( - json.contains(maybeJsonB(JsonDocument.four)) || json.contains(maybeJsonB(JsonDocument.five)), - "Expected document 'four' or 'five' ($json)" - ) + 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 firstByContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - maybeJsonB(JsonDocument.five), db.conn.jsonFirstByContains( - TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) - ), "An incorrect document was returned" + val json = db.conn.jsonFirstByContains( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) ) + 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 firstByContainsNoMatch(db: ThrowawayDatabase) { @@ -279,29 +360,35 @@ object JsonFunctions { fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - maybeJsonB(JsonDocument.two), - db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)"), - "An incorrect document was returned" - ) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + 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 firstByJsonPathMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") - assertTrue( - json.contains(maybeJsonB(JsonDocument.four)) || json.contains(maybeJsonB(JsonDocument.five)), - "Expected document 'four' or 'five' ($json)" - ) + 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 firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - maybeJsonB(JsonDocument.four), - db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))), - "An incorrect document was returned" - ) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + 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 firstByJsonPathNoMatch(db: ThrowawayDatabase) { -- 2.47.2 From 2e87ae4b1fe3c0a5268743c772c91aa3f0a8d571 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 28 Mar 2025 13:26:38 -0400 Subject: [PATCH 71/88] Add Java Json ITs --- .../tests/java/integration/JsonDocument.java | 17 + .../tests/java/integration/JsonFunctions.java | 416 +++++++++++++++++- .../tests/java/integration/NumIdDocument.java | 14 +- .../java/integration/PostgreSQLJsonIT.java | 245 +++++++++++ .../tests/java/integration/SQLiteJsonIT.java | 167 +++++++ .../test/kotlin/integration/JsonFunctions.kt | 15 +- 6 files changed, 851 insertions(+), 23 deletions(-) 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/SQLiteJsonIT.java 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 index cb85902..0745d92 100644 --- 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 @@ -87,4 +87,21 @@ public class JsonDocument { 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 index 0d440d2..646e386 100644 --- 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 @@ -1,10 +1,25 @@ package solutions.bitbadger.documents.core.tests.java.integration; -import solutions.bitbadger.documents.Configuration; -import solutions.bitbadger.documents.Dialect; -import solutions.bitbadger.documents.DocumentException; +import solutions.bitbadger.documents.*; +import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase; -public class JsonFunctions { +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. @@ -14,9 +29,396 @@ public class JsonFunctions { * @return The actual expected JSON based on the database being tested */ public static String maybeJsonB(String json) throws DocumentException { - if (Configuration.dialect() == Dialect.SQLITE) { - return json; + 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)); + } + + public static void allDefault(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonAll(db.getConn(), TEST_TABLE); + 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; } - return json.replace("\":\"", "\": \"").replace("\",\"", "\", \"").replace("\":[", "\": ["); + assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)"); + } + + public static void allEmpty(ThrowawayDatabase db) throws DocumentException { + assertEquals("[]", jsonAll(db.getConn(), TEST_TABLE), "There should have been no documents returned"); + } + + public static void byIdString(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonById(db.getConn(), TEST_TABLE, "two"); + 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 byIdNumber(ThrowawayDatabase db) throws DocumentException { + Configuration.idField = "key"; + try { + insert(db.getConn(), TEST_TABLE, new NumIdDocument(18, "howdy")); + assertEquals(maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), jsonById(db.getConn(), TEST_TABLE, 18), + "The document should have been found by numeric ID"); + } finally { + Configuration.idField = "id"; + } + } + + public static void byIdNotFound(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals("{}", jsonById(db.getConn(), TEST_TABLE, "x"), "There should have been no document returned"); + } + + public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonByFields(db.getConn(), TEST_TABLE, + List.of(Field.any("value", List.of("blue", "purple")), Field.exists("sub")), FieldMatch.ALL); + 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 byFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "purple")), null, + List.of(Field.named("id"))); + 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 byFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.any("numValue", List.of(2, 4, 6, 8)))); + 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 byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals("[]", jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.greater("numValue", 100))), + "There should have been no documents returned"); + } + + public static void byFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + final String json = jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE, + List.of("c")))); + 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 byFieldsNoMatchInArray(ThrowawayDatabase db) throws DocumentException { + for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } + assertEquals("[]", + jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE, List.of("j")))), + "There should have been no documents returned"); + } + + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")); + 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 byContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonByContains(db.getConn(), TEST_TABLE, Map.of("sub", Map.of("foo", "green")), + List.of(Field.named("value"))); + 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 byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals("[]", jsonByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo")), + "There should have been no documents returned"); + } + + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)"); + 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 byJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + List.of(Field.named("id"))); + 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 byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals("[]", jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no documents returned"); + } + + public static void firstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "another"))); + 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 firstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("sub.foo", "green"))); + 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 firstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("sub.foo", "green")), null, + List.of(Field.named("n:numValue DESC"))); + 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 firstByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals("{}", jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "absent"))), + "There should have been no document returned"); + } + + public static void firstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "FIRST!")); + 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 firstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")); + 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 firstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"), + List.of(Field.named("sub.bar NULLS FIRST"))); + 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 firstByContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals("{}", jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo")), + "There should have been no document returned"); + } + + public static void firstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10)"); + 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 firstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)"); + 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 firstByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + final String json = jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + List.of(Field.named("id DESC"))); + 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 firstByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + assertEquals("{}", jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no document returned"); } } 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 index f980c09..3d4ea66 100644 --- 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 @@ -3,7 +3,7 @@ package solutions.bitbadger.documents.core.tests.java.integration; public class NumIdDocument { private int key; - private String value; + private String text; public int getKey() { return key; @@ -13,17 +13,17 @@ public class NumIdDocument { this.key = key; } - public String getValue() { - return value; + public String getText() { + return text; } - public void setValue(String value) { - this.value = value; + public void setText(String text) { + this.text = text; } - public NumIdDocument(int key, String value) { + public NumIdDocument(int key, String text) { this.key = key; - this.value = value; + this.text = text; } public NumIdDocument() { 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..c8129a5 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLJsonIT.java @@ -0,0 +1,245 @@ +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); + } + } +} 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..1b3d447 --- /dev/null +++ b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteJsonIT.java @@ -0,0 +1,167 @@ +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)); + } + } +} diff --git a/src/core/src/test/kotlin/integration/JsonFunctions.kt b/src/core/src/test/kotlin/integration/JsonFunctions.kt index 77de5b8..94b23d4 100644 --- a/src/core/src/test/kotlin/integration/JsonFunctions.kt +++ b/src/core/src/test/kotlin/integration/JsonFunctions.kt @@ -58,11 +58,11 @@ object JsonFunctions { assertTrue(json.contains(JsonDocument.five), "Document 'five' not found in JSON ($json)") } Dialect.POSTGRESQL -> { - assertTrue(json.indexOf(docId("one")) >= 0, "Document 'one' not found in JSON ($json)") - assertTrue(json.indexOf(docId("two")) >= 0, "Document 'two' not found in JSON ($json)") - assertTrue(json.indexOf(docId("three")) >= 0, "Document 'three' not found in JSON ($json)") - assertTrue(json.indexOf(docId("four")) >= 0, "Document 'four' not found in JSON ($json)") - assertTrue(json.indexOf(docId("five")) >= 0, "Document 'five' not found in JSON ($json)") + 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)") @@ -76,10 +76,7 @@ object JsonFunctions { val json = db.conn.jsonById(TEST_TABLE, "two") when (Configuration.dialect()) { Dialect.SQLITE -> assertEquals(JsonDocument.two, json, "An incorrect document was returned") - Dialect.POSTGRESQL -> assertTrue( - json.indexOf(docId("two")) >= 0, - "An incorrect document was returned ($json)" - ) + Dialect.POSTGRESQL -> assertTrue(json.contains(docId("two")), "An incorrect document was returned ($json)") } } -- 2.47.2 From 61b5381c73cea1c9ac6646452b8bdd3ee6495f3f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 28 Mar 2025 22:16:37 -0400 Subject: [PATCH 72/88] WIP on Groovy Json ITs --- .../tests/integration/JsonDocument.groovy | 15 + .../tests/integration/JsonFunctions.groovy | 382 +++++++++++++++++- 2 files changed, 394 insertions(+), 3 deletions(-) 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 index 71594e8..db6c1bb 100644 --- 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 @@ -25,4 +25,19 @@ class JsonDocument { 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 index f657d5e..992c403 100644 --- 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 @@ -3,6 +3,18 @@ package solutions.bitbadger.documents.groovy.tests.integration import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect +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 { /** @@ -13,8 +25,372 @@ final class JsonFunctions { * @return The actual expected JSON based on the database being tested */ static String maybeJsonB(String json) { - Configuration.dialect() == Dialect.SQLITE - ? json - : json.replace('":"', '": "').replace('","', '", "').replace('":[', '": [') + 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\"") + } + + static void allDefault(ThrowawayDatabase db) { + JsonDocument.load(db) + def json = db.conn.jsonAll(TEST_TABLE) + 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 allEmpty(ThrowawayDatabase db) = + assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") + + static void byIdString(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonById(TEST_TABLE, "two") + 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)") + } + } + + static void byIdNumber(ThrowawayDatabase db) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + assertEquals( + maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), db.conn.jsonById(TEST_TABLE, 18), + "The document should have been found by numeric ID" + ) + } finally { + Configuration.idField = "id" + } + } + + static void byIdNotFound(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + } + + static void byFieldsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByFields( + TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL + ) + 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)") + } + } + } + + static void byFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByFields( + TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + ) + 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)") + } + } + } + + static void byFieldsMatchNumIn(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + 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)") + } + } + } + + static void byFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "[]", db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))), + "There should have been no documents returned" + ) + } + + static void byFieldsMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + 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 byFieldsNoMatchInArray(ThrowawayDatabase db) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals( + "[]", db.conn.jsonByFields( + TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) + ), "There should have been no documents returned" + ) + } + + static void byContainsMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "purple")) + 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)") + } + + static void byContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByContains( + TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) + ) + 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)") + } + } + } + + static void byContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "[]", + db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "indigo")), + "There should have been no documents returned" + ) + } + + static void byJsonPathMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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)") + } + + static void byJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + 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)") + } + } + } + + static void byJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "[]", + db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no documents returned" + ) + } + + static void firstByFieldsMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) + 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)") + } + } + + static void firstByFieldsMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) + 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)" + ) + } + } + + static void firstByFieldsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) + ) + 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)") + } + } + + static void firstByFieldsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), + "There should have been no document returned" + ) + } + + static void firstByContainsMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!")) + 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)") + } + } + + static void firstByContainsMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "purple")) + 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)" + ) + } + } + + static void firstByContainsMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) + 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)") + } + } + + static void firstByContainsNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "indigo")), + "There should have been no document returned" + ) + } + + static void firstByJsonPathMatchOne(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + 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)") + } + } + + static void firstByJsonPathMatchMany(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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)" + ) + } + } + + static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + 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)") + } + } + + static void firstByJsonPathNoMatch(ThrowawayDatabase db) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no document returned" + ) + } + } -- 2.47.2 From 4e6cfa35a7fa8f8818f12919fed2b232939ea62c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 28 Mar 2025 23:11:19 -0400 Subject: [PATCH 73/88] Finish Groovy Json ITs --- .../tests/integration/JsonFunctions.groovy | 402 +++++++++--------- .../tests/integration/NumIdDocument.groovy | 6 +- .../tests/integration/PostgreSQLJsonIT.groovy | 185 ++++++++ .../tests/integration/SQLiteJsonIT.groovy | 136 ++++++ 4 files changed, 529 insertions(+), 200 deletions(-) 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/SQLiteJsonIT.groovy 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 index 992c403..56c8b03 100644 --- 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 @@ -2,6 +2,8 @@ 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 @@ -43,8 +45,8 @@ final class JsonFunctions { static void allDefault(ThrowawayDatabase db) { JsonDocument.load(db) - def json = db.conn.jsonAll(TEST_TABLE) - assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") + String json = db.conn.jsonAll(TEST_TABLE) + 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)") @@ -54,343 +56,349 @@ final class JsonFunctions { 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)") + 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)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") } - static void allEmpty(ThrowawayDatabase db) = - assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") + static void allEmpty(ThrowawayDatabase db) { + assertEquals('[]', db.conn.jsonAll(TEST_TABLE), 'There should have been no documents returned') + } static void byIdString(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonById(TEST_TABLE, "two") - 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)") + String json = db.conn.jsonById(TEST_TABLE, 'two') + 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 byIdNumber(ThrowawayDatabase db) { - Configuration.idField = "key" + Configuration.idField = 'key' try { - db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) - assertEquals( - maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), db.conn.jsonById(TEST_TABLE, 18), - "The document should have been found by numeric ID" - ) + db.conn.insert(TEST_TABLE, new NumIdDocument(18, 'howdy')) + assertEquals(maybeJsonB('{"key":18,"text":"howdy"}'), db.conn.jsonById(TEST_TABLE, 18), + 'The document should have been found by numeric ID') } finally { - Configuration.idField = "id" + Configuration.idField = 'id' } } static void byIdNotFound(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + assertEquals('{}', db.conn.jsonById(TEST_TABLE, 'x'), 'There should have been no document returned') } static void byFieldsMatch(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonByFields( - TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL - ) - 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)") - } + String json = db.conn.jsonByFields(TEST_TABLE, List.of(Field.any('value', List.of('blue', 'purple')), + Field.exists('sub')), FieldMatch.ALL) + 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 byFieldsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonByFields( - TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) - ) - 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)") + String json = db.conn.jsonByFields(TEST_TABLE, List.of(Field.equal('value', 'purple')), null, + List.of(Field.named('id'))) + 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)") - } + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + break } } static void byFieldsMatchNumIn(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) - 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)") - } + String json = db.conn.jsonByFields(TEST_TABLE, List.of(Field.any('numValue', List.of(2, 4, 6, 8)))) + 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 byFieldsNoMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals( - "[]", db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))), - "There should have been no documents returned" - ) + assertEquals('[]', db.conn.jsonByFields(TEST_TABLE, List.of(Field.greater('numValue', 100))), + 'There should have been no documents returned') } static void byFieldsMatchInArray(ThrowawayDatabase db) { ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } - val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) - 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)") + String json = db.conn.jsonByFields(TEST_TABLE, List.of(Field.inArray('values', TEST_TABLE, List.of('c')))) + 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 byFieldsNoMatchInArray(ThrowawayDatabase db) { ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } - assertEquals( - "[]", db.conn.jsonByFields( - TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) - ), "There should have been no documents returned" - ) + assertEquals('[]', db.conn.jsonByFields(TEST_TABLE, List.of(Field.inArray('values', TEST_TABLE, List.of('j')))), + 'There should have been no documents returned') } static void byContainsMatch(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "purple")) - assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") - when (Configuration.dialect()) { - Dialect.SQLITE -> { + String json = db.conn.jsonByContains(TEST_TABLE, Map.of('value', 'purple')) + 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)") - } - Dialect.POSTGRESQL -> { - assertTrue(json.contains(docId("four")), "Document 'four' not found ($json)") - assertTrue(json.contains(docId("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)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") } static void byContainsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonByContains( - TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) - ) - 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)") + String json = db.conn.jsonByContains(TEST_TABLE, Map.of('sub', Map.of('foo', 'green')), + List.of(Field.named('value'))) + 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)") - } + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + break } } static void byContainsNoMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals( - "[]", - db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "indigo")), - "There should have been no documents returned" - ) + assertEquals('[]', db.conn.jsonByContains(TEST_TABLE, Map.of('value', 'indigo')), + 'There should have been no documents returned') } static void byJsonPathMatch(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") - assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") - when (Configuration.dialect()) { - Dialect.SQLITE -> { + String json = db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)') + 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)") - } - Dialect.POSTGRESQL -> { - assertTrue(json.contains(docId("four")), "Document 'four' not found ($json)") - assertTrue(json.contains(docId("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)") + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") } static void byJsonPathMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) - 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)") + String json = db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)', List.of(Field.named('id'))) + 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)") - } + assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") + break } } static void byJsonPathNoMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals( - "[]", - db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no documents returned" - ) + assertEquals('[]', db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)'), + 'There should have been no documents returned') } static void firstByFieldsMatchOne(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) - 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)") + String json = db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('value', 'another'))) + 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 firstByFieldsMatchMany(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) - 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)" - ) + String json = db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('sub.foo', 'green'))) + 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 firstByFieldsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields( - TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) - ) - 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)") + String json = db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('sub.foo', 'green')), null, + List.of(Field.named('n:numValue DESC'))) + 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 firstByFieldsNoMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals( - "{}", - db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), - "There should have been no document returned" - ) + assertEquals('{}', db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('value', 'absent'))), + 'There should have been no document returned') } static void firstByContainsMatchOne(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!")) - 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)") + String json = db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'FIRST!')) + 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 firstByContainsMatchMany(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "purple")) - 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)" - ) + String json = db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'purple')) + 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 firstByContainsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains( - TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) - ) - 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)") + String json = db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'purple'), + List.of(Field.named('sub.bar NULLS FIRST'))) + 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 firstByContainsNoMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals( - "{}", - db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "indigo")), - "There should have been no document returned" - ) + assertEquals('{}', db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'indigo')), + 'There should have been no document returned') } static void firstByJsonPathMatchOne(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") - 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)") + String json = db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ == 10)') + 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 firstByJsonPathMatchMany(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") - 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)" - ) + String json = db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)') + 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 firstByJsonPathMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) - 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)") + String json = db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)', List.of(Field.named('id DESC'))) + 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 firstByJsonPathNoMatch(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals( - "{}", - db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no document returned" - ) + assertEquals('{}', db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)'), + 'There should have been no document returned') } - } 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 index a980ef2..e4439a5 100644 --- 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 @@ -2,10 +2,10 @@ package solutions.bitbadger.documents.groovy.tests.integration class NumIdDocument { int key - String value + String text - NumIdDocument(int key = 0, String value = "") { + NumIdDocument(int key = 0, String text = "") { this.key = key - this.value = value + this.text = text } } 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..d0b61fa --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLJsonIT.groovy @@ -0,0 +1,185 @@ +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 + } +} 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..222bb86 --- /dev/null +++ b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteJsonIT.groovy @@ -0,0 +1,136 @@ +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 } + } + } +} -- 2.47.2 From 53dd31b91324b9abdc0ad57dba1cdd173fc157c0 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 29 Mar 2025 17:08:12 -0400 Subject: [PATCH 74/88] Add Scala Json wrapper --- src/core/src/main/kotlin/java/Json.kt | 20 +- src/scala/src/main/scala/Find.scala | 16 +- src/scala/src/main/scala/Json.scala | 341 ++++++++++++++++++ .../src/main/scala/extensions/package.scala | 118 +++++- 4 files changed, 470 insertions(+), 25 deletions(-) create mode 100644 src/scala/src/main/scala/Json.scala diff --git a/src/core/src/main/kotlin/java/Json.kt b/src/core/src/main/kotlin/java/Json.kt index cebc83e..a5b4c1e 100644 --- a/src/core/src/main/kotlin/java/Json.kt +++ b/src/core/src/main/kotlin/java/Json.kt @@ -26,7 +26,7 @@ object Json { Custom.jsonArray(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), conn, Results::jsonFromData) /** - * Retrieve all documents in the given table + * 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) @@ -72,7 +72,7 @@ object Json { ) /** - * Retrieve a document by its ID + * 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 @@ -114,7 +114,7 @@ object Json { } /** - * Retrieve documents using a field comparison, ordering results by the given fields + * 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 @@ -177,7 +177,8 @@ object Json { ) /** - * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) + * 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 @@ -226,7 +227,8 @@ object Json { ) /** - * Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only) + * 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 @@ -284,7 +286,7 @@ object Json { } /** - * Retrieve the first document using a field comparison and optional ordering fields + * 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 @@ -361,7 +363,8 @@ object Json { firstByContains(tableName, criteria, null, conn) /** - * Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only) + * 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 @@ -410,7 +413,8 @@ object Json { firstByJsonPath(tableName, path, null, conn) /** - * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only) + * 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 diff --git a/src/scala/src/main/scala/Find.scala b/src/scala/src/main/scala/Find.scala index 4335be7..8f6a2fc 100644 --- a/src/scala/src/main/scala/Find.scala +++ b/src/scala/src/main/scala/Find.scala @@ -44,7 +44,7 @@ object Find: * @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[?]] = List())(using tag: ClassTag[Doc]): List[Doc] = + 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 /** @@ -61,7 +61,7 @@ object Find: Parameters.addFields(Field.equal(Configuration.idField, docId, ":id") :: Nil).toSeq, conn, Results.fromData) /** - * Retrieve a document by its ID (creates connection + * 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 @@ -128,7 +128,7 @@ object Find: * @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[?]] = List())(using tag: ClassTag[Doc]): List[Doc] = + orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): List[Doc] = Using(Configuration.dbConn()) { conn => byFields[Doc](tableName, fields, howMatched, orderBy, conn) }.get /** @@ -168,7 +168,7 @@ object Find: * @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[?]] = List()) + 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 @@ -209,7 +209,7 @@ object Find: * @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[?]] = List()) + 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 @@ -283,7 +283,7 @@ object Find: * @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[?]] = List())(using tag: ClassTag[Doc]): Option[Doc] = + orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): Option[Doc] = Using(Configuration.dbConn()) { conn => firstByFields[Doc](tableName, fields, howMatched, orderBy, conn) }.get /** @@ -323,7 +323,7 @@ object Find: * @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[?]] = List()) + 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 @@ -364,6 +364,6 @@ object Find: * @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[?]] = List()) + 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..d38c968 --- /dev/null +++ b/src/scala/src/main/scala/Json.scala @@ -0,0 +1,341 @@ +package solutions.bitbadger.documents.scala + +import solutions.bitbadger.documents.{Field, FieldMatch} +import solutions.bitbadger.documents.java.Json as CoreJson + +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, 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 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 (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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) diff --git a/src/scala/src/main/scala/extensions/package.scala b/src/scala/src/main/scala/extensions/package.scala index 75275f6..e51dd15 100644 --- a/src/scala/src/main/scala/extensions/package.scala +++ b/src/scala/src/main/scala/extensions/package.scala @@ -113,7 +113,7 @@ extension (conn: Connection) * @param parameters Parameters to use for the query * @throws DocumentException If parameters are invalid */ - def customNonQuery(query: String, parameters: Seq[Parameter[?]] = List()): Unit = + def customNonQuery(query: String, parameters: Seq[Parameter[?]] = Nil): Unit = Custom.nonQuery(query, parameters, conn) /** @@ -298,7 +298,7 @@ extension (conn: Connection) def existsByJsonPath(tableName: String, path: String): Boolean = Exists.byJsonPath(tableName, path, conn) - // ~~~ DOCUMENT RETRIEVAL QUERIES ~~~ + // ~~~ DOCUMENT RETRIEVAL QUERIES (Domain Objects) ~~~ /** * Retrieve all documents in the given table, ordering results by the optional given fields @@ -308,7 +308,7 @@ extension (conn: Connection) * @return A list of documents from the given table * @throws DocumentException If query execution fails */ - def findAll[Doc](tableName: String, orderBy: Seq[Field[?]] = List())(using tag: ClassTag[Doc]): List[Doc] = + def findAll[Doc](tableName: String, orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): List[Doc] = Find.all[Doc](tableName, orderBy, conn) /** @@ -333,7 +333,7 @@ extension (conn: Connection) * @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[?]] = List())(using tag: ClassTag[Doc]): List[Doc] = + orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): List[Doc] = Find.byFields[Doc](tableName, fields, howMatched, orderBy, conn) /** @@ -346,7 +346,7 @@ extension (conn: Connection) * @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[?]] = List()) + 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) @@ -359,7 +359,7 @@ extension (conn: Connection) * @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[?]] = List()) + def findByJsonPath[Doc](tableName: String, path: String, orderBy: Seq[Field[?]] = Nil) (using tag: ClassTag[Doc]): List[Doc] = Find.byJsonPath[Doc](tableName, path, orderBy, conn) @@ -374,7 +374,7 @@ extension (conn: Connection) * @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[?]] = List())(using tag: ClassTag[Doc]): Option[Doc] = + orderBy: Seq[Field[?]] = Nil)(using tag: ClassTag[Doc]): Option[Doc] = Find.firstByFields[Doc](tableName, fields, howMatched, orderBy, conn) /** @@ -386,7 +386,7 @@ extension (conn: Connection) * @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[?]] = List()) + 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) @@ -399,10 +399,110 @@ extension (conn: Connection) * @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[?]] = List()) + 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 PATCH (PARTIAL UPDATE) QUERIES ~~~ /** -- 2.47.2 From 33daaed7b4a80b9e462f8676472c7b440aaf6611 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 29 Mar 2025 17:59:08 -0400 Subject: [PATCH 75/88] Add Scala Json ITs --- .../test/scala/integration/JsonDocument.scala | 15 + .../scala/integration/JsonFunctions.scala | 287 +++++++++++++++++- .../scala/integration/PostgreSQLFindIT.scala | 1 - .../scala/integration/PostgreSQLJsonIT.scala | 156 ++++++++++ .../test/scala/integration/SQLiteJsonIT.scala | 112 +++++++ 5 files changed, 568 insertions(+), 3 deletions(-) create mode 100644 src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala create mode 100644 src/scala/src/test/scala/integration/SQLiteJsonIT.scala diff --git a/src/scala/src/test/scala/integration/JsonDocument.scala b/src/scala/src/test/scala/integration/JsonDocument.scala index 899409d..b5ef2e7 100644 --- a/src/scala/src/test/scala/integration/JsonDocument.scala +++ b/src/scala/src/test/scala/integration/JsonDocument.scala @@ -17,3 +17,18 @@ object JsonDocument: 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 index a8cfc97..938b064 100644 --- a/src/scala/src/test/scala/integration/JsonFunctions.scala +++ b/src/scala/src/test/scala/integration/JsonFunctions.scala @@ -1,7 +1,21 @@ package solutions.bitbadger.documents.scala.tests.integration -import solutions.bitbadger.documents.{Configuration, Dialect} +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 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: /** @@ -14,4 +28,273 @@ object JsonFunctions: def maybeJsonB(json: String): String = Configuration.dialect() match case Dialect.SQLITE => json - case Dialect.POSTGRESQL => json.replace("\":\"", "\": \"").replace("\",\"", "\", \"").replace("\":[", "\": [") + 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"""") + + def allDefault(db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonAll(TEST_TABLE) + 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 allEmpty(db: ThrowawayDatabase): Unit = + assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") + + def byIdString (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonById(TEST_TABLE, "two") + 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 byIdNumber (db: ThrowawayDatabase): Unit = + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + assertEquals(maybeJsonB("""{"key":18,"text":"howdy"}"""), db.conn.jsonById(TEST_TABLE, 18), + "The document should have been found by numeric ID") + } finally { + Configuration.idField = "id" + } + + def byIdNotFound (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + + def byFieldsMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonByFields(TEST_TABLE, + Field.any("value", ("blue" :: "purple" :: Nil).asJava) :: Field.exists("sub") :: Nil, Some(FieldMatch.ALL)) + 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 byFieldsMatchOrdered (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonByFields(TEST_TABLE, Field.equal("value", "purple") :: Nil, None, Field.named("id") :: Nil) + 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 byFieldsMatchNumIn (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonByFields(TEST_TABLE, Field.any("numValue", (2 :: 4 :: 6 :: 8 :: Nil).asJava) :: Nil) + 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 byFieldsNoMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals("[]", db.conn.jsonByFields(TEST_TABLE, Field.greater("numValue", 100) :: Nil), + "There should have been no documents returned") + + def byFieldsMatchInArray (db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + val json = db.conn.jsonByFields(TEST_TABLE, Field.inArray("values", TEST_TABLE, ("c" :: Nil).asJava) :: Nil) + 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 byFieldsNoMatchInArray (db: ThrowawayDatabase): Unit = + ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } + assertEquals("[]", + db.conn.jsonByFields(TEST_TABLE, Field.inArray("values", TEST_TABLE, ("j" :: Nil).asJava) :: Nil), + "There should have been no documents returned") + + def byContainsMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonByContains(TEST_TABLE, Map.Map1("value", "purple")) + 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 byContainsMatchOrdered (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonByContains(TEST_TABLE, Map.Map1("sub", Map.Map1("foo", "green")), + Field.named("value") :: Nil) + 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 byContainsNoMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals("[]", db.conn.jsonByContains(TEST_TABLE, Map.Map1("value", "indigo")), + "There should have been no documents returned") + + def byJsonPathMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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 byJsonPathMatchOrdered (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", Field.named("id") :: Nil) + 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 byJsonPathNoMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals("[]", db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no documents returned") + + def firstByFieldsMatchOne (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("value", "another") :: Nil) + 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 firstByFieldsMatchMany (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("sub.foo", "green") :: Nil) + 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 firstByFieldsMatchOrdered (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("sub.foo", "green") :: Nil, None, + Field.named("n:numValue DESC") :: Nil) + 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 firstByFieldsNoMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals("{}", db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("value", "absent") :: Nil), + "There should have been no document returned") + + def firstByContainsMatchOne (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "FIRST!")) + 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 firstByContainsMatchMany (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "purple")) + 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 firstByContainsMatchOrdered (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "purple"), + Field.named("sub.bar NULLS FIRST") :: Nil) + 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 firstByContainsNoMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals("{}", db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "indigo")), + "There should have been no document returned") + + def firstByJsonPathMatchOne (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + 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 firstByJsonPathMatchMany (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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 firstByJsonPathMatchOrdered (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", Field.named("id DESC") :: Nil) + 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 firstByJsonPathNoMatch (db: ThrowawayDatabase): Unit = + JsonDocument.load(db) + assertEquals("{}", db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no document returned") diff --git a/src/scala/src/test/scala/integration/PostgreSQLFindIT.scala b/src/scala/src/test/scala/integration/PostgreSQLFindIT.scala index 7074ada..e7f85be 100644 --- a/src/scala/src/test/scala/integration/PostgreSQLFindIT.scala +++ b/src/scala/src/test/scala/integration/PostgreSQLFindIT.scala @@ -169,4 +169,3 @@ class PostgreSQLFindIT: @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..cc7f04c --- /dev/null +++ b/src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala @@ -0,0 +1,156 @@ +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) } 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..b58a35e --- /dev/null +++ b/src/scala/src/test/scala/integration/SQLiteJsonIT.scala @@ -0,0 +1,112 @@ +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)) + } -- 2.47.2 From 79436d6fd018d057e972d7aba773928500f7f4c0 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 29 Mar 2025 19:49:35 -0400 Subject: [PATCH 76/88] Add Json support to kotlinx --- src/core/src/main/kotlin/java/Json.kt | 8 +- src/kotlinx/src/main/kotlin/Json.kt | 348 ++++++++++++++++ .../src/main/kotlin/extensions/Connection.kt | 115 +++++- src/kotlinx/src/test/kotlin/Types.kt | 15 + .../test/kotlin/integration/JsonFunctions.kt | 379 +++++++++++++++++- .../kotlin/integration/PostgreSQLJsonIT.kt | 156 +++++++ .../test/kotlin/integration/SQLiteJsonIT.kt | 112 ++++++ 7 files changed, 1125 insertions(+), 8 deletions(-) create mode 100644 src/kotlinx/src/main/kotlin/Json.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt create mode 100644 src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt diff --git a/src/core/src/main/kotlin/java/Json.kt b/src/core/src/main/kotlin/java/Json.kt index a5b4c1e..abeda6f 100644 --- a/src/core/src/main/kotlin/java/Json.kt +++ b/src/core/src/main/kotlin/java/Json.kt @@ -145,12 +145,8 @@ object Json { */ @Throws(DocumentException::class) @JvmStatic - fun byFields( - tableName: String, - fields: Collection>, - howMatched: FieldMatch? = null, - conn: Connection - ) = byFields(tableName, fields, howMatched, null, conn) + fun byFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) = + byFields(tableName, fields, howMatched, null, conn) /** * Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only) diff --git a/src/kotlinx/src/main/kotlin/Json.kt b/src/kotlinx/src/main/kotlin/Json.kt new file mode 100644 index 0000000..a8d14bd --- /dev/null +++ b/src/kotlinx/src/main/kotlin/Json.kt @@ -0,0 +1,348 @@ +package solutions.bitbadger.documents.kotlinx + +import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.query.FindQuery +import solutions.bitbadger.documents.query.orderBy +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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) + + /** + * 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) } + + /** + * 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) +} diff --git a/src/kotlinx/src/main/kotlin/extensions/Connection.kt b/src/kotlinx/src/main/kotlin/extensions/Connection.kt index 2b15a1b..090a0c1 100644 --- a/src/kotlinx/src/main/kotlin/extensions/Connection.kt +++ b/src/kotlinx/src/main/kotlin/extensions/Connection.kt @@ -233,7 +233,7 @@ inline fun Connection.existsByContains(tableName: String, cr fun Connection.existsByJsonPath(tableName: String, path: String) = Exists.byJsonPath(tableName, path, this) -// ~~~ DOCUMENT RETRIEVAL QUERIES ~~~ +// ~~~ DOCUMENT RETRIEVAL QUERIES (Domain Objects) ~~~ /** * Retrieve all documents in the given table, ordering results by the optional given fields @@ -347,6 +347,119 @@ inline fun Connection.findFirstByJsonPath( 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; 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 Connection.jsonFirstByJsonPath(tableName: String, path: String, orderBy: Collection>? = null) = + Json.firstByJsonPath(tableName, path, orderBy, this) + // ~~~ DOCUMENT PATCH (PARTIAL UPDATE) QUERIES ~~~ /** diff --git a/src/kotlinx/src/test/kotlin/Types.kt b/src/kotlinx/src/test/kotlin/Types.kt index ab3754f..426de88 100644 --- a/src/kotlinx/src/test/kotlin/Types.kt +++ b/src/kotlinx/src/test/kotlin/Types.kt @@ -49,5 +49,20 @@ data class JsonDocument(val id: String, val value: String = "", val numValue: In 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/JsonFunctions.kt b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt index f877497..be8e339 100644 --- a/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt +++ b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt @@ -2,7 +2,22 @@ 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 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 { /** @@ -15,6 +30,368 @@ object JsonFunctions { fun maybeJsonB(json: String) = when (Configuration.dialect()) { Dialect.SQLITE -> json - Dialect.POSTGRESQL -> json.replace("\":\"", "\": \"").replace("\",\"", "\", \"").replace("\":[", "\": [") + 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\"") + + fun allDefault(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonAll(TEST_TABLE) + 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 allEmpty(db: ThrowawayDatabase) = + assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") + + fun byIdString(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonById(TEST_TABLE, "two") + 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 byIdNumber(db: ThrowawayDatabase) { + Configuration.idField = "key" + try { + db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) + assertEquals( + maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), db.conn.jsonById(TEST_TABLE, 18), + "The document should have been found by numeric ID" + ) + } finally { + Configuration.idField = "id" + } + } + + fun byIdNotFound(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + } + + fun byFieldsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByFields( + TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL + ) + 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 byFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByFields( + TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + ) + 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 byFieldsMatchNumIn(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + 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 byFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "[]", db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))), + "There should have been no documents returned" + ) + } + + fun byFieldsMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + 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 byFieldsNoMatchInArray(db: ThrowawayDatabase) { + ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } + assertEquals( + "[]", db.conn.jsonByFields( + TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) + ), "There should have been no documents returned" + ) + } + + fun byContainsMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByContains>(TEST_TABLE, mapOf("value" to "purple")) + 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 byContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByContains>>( + TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) + ) + 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 byContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "[]", + db.conn.jsonByContains>(TEST_TABLE, mapOf("value" to "indigo")), + "There should have been no documents returned" + ) + } + + fun byJsonPathMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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 byJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + 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 byJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "[]", + db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no documents returned" + ) + } + + fun firstByFieldsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) + 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 firstByFieldsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) + 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 firstByFieldsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) + ) + 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 firstByFieldsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), + "There should have been no document returned" + ) + } + + fun firstByContainsMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "FIRST!")) + 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 firstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "purple")) + 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 firstByContainsMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByContains>( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) + 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 firstByContainsNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "indigo")), + "There should have been no document returned" + ) + } + + fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + 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 firstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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 firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + JsonDocument.load(db) + val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + 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 firstByJsonPathNoMatch(db: ThrowawayDatabase) { + JsonDocument.load(db) + assertEquals( + "{}", + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), + "There should have been no document returned" + ) + } + } \ No newline at end of file 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..c17b2f4 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt @@ -0,0 +1,156 @@ +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) +} \ No newline at end of file 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..91bcb67 --- /dev/null +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt @@ -0,0 +1,112 @@ +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) } + } +} \ No newline at end of file -- 2.47.2 From 0070b8059216fde23571c70da0b2cc57f7b26356 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 29 Mar 2025 22:23:47 -0400 Subject: [PATCH 77/88] Add custom writer funcs to core --- src/core/src/main/kotlin/java/Custom.kt | 41 +++++++++++++++++ src/core/src/main/kotlin/java/Results.kt | 31 +++++++++++++ .../main/kotlin/java/extensions/Connection.kt | 46 +++++++++++-------- .../java/integration/CustomFunctions.java | 31 +++++++++++++ .../java/integration/PostgreSQLCustomIT.java | 24 ++++++++++ .../java/integration/SQLiteCustomIT.java | 24 ++++++++++ .../kotlin/integration/CustomFunctions.kt | 43 +++++++++++++++-- .../kotlin/integration/PostgreSQLCustomIT.kt | 15 ++++++ .../test/kotlin/integration/SQLiteCustomIT.kt | 15 ++++++ 9 files changed, 248 insertions(+), 22 deletions(-) diff --git a/src/core/src/main/kotlin/java/Custom.kt b/src/core/src/main/kotlin/java/Custom.kt index b1fc6be..025b8b9 100644 --- a/src/core/src/main/kotlin/java/Custom.kt +++ b/src/core/src/main/kotlin/java/Custom.kt @@ -3,6 +3,7 @@ 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 @@ -87,6 +88,46 @@ object Custom { 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 + * @return A JSON array of results for the given 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 + * @return A JSON array of results for the given 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 * diff --git a/src/core/src/main/kotlin/java/Results.kt b/src/core/src/main/kotlin/java/Results.kt index c71557c..0e98fc3 100644 --- a/src/core/src/main/kotlin/java/Results.kt +++ b/src/core/src/main/kotlin/java/Results.kt @@ -3,6 +3,7 @@ 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 @@ -133,4 +134,34 @@ object Results { } 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 index f5c9ac1..cb163b3 100644 --- a/src/core/src/main/kotlin/java/extensions/Connection.kt +++ b/src/core/src/main/kotlin/java/extensions/Connection.kt @@ -4,6 +4,7 @@ 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 @@ -26,8 +27,7 @@ fun Connection.customList( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> TDoc -) = - Custom.list(query, parameters, clazz, this, mapFunc) +) = Custom.list(query, parameters, clazz, this, mapFunc) /** * Execute a query that returns a JSON array of results @@ -43,8 +43,25 @@ fun Connection.customJsonArray( query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String -) = - Custom.jsonArray(query, parameters, this, mapFunc) +) = 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 @@ -62,8 +79,7 @@ fun Connection.customSingle( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> TDoc -) = - Custom.single(query, parameters, clazz, this, mapFunc) +) = Custom.single(query, parameters, clazz, this, mapFunc) /** * Execute a query that returns JSON for one or no documents @@ -79,8 +95,7 @@ fun Connection.customJsonSingle( query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String -) = - Configuration.dbConn().use { Custom.jsonSingle(query, parameters, it, mapFunc) } +) = Configuration.dbConn().use { Custom.jsonSingle(query, parameters, it, mapFunc) } /** * Execute a query that returns no results @@ -110,8 +125,7 @@ fun Connection.customScalar( parameters: Collection> = listOf(), clazz: Class, mapFunc: (ResultSet, Class) -> T -) = - Custom.scalar(query, parameters, clazz, this, mapFunc) +) = Custom.scalar(query, parameters, clazz, this, mapFunc) // ~~~ DEFINITION QUERIES ~~~ @@ -591,8 +605,7 @@ fun Connection.patchByFields( fields: Collection>, patch: TPatch, howMatched: FieldMatch? = null -) = - Patch.byFields(tableName, fields, patch, howMatched, this) +) = Patch.byFields(tableName, fields, patch, howMatched, this) /** * Patch documents using a JSON containment query (PostgreSQL only) @@ -607,8 +620,7 @@ fun Connection.patchByContains( tableName: String, criteria: TContains, patch: TPatch -) = - Patch.byContains(tableName, criteria, patch, this) +) = Patch.byContains(tableName, criteria, patch, this) /** * Patch documents using a JSON Path match query (PostgreSQL only) @@ -652,8 +664,7 @@ fun Connection.removeFieldsByFields( fields: Collection>, toRemove: Collection, howMatched: FieldMatch? = null -) = - RemoveFields.byFields(tableName, fields, toRemove, howMatched, this) +) = RemoveFields.byFields(tableName, fields, toRemove, howMatched, this) /** * Remove fields from documents using a JSON containment query (PostgreSQL only) @@ -668,8 +679,7 @@ fun Connection.removeFieldsByContains( tableName: String, criteria: TContains, toRemove: Collection -) = - RemoveFields.byContains(tableName, criteria, toRemove, this) +) = RemoveFields.byContains(tableName, criteria, toRemove, this) /** * Remove fields from documents using a JSON Path match query (PostgreSQL only) 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 index a2a4d55..f0a2dc9 100644 --- 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 @@ -7,6 +7,8 @@ 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; @@ -55,6 +57,35 @@ final public class CustomFunctions { "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) 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 index a2d8f5c..3f12577 100644 --- 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 @@ -51,6 +51,30 @@ final public class PostgreSQLCustomIT { } } + @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 { 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 index 66bc246..b2e25bf 100644 --- 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 @@ -51,6 +51,30 @@ final public class SQLiteCustomIT { } } + @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 { diff --git a/src/core/src/test/kotlin/integration/CustomFunctions.kt b/src/core/src/test/kotlin/integration/CustomFunctions.kt index dafce1c..6ce54d4 100644 --- a/src/core/src/test/kotlin/integration/CustomFunctions.kt +++ b/src/core/src/test/kotlin/integration/CustomFunctions.kt @@ -8,6 +8,8 @@ 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.* /** @@ -42,7 +44,7 @@ object CustomFunctions { fun jsonArraySingle(db: ThrowawayDatabase) { db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3"))) assertEquals( - JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), + 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" ) @@ -51,15 +53,48 @@ object CustomFunctions { 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\"]}]"), + 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( diff --git a/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt index ca28dd7..d96b076 100644 --- a/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt +++ b/src/core/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -35,6 +35,21 @@ class PostgreSQLCustomIT { 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() = diff --git a/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt index 9eca13a..01f8ffe 100644 --- a/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -34,6 +34,21 @@ class SQLiteCustomIT { 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() = -- 2.47.2 From e51728d16a2ac8437f6cabfafe89da28c724e8a1 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 30 Mar 2025 20:34:19 -0400 Subject: [PATCH 78/88] Add write methods for Json output --- src/core/src/main/kotlin/java/Json.kt | 473 +++++++++++++++- .../main/kotlin/java/extensions/Connection.kt | 145 ++++- .../test/kotlin/integration/JsonFunctions.kt | 512 ++++++++++++++---- .../kotlin/integration/PostgreSQLJsonIT.kt | 153 +++++- .../test/kotlin/integration/SQLiteJsonIT.kt | 99 ++++ 5 files changed, 1268 insertions(+), 114 deletions(-) diff --git a/src/core/src/main/kotlin/java/Json.kt b/src/core/src/main/kotlin/java/Json.kt index abeda6f..d41aa51 100644 --- a/src/core/src/main/kotlin/java/Json.kt +++ b/src/core/src/main/kotlin/java/Json.kt @@ -3,6 +3,7 @@ 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 @@ -23,7 +24,12 @@ object Json { @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) + Custom.jsonArray( + FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), + listOf(), + conn, + Results::jsonFromData + ) /** * Retrieve all documents in the given table (creates connection) @@ -52,6 +58,54 @@ object Json { 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 * @@ -84,6 +138,33 @@ object Json { 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 * @@ -148,6 +229,79 @@ object Json { 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) * @@ -166,11 +320,11 @@ object Json { orderBy: Collection>? = null, conn: Connection ) = Custom.jsonArray( - FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), - listOf(Parameters.json(":criteria", criteria)), - conn, - Results::jsonFromData - ) + 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 @@ -202,6 +356,67 @@ object Json { 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) * @@ -252,6 +467,63 @@ object Json { 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 * @@ -320,6 +592,71 @@ object Json { 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) * @@ -338,11 +675,11 @@ object Json { orderBy: Collection>? = null, conn: Connection ) = Custom.jsonSingle( - FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""), - listOf(Parameters.json(":criteria", criteria)), - conn, - Results::jsonFromData - ) + 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) @@ -374,6 +711,65 @@ object Json { 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) * @@ -423,4 +819,59 @@ object Json { @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/extensions/Connection.kt b/src/core/src/main/kotlin/java/extensions/Connection.kt index cb163b3..9b13fee 100644 --- a/src/core/src/main/kotlin/java/extensions/Connection.kt +++ b/src/core/src/main/kotlin/java/extensions/Connection.kt @@ -95,7 +95,7 @@ fun Connection.customJsonSingle( query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String -) = Configuration.dbConn().use { Custom.jsonSingle(query, parameters, it, mapFunc) } +) = Custom.jsonSingle(query, parameters, this, mapFunc) /** * Execute a query that returns no results @@ -575,6 +575,149 @@ fun Connection.jsonFirstByContains( 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 ~~~ /** diff --git a/src/core/src/test/kotlin/integration/JsonFunctions.kt b/src/core/src/test/kotlin/integration/JsonFunctions.kt index 94b23d4..2ef24f1 100644 --- a/src/core/src/test/kotlin/integration/JsonFunctions.kt +++ b/src/core/src/test/kotlin/integration/JsonFunctions.kt @@ -9,6 +9,8 @@ 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 @@ -45,9 +47,7 @@ object JsonFunctions { private fun docId(id: String) = maybeJsonB("{\"id\":\"$id\"") - fun allDefault(db: ThrowawayDatabase) { - JsonDocument.load(db) - val json = db.conn.jsonAll(TEST_TABLE) + private fun checkAllDefault(json: String) { assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") when (Configuration.dialect()) { Dialect.SQLITE -> { @@ -68,41 +68,98 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } - fun allEmpty(db: ThrowawayDatabase) = - assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") - - fun byIdString(db: ThrowawayDatabase) { + fun allDefault(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonById(TEST_TABLE, "two") + 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")) - assertEquals( - maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), db.conn.jsonById(TEST_TABLE, 18), - "The document should have been found by numeric ID" - ) + checkByIdNumber(db.conn.jsonById(TEST_TABLE, 18)) } finally { Configuration.idField = "id" } } - fun byIdNotFound(db: ThrowawayDatabase) { - JsonDocument.load(db) - assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + 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" + } } - fun byFieldsMatch(db: ThrowawayDatabase) { + private fun checkByIdNotFound(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun byIdNotFound(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByFields( - TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL - ) + 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 -> { @@ -111,13 +168,30 @@ object JsonFunctions { 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 byFieldsMatchOrdered(db: ThrowawayDatabase) { + fun writeByFieldsMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByFields( - TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + 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" @@ -133,11 +207,27 @@ object JsonFunctions { 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 byFieldsMatchNumIn(db: ThrowawayDatabase) { + fun writeByFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + 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 -> { @@ -146,37 +236,77 @@ object JsonFunctions { 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) - assertEquals( - "[]", db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))), - "There should have been no documents returned" - ) + checkByFieldsNoMatch(db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100)))) } - fun byFieldsMatchInArray(db: ThrowawayDatabase) { - ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } - val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + 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 byFieldsNoMatchInArray(db: ThrowawayDatabase) { + fun byFieldsMatchInArray(db: ThrowawayDatabase) { ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } - assertEquals( - "[]", db.conn.jsonByFields( - TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) - ), "There should have been no documents returned" + checkByFieldsMatchInArray( + db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) ) } - fun byContainsMatch(db: ThrowawayDatabase) { - JsonDocument.load(db) - val json = db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "purple")) + 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 -> { @@ -191,11 +321,20 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } - fun byContainsMatchOrdered(db: ThrowawayDatabase) { + fun byContainsMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByContains( - TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) - ) + 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" @@ -210,20 +349,41 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } } - } - fun byContainsNoMatch(db: ThrowawayDatabase) { + fun byContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "[]", - db.conn.jsonByContains(TEST_TABLE, mapOf("value" to "indigo")), - "There should have been no documents returned" + checkByContainsMatchOrdered( + db.conn.jsonByContains(TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value"))) ) } - fun byJsonPathMatch(db: ThrowawayDatabase) { + fun writeByContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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 -> { @@ -238,9 +398,20 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } - fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { + fun byJsonPathMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + 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" @@ -256,29 +427,58 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } } - } - fun byJsonPathNoMatch(db: ThrowawayDatabase) { + fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "[]", - db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no documents returned" + checkByJsonPathMatchOrdered( + db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) ) } - fun firstByFieldsMatchOne(db: ThrowawayDatabase) { + fun writeByJsonPathMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) + 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 firstByFieldsMatchMany(db: ThrowawayDatabase) { + fun writeFirstByFieldsMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) + 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), @@ -289,40 +489,84 @@ object JsonFunctions { "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 firstByFieldsMatchOrdered(db: ThrowawayDatabase) { + fun writeFirstByFieldsMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields( - TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) - ) + 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 firstByFieldsNoMatch(db: ThrowawayDatabase) { + fun firstByFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "{}", - db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), - "There should have been no document returned" + checkFirstByFieldsMatchOrdered( + db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) + ) ) } - fun firstByContainsMatchOne(db: ThrowawayDatabase) { + fun writeFirstByFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "FIRST!")) + 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 firstByContainsMatchMany(db: ThrowawayDatabase) { + fun writeFirstByContainsMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "purple")) + 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), @@ -333,40 +577,81 @@ object JsonFunctions { "Expected document 'four' or 'five' ($json)" ) } + + fun firstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsMatchMany(db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "purple"))) } - fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { + fun writeFirstByContainsMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains( - TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) - ) + 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 firstByContainsNoMatch(db: ThrowawayDatabase) { + fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "{}", - db.conn.jsonFirstByContains(TEST_TABLE, mapOf("value" to "indigo")), - "There should have been no document returned" + checkFirstByContainsMatchOrdered( + db.conn.jsonFirstByContains( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) ) } - fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { + fun writeFirstByContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + 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 firstByJsonPathMatchMany(db: ThrowawayDatabase) { + fun writeFirstByJsonPathMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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), @@ -377,23 +662,54 @@ object JsonFunctions { "Expected document 'four' or 'five' ($json)" ) } + + fun firstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathMatchMany(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)")) } - fun firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + fun writeFirstByJsonPathMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + 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) - assertEquals( - "{}", - db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no document returned" - ) + 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/PostgreSQLJsonIT.kt b/src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt index 4e98051..703078d 100644 --- a/src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt +++ b/src/core/src/test/kotlin/integration/PostgreSQLJsonIT.kt @@ -30,7 +30,7 @@ class PostgreSQLJsonIT { PgDB().use(JsonFunctions::byIdNumber) @Test - @DisplayName("byId returns null when a matching ID is not found") + @DisplayName("byId returns an empty document when a matching ID is not found") fun byIdNotFound() = PgDB().use(JsonFunctions::byIdNotFound) @@ -110,7 +110,7 @@ class PostgreSQLJsonIT { PgDB().use(JsonFunctions::firstByFieldsMatchOrdered) @Test - @DisplayName("firstByFields returns null when no document matches") + @DisplayName("firstByFields returns an empty document when no document matches") fun firstByFieldsNoMatch() = PgDB().use(JsonFunctions::firstByFieldsNoMatch) @@ -130,7 +130,7 @@ class PostgreSQLJsonIT { PgDB().use(JsonFunctions::firstByContainsMatchOrdered) @Test - @DisplayName("firstByContains returns null when no document matches") + @DisplayName("firstByContains returns an empty document when no document matches") fun firstByContainsNoMatch() = PgDB().use(JsonFunctions::firstByContainsNoMatch) @@ -150,7 +150,152 @@ class PostgreSQLJsonIT { PgDB().use(JsonFunctions::firstByJsonPathMatchOrdered) @Test - @DisplayName("firstByJsonPath returns null when no document matches") + @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/SQLiteJsonIT.kt b/src/core/src/test/kotlin/integration/SQLiteJsonIT.kt index 6f20601..f416849 100644 --- a/src/core/src/test/kotlin/integration/SQLiteJsonIT.kt +++ b/src/core/src/test/kotlin/integration/SQLiteJsonIT.kt @@ -109,4 +109,103 @@ class SQLiteJsonIT { 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) } + } } -- 2.47.2 From 72b2c69c111cf60112962e0585d7d82093c13e0f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 31 Mar 2025 13:20:42 -0400 Subject: [PATCH 79/88] Add Java Json write ITs --- .../tests/java/integration/JsonFunctions.java | 491 +++++++++++++++--- .../java/integration/PostgreSQLJsonIT.java | 232 +++++++++ .../tests/java/integration/SQLiteJsonIT.java | 152 ++++++ src/core/src/test/kotlin/integration/PgDB.kt | 15 +- .../src/test/kotlin/integration/SQLiteDB.kt | 7 +- .../kotlin/integration/ThrowawayDatabase.kt | 15 +- 6 files changed, 814 insertions(+), 98 deletions(-) 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 index 646e386..e24d46b 100644 --- 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 @@ -3,6 +3,8 @@ 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; @@ -45,9 +47,7 @@ final public class JsonFunctions { return maybeJsonB(String.format("{\"id\":\"%s\"", id)); } - public static void allDefault(ThrowawayDatabase db) throws DocumentException { - JsonDocument.load(db); - final String json = jsonAll(db.getConn(), TEST_TABLE); + private static void checkAllDefault(String json) throws DocumentException { assertTrue(json.startsWith("["), "JSON should start with '[' ($json)"); switch (Configuration.dialect()) { case SQLITE: @@ -74,13 +74,35 @@ final public class JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)"); } - public static void allEmpty(ThrowawayDatabase db) throws DocumentException { - assertEquals("[]", jsonAll(db.getConn(), TEST_TABLE), "There should have been no documents returned"); + public static void allDefault(ThrowawayDatabase db) throws DocumentException { + JsonDocument.load(db); + checkAllDefault(jsonAll(db.getConn(), TEST_TABLE)); } - public static void byIdString(ThrowawayDatabase db) throws DocumentException { + public static void writeAllDefault(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonById(db.getConn(), TEST_TABLE, "two"); + 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"); @@ -91,26 +113,65 @@ final public class JsonFunctions { } } + 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")); - assertEquals(maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), jsonById(db.getConn(), TEST_TABLE, 18), - "The document should have been found by numeric ID"); + checkByIdNumber(jsonById(db.getConn(), TEST_TABLE, 18)); } finally { Configuration.idField = "id"; } } - public static void byIdNotFound(ThrowawayDatabase db) throws DocumentException { - JsonDocument.load(db); - assertEquals("{}", jsonById(db.getConn(), TEST_TABLE, "x"), "There should have been no document returned"); + 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"; + } } - public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException { + 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); - final String json = jsonByFields(db.getConn(), TEST_TABLE, - List.of(Field.any("value", List.of("blue", "purple")), Field.exists("sub")), FieldMatch.ALL); + 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"); @@ -124,10 +185,22 @@ final public class JsonFunctions { } } - public static void byFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "purple")), null, - List.of(Field.named("id"))); + 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, @@ -146,9 +219,22 @@ final public class JsonFunctions { } } - public static void byFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException { + public static void byFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.any("numValue", List.of(2, 4, 6, 8)))); + 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"); @@ -162,32 +248,77 @@ final public class JsonFunctions { } } - public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + public static void byFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals("[]", jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.greater("numValue", 100))), - "There should have been no documents returned"); + checkByFieldsMatchNumIn(jsonByFields(db.getConn(), TEST_TABLE, + List.of(Field.any("numValue", List.of(2, 4, 6, 8))))); } - public static void byFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException { - for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } - final String json = jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE, - List.of("c")))); + 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 byFieldsNoMatchInArray(ThrowawayDatabase db) throws DocumentException { + public static void byFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException { for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); } - assertEquals("[]", - jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE, List.of("j")))), - "There should have been no documents returned"); + checkByFieldsMatchInArray(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE, + List.of("c"))))); } - public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { - JsonDocument.load(db); - final String json = jsonByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")); + 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: @@ -202,10 +333,20 @@ final public class JsonFunctions { assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); } - public static void byContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonByContains(db.getConn(), TEST_TABLE, Map.of("sub", Map.of("foo", "green")), - List.of(Field.named("value"))); + 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, @@ -223,15 +364,39 @@ final public class JsonFunctions { } } - public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + public static void byContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals("[]", jsonByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo")), - "There should have been no documents returned"); + checkByContainsMatchOrdered(jsonByContains(db.getConn(), TEST_TABLE, Map.of("sub", Map.of("foo", "green")), + List.of(Field.named("value")))); } - public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { + public static void writeByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)"); + 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: @@ -246,10 +411,20 @@ final public class JsonFunctions { assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json)); } - public static void byJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", - List.of(Field.named("id"))); + 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, @@ -268,15 +443,38 @@ final public class JsonFunctions { } } - public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException { + public static void byJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals("[]", jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no documents returned"); + checkByJsonPathMatchOrdered(jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", + List.of(Field.named("id")))); } - public static void firstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException { + public static void writeByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "another"))); + 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"); @@ -288,9 +486,21 @@ final public class JsonFunctions { } } - public static void firstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException { + public static void firstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("sub.foo", "green"))); + 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), @@ -303,10 +513,21 @@ final public class JsonFunctions { } } - public static void firstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + public static void firstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("sub.foo", "green")), null, - List.of(Field.named("n:numValue DESC"))); + 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"); @@ -318,15 +539,39 @@ final public class JsonFunctions { } } - public static void firstByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException { + public static void firstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals("{}", jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "absent"))), - "There should have been no document returned"); + checkFirstByFieldsMatchOrdered(jsonFirstByFields(db.getConn(), TEST_TABLE, + List.of(Field.equal("sub.foo", "green")), null, List.of(Field.named("n:numValue DESC")))); } - public static void firstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException { + public static void writeFirstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "FIRST!")); + 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"); @@ -337,9 +582,20 @@ final public class JsonFunctions { } } - public static void firstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException { + public static void firstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")); + 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), @@ -352,10 +608,20 @@ final public class JsonFunctions { } } - public static void firstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { + public static void firstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"), - List.of(Field.named("sub.bar NULLS FIRST"))); + 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"); @@ -367,15 +633,39 @@ final public class JsonFunctions { } } - public static void firstByContainsNoMatch(ThrowawayDatabase db) throws DocumentException { + public static void firstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - assertEquals("{}", jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo")), - "There should have been no document returned"); + checkFirstByContainsMatchOrdered(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"), + List.of(Field.named("sub.bar NULLS FIRST")))); } - public static void firstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException { + public static void writeFirstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10)"); + 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"); @@ -386,9 +676,20 @@ final public class JsonFunctions { } } - public static void firstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException { + public static void firstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)"); + 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), @@ -401,10 +702,20 @@ final public class JsonFunctions { } } - public static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException { + public static void firstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException { JsonDocument.load(db); - final String json = jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)", - List.of(Field.named("id DESC"))); + 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"); @@ -416,9 +727,35 @@ final public class JsonFunctions { } } + 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); - assertEquals("{}", jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no document returned"); + 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/PostgreSQLJsonIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/PostgreSQLJsonIT.java index c8129a5..144df98 100644 --- 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 @@ -242,4 +242,236 @@ final public class PostgreSQLJsonIT { 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/SQLiteJsonIT.java b/src/core/src/test/java/solutions/bitbadger/documents/core/tests/java/integration/SQLiteJsonIT.java index 1b3d447..37b56aa 100644 --- 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 @@ -164,4 +164,156 @@ final public class SQLiteJsonIT { 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/kotlin/integration/PgDB.kt b/src/core/src/test/kotlin/integration/PgDB.kt index 4586d5f..1daed4d 100644 --- a/src/core/src/test/kotlin/integration/PgDB.kt +++ b/src/core/src/test/kotlin/integration/PgDB.kt @@ -2,24 +2,17 @@ package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.* import solutions.bitbadger.documents.core.tests.TEST_TABLE -import solutions.bitbadger.documents.java.DocumentConfig import solutions.bitbadger.documents.java.Results import solutions.bitbadger.documents.java.extensions.* /** * A wrapper for a throwaway PostgreSQL database */ -class PgDB : ThrowawayDatabase { - - private var dbName = "" +class PgDB : ThrowawayDatabase() { init { - DocumentConfig.serializer = JacksonDocumentSerializer() - dbName = "throwaway_${AutoId.generateRandomString(8)}" Configuration.connectionString = connString("postgres") - Configuration.dbConn().use { - it.customNonQuery("CREATE DATABASE $dbName") - } + Configuration.dbConn().use { it.customNonQuery("CREATE DATABASE $dbName") } Configuration.connectionString = connString(dbName) } @@ -32,9 +25,7 @@ class PgDB : ThrowawayDatabase { override fun close() { conn.close() Configuration.connectionString = connString("postgres") - Configuration.dbConn().use { - it.customNonQuery("DROP DATABASE $dbName") - } + Configuration.dbConn().use { it.customNonQuery("DROP DATABASE $dbName") } Configuration.connectionString = null } diff --git a/src/core/src/test/kotlin/integration/SQLiteDB.kt b/src/core/src/test/kotlin/integration/SQLiteDB.kt index 1c930a0..4f9a05f 100644 --- a/src/core/src/test/kotlin/integration/SQLiteDB.kt +++ b/src/core/src/test/kotlin/integration/SQLiteDB.kt @@ -2,7 +2,6 @@ package solutions.bitbadger.documents.core.tests.integration import solutions.bitbadger.documents.* import solutions.bitbadger.documents.core.tests.TEST_TABLE -import solutions.bitbadger.documents.java.DocumentConfig import solutions.bitbadger.documents.java.Results import solutions.bitbadger.documents.java.extensions.* import java.io.File @@ -10,13 +9,9 @@ import java.io.File /** * A wrapper for a throwaway SQLite database */ -class SQLiteDB : ThrowawayDatabase { - - private var dbName = "" +class SQLiteDB : ThrowawayDatabase() { init { - DocumentConfig.serializer = JacksonDocumentSerializer() - dbName = "test-db-${AutoId.generateRandomString(8)}.db" Configuration.connectionString = "jdbc:sqlite:$dbName" } diff --git a/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt b/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt index 4f454d1..b2b48e7 100644 --- a/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt +++ b/src/core/src/test/kotlin/integration/ThrowawayDatabase.kt @@ -1,14 +1,23 @@ 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 */ -interface ThrowawayDatabase : AutoCloseable { +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 */ - val conn: Connection + abstract val conn: Connection /** * Determine if a database object exists @@ -16,5 +25,5 @@ interface ThrowawayDatabase : AutoCloseable { * @param name The name of the object whose existence should be checked * @return True if the object exists, false if not */ - fun dbObjectExists(name: String): Boolean + abstract fun dbObjectExists(name: String): Boolean } -- 2.47.2 From 18866b3ff7da2cdf4e8bcc47ce15350582a4b6b7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 31 Mar 2025 20:04:43 -0400 Subject: [PATCH 80/88] Add JSON writing to Groovy impl --- .../tests/integration/CustomFunctions.groovy | 38 +- .../tests/integration/JsonFunctions.groovy | 490 +++++++++++++++--- .../integration/PostgreSQLCustomIT.groovy | 18 + .../tests/integration/PostgreSQLJsonIT.groovy | 174 +++++++ .../tests/integration/SQLiteCustomIT.groovy | 36 ++ .../tests/integration/SQLiteJsonIT.groovy | 122 +++++ 6 files changed, 795 insertions(+), 83 deletions(-) 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 index 36a5bc9..70732bb 100644 --- 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 @@ -30,14 +30,14 @@ final class CustomFunctions { 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), + 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), + db.conn.customJsonArray(FindQuery.all(TEST_TABLE), List.of(), Results.&jsonFromData), 'A single document list was not represented correctly') } @@ -46,10 +46,38 @@ final class CustomFunctions { 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), + 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(), @@ -65,14 +93,14 @@ final class CustomFunctions { } static void jsonSingleNone(ThrowawayDatabase db) { - assertEquals('{}', db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData), + 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), + db.conn.customJsonSingle(FindQuery.all(TEST_TABLE), List.of(), Results.&jsonFromData), 'A single document was not represented correctly'); } 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 index 56c8b03..186c480 100644 --- 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 @@ -43,9 +43,7 @@ final class JsonFunctions { return maybeJsonB("{\"id\":\"$id\"") } - static void allDefault(ThrowawayDatabase db) { - JsonDocument.load(db) - String json = db.conn.jsonAll(TEST_TABLE) + private static void checkAllDefault(String json) { assertTrue(json.startsWith('['), "JSON should start with '[' ($json)") switch (Configuration.dialect()) { case Dialect.SQLITE: @@ -66,13 +64,35 @@ final class JsonFunctions { assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") } - static void allEmpty(ThrowawayDatabase db) { - assertEquals('[]', db.conn.jsonAll(TEST_TABLE), 'There should have been no documents returned') + static void allDefault(ThrowawayDatabase db) { + JsonDocument.load(db) + checkAllDefault(db.conn.jsonAll(TEST_TABLE)) } - static void byIdString(ThrowawayDatabase db) { + static void writeAllDefault(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonById(TEST_TABLE, 'two') + 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') @@ -83,26 +103,64 @@ final class JsonFunctions { } } + 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')) - assertEquals(maybeJsonB('{"key":18,"text":"howdy"}'), db.conn.jsonById(TEST_TABLE, 18), - 'The document should have been found by numeric ID') + checkByIdNumber(db.conn.jsonById(TEST_TABLE, 18)) } finally { Configuration.idField = 'id' } } - static void byIdNotFound(ThrowawayDatabase db) { - JsonDocument.load(db) - assertEquals('{}', db.conn.jsonById(TEST_TABLE, 'x'), 'There should have been no document returned') + 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' + } } - static void byFieldsMatch(ThrowawayDatabase db) { + private static void checkByIdNotFound(String json) { + assertEquals('{}', json, 'There should have been no document returned') + } + + static void byIdNotFound(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonByFields(TEST_TABLE, List.of(Field.any('value', List.of('blue', 'purple')), - Field.exists('sub')), FieldMatch.ALL) + 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') @@ -115,10 +173,22 @@ final class JsonFunctions { } } - static void byFieldsMatchOrdered(ThrowawayDatabase db) { + static void byFieldsMatch(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonByFields(TEST_TABLE, List.of(Field.equal('value', 'purple')), null, - List.of(Field.named('id'))) + 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, @@ -136,9 +206,22 @@ final class JsonFunctions { } } - static void byFieldsMatchNumIn(ThrowawayDatabase db) { + static void byFieldsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonByFields(TEST_TABLE, List.of(Field.any('numValue', List.of(2, 4, 6, 8)))) + 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') @@ -151,30 +234,76 @@ final class JsonFunctions { } } - static void byFieldsNoMatch(ThrowawayDatabase db) { + static void byFieldsMatchNumIn(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('[]', db.conn.jsonByFields(TEST_TABLE, List.of(Field.greater('numValue', 100))), - 'There should have been no documents returned') + checkByFieldsMatchNumIn(db.conn.jsonByFields(TEST_TABLE, List.of(Field.any('numValue', List.of(2, 4, 6, 8))))) } - static void byFieldsMatchInArray(ThrowawayDatabase db) { - ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } - String json = db.conn.jsonByFields(TEST_TABLE, List.of(Field.inArray('values', TEST_TABLE, List.of('c')))) + 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 byFieldsNoMatchInArray(ThrowawayDatabase db) { + static void byFieldsMatchInArray(ThrowawayDatabase db) { ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } - assertEquals('[]', db.conn.jsonByFields(TEST_TABLE, List.of(Field.inArray('values', TEST_TABLE, List.of('j')))), - 'There should have been no documents returned') + checkByFieldsMatchInArray(db.conn.jsonByFields(TEST_TABLE, + List.of(Field.inArray('values', TEST_TABLE, List.of('c'))))) } - static void byContainsMatch(ThrowawayDatabase db) { - JsonDocument.load(db) - String json = db.conn.jsonByContains(TEST_TABLE, Map.of('value', 'purple')) + 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: @@ -189,10 +318,20 @@ final class JsonFunctions { assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") } - static void byContainsMatchOrdered(ThrowawayDatabase db) { + static void byContainsMatch(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonByContains(TEST_TABLE, Map.of('sub', Map.of('foo', 'green')), - List.of(Field.named('value'))) + 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, @@ -210,15 +349,39 @@ final class JsonFunctions { } } - static void byContainsNoMatch(ThrowawayDatabase db) { + static void byContainsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('[]', db.conn.jsonByContains(TEST_TABLE, Map.of('value', 'indigo')), - 'There should have been no documents returned') + checkByContainsMatchOrdered(db.conn.jsonByContains(TEST_TABLE, Map.of('sub', Map.of('foo', 'green')), + List.of(Field.named('value')))) } - static void byJsonPathMatch(ThrowawayDatabase db) { + static void writeByContainsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)') + 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: @@ -233,9 +396,20 @@ final class JsonFunctions { assertTrue(json.endsWith(']'), "JSON should end with ']' ($json)") } - static void byJsonPathMatchOrdered(ThrowawayDatabase db) { + static void byJsonPathMatch(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)', List.of(Field.named('id'))) + 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, @@ -253,15 +427,38 @@ final class JsonFunctions { } } - static void byJsonPathNoMatch(ThrowawayDatabase db) { + static void byJsonPathMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('[]', db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)'), - 'There should have been no documents returned') + checkByJsonPathMatchOrdered(db.conn.jsonByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)', + List.of(Field.named('id')))) } - static void firstByFieldsMatchOne(ThrowawayDatabase db) { + static void writeByJsonPathMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('value', 'another'))) + 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') @@ -272,13 +469,24 @@ final class JsonFunctions { } } - static void firstByFieldsMatchMany(ThrowawayDatabase db) { + static void firstByFieldsMatchOne(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('sub.foo', 'green'))) + 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)") + "Expected document 'two' or 'four' ($json)") break case Dialect.POSTGRESQL: assertTrue(json.contains(docId('two')) || json.contains(docId('four')), @@ -287,10 +495,20 @@ final class JsonFunctions { } } - static void firstByFieldsMatchOrdered(ThrowawayDatabase db) { + static void firstByFieldsMatchMany(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('sub.foo', 'green')), null, - List.of(Field.named('n:numValue DESC'))) + 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') @@ -301,15 +519,39 @@ final class JsonFunctions { } } - static void firstByFieldsNoMatch(ThrowawayDatabase db) { + static void firstByFieldsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('{}', db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('value', 'absent'))), - 'There should have been no document returned') + checkFirstByFieldsMatchOrdered(db.conn.jsonFirstByFields(TEST_TABLE, List.of(Field.equal('sub.foo', 'green')), + null, List.of(Field.named('n:numValue DESC')))) } - static void firstByContainsMatchOne(ThrowawayDatabase db) { + static void writeFirstByFieldsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'FIRST!')) + 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') @@ -320,25 +562,46 @@ final class JsonFunctions { } } - static void firstByContainsMatchMany(ThrowawayDatabase db) { + static void firstByContainsMatchOne(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'purple')) + 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)") + "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)") + "Expected document 'four' or 'five' ($json)") break } } - static void firstByContainsMatchOrdered(ThrowawayDatabase db) { + static void firstByContainsMatchMany(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'purple'), - List.of(Field.named('sub.bar NULLS FIRST'))) + 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') @@ -349,15 +612,39 @@ final class JsonFunctions { } } - static void firstByContainsNoMatch(ThrowawayDatabase db) { + static void firstByContainsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - assertEquals('{}', db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'indigo')), - 'There should have been no document returned') + checkFirstByContainsMatchOrdered(db.conn.jsonFirstByContains(TEST_TABLE, Map.of('value', 'purple'), + List.of(Field.named('sub.bar NULLS FIRST')))) } - static void firstByJsonPathMatchOne(ThrowawayDatabase db) { + static void writeFirstByContainsMatchOrdered(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ == 10)') + 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') @@ -368,24 +655,46 @@ final class JsonFunctions { } } - static void firstByJsonPathMatchMany(ThrowawayDatabase db) { + static void firstByJsonPathMatchOne(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)') + 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)") + "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)") + "Expected document 'four' or 'five' ($json)") break } } - static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) { + static void firstByJsonPathMatchMany(ThrowawayDatabase db) { JsonDocument.load(db) - String json = db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 10)', List.of(Field.named('id DESC'))) + 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') @@ -396,9 +705,34 @@ final class JsonFunctions { } } + 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) - assertEquals('{}', db.conn.jsonFirstByJsonPath(TEST_TABLE, '$.numValue ? (@ > 100)'), - 'There should have been no document returned') + 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/PostgreSQLCustomIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/PostgreSQLCustomIT.groovy index e052725..bca62d8 100644 --- 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 @@ -39,6 +39,24 @@ final class PostgreSQLCustomIT { 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() { 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 index d0b61fa..690e401 100644 --- 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 @@ -182,4 +182,178 @@ final class PostgreSQLJsonIT { 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/SQLiteCustomIT.groovy b/src/groovy/src/test/groovy/solutions/bitbadger/documents/groovy/tests/integration/SQLiteCustomIT.groovy index 05d9f24..517e011 100644 --- 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 @@ -21,6 +21,42 @@ final class SQLiteCustomIT { 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() { 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 index 222bb86..3e81c59 100644 --- 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 @@ -133,4 +133,126 @@ final class SQLiteJsonIT { 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 } + } + } } -- 2.47.2 From 40f8bded81887f5a3dd114554beb90663fb5f897 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 31 Mar 2025 23:04:51 -0400 Subject: [PATCH 81/88] Add JSON writer to Scala Custom --- src/scala/src/main/scala/Custom.scala | 55 +++++++++++++++++++ src/scala/src/main/scala/Results.scala | 37 +++++++++++-- .../src/main/scala/extensions/package.scala | 27 +++++++++ .../scala/integration/CustomFunctions.scala | 33 +++++++++-- .../integration/PostgreSQLCustomIT.scala | 15 +++++ .../scala/integration/SQLiteCustomIT.scala | 15 +++++ 6 files changed, 172 insertions(+), 10 deletions(-) diff --git a/src/scala/src/main/scala/Custom.scala b/src/scala/src/main/scala/Custom.scala index 110562f..63704b4 100644 --- a/src/scala/src/main/scala/Custom.scala +++ b/src/scala/src/main/scala/Custom.scala @@ -2,6 +2,7 @@ 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 @@ -107,6 +108,60 @@ object Custom: 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 * diff --git a/src/scala/src/main/scala/Results.scala b/src/scala/src/main/scala/Results.scala index 6845076..3b10978 100644 --- a/src/scala/src/main/scala/Results.scala +++ b/src/scala/src/main/scala/Results.scala @@ -3,6 +3,7 @@ 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 @@ -47,9 +48,8 @@ object Results: try val buffer = ListBuffer[Doc]() Using(stmt.executeQuery()) { rs => - while (rs.next()) { + while rs.next() do buffer.append(mapFunc(rs, tag)) - } } buffer.toList catch @@ -107,12 +107,37 @@ object Results: try val results = StringBuilder("[") Using(stmt.executeQuery()) { rs => - while (rs.next()) { + 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) + 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 index e51dd15..f9fab6b 100644 --- a/src/scala/src/main/scala/extensions/package.scala +++ b/src/scala/src/main/scala/extensions/package.scala @@ -3,6 +3,7 @@ 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 @@ -58,6 +59,32 @@ extension (conn: Connection) 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 * diff --git a/src/scala/src/test/scala/integration/CustomFunctions.scala b/src/scala/src/test/scala/integration/CustomFunctions.scala index 32748fe..b4acc2d 100644 --- a/src/scala/src/test/scala/integration/CustomFunctions.scala +++ b/src/scala/src/test/scala/integration/CustomFunctions.scala @@ -7,6 +7,7 @@ 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: @@ -29,19 +30,43 @@ object CustomFunctions: def jsonArraySingle(db: ThrowawayDatabase): Unit = db.conn.insert(TEST_TABLE, ArrayDocument("one", "2" :: "3" :: Nil)) - assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), + 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\"]}]"), + 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") diff --git a/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala index 9c0c131..e1a6cc1 100644 --- a/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala +++ b/src/scala/src/test/scala/integration/PostgreSQLCustomIT.scala @@ -32,6 +32,21 @@ class PostgreSQLCustomIT: 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 = diff --git a/src/scala/src/test/scala/integration/SQLiteCustomIT.scala b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala index dfbc0ca..e93bdb3 100644 --- a/src/scala/src/test/scala/integration/SQLiteCustomIT.scala +++ b/src/scala/src/test/scala/integration/SQLiteCustomIT.scala @@ -32,6 +32,21 @@ class SQLiteCustomIT: 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 = -- 2.47.2 From 515dbb2b1d96d4637c79ff10c765d39b1575b62c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 1 Apr 2025 14:01:56 -0400 Subject: [PATCH 82/88] Tweak Scala code --- src/scala/src/main/scala/Parameters.scala | 7 +++---- src/scala/src/test/scala/ConfigurationTest.scala | 5 ++--- src/scala/src/test/scala/DialectTest.scala | 6 ++---- src/scala/src/test/scala/DocumentQueryTest.scala | 5 ++--- .../scala/integration/DocumentFunctions.scala | 15 ++++++--------- .../test/scala/integration/FindFunctions.scala | 5 ++--- .../test/scala/integration/JsonFunctions.scala | 5 ++--- 7 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/scala/src/main/scala/Parameters.scala b/src/scala/src/main/scala/Parameters.scala index 2499863..b42c546 100644 --- a/src/scala/src/main/scala/Parameters.scala +++ b/src/scala/src/main/scala/Parameters.scala @@ -23,12 +23,11 @@ object Parameters: 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)) { + if (it.getParameterName == null || it.getParameterName.isEmpty) + && !(Op.EXISTS :: Op.NOT_EXISTS :: Nil).contains(it.getComparison.getOp) then it.withParameterName(name.derive(null)) - } else { + else it - } } /** diff --git a/src/scala/src/test/scala/ConfigurationTest.scala b/src/scala/src/test/scala/ConfigurationTest.scala index f325ff9..8060f85 100644 --- a/src/scala/src/test/scala/ConfigurationTest.scala +++ b/src/scala/src/test/scala/ConfigurationTest.scala @@ -25,10 +25,9 @@ class ConfigurationTest: @Test @DisplayName("Dialect is derived from connection string") def dialectIsDerived(): Unit = - try { + try assertThrows(classOf[DocumentException], () => Configuration.dialect()) Configuration.setConnectionString("jdbc:postgresql:db") assertEquals(Dialect.POSTGRESQL, Configuration.dialect()) - } finally { + finally Configuration.setConnectionString(null) - } diff --git a/src/scala/src/test/scala/DialectTest.scala b/src/scala/src/test/scala/DialectTest.scala index 27f86da..9940ef2 100644 --- a/src/scala/src/test/scala/DialectTest.scala +++ b/src/scala/src/test/scala/DialectTest.scala @@ -22,13 +22,11 @@ class DialectTest: @Test @DisplayName("deriveFromConnectionString fails when the connection string is unknown") def deriveFailsWhenUnknown(): Unit = - try { + try Dialect.deriveFromConnectionString("SQL Server") fail("Dialect derivation should have failed") - } catch { + 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/DocumentQueryTest.scala b/src/scala/src/test/scala/DocumentQueryTest.scala index 57a7cb8..939a5d5 100644 --- a/src/scala/src/test/scala/DocumentQueryTest.scala +++ b/src/scala/src/test/scala/DocumentQueryTest.scala @@ -64,7 +64,7 @@ class DocumentQueryTest: @Test @DisplayName("insert generates auto random string | PostgreSQL") def insertAutoRandomPostgres(): Unit = - try { + try ForceDialect.postgres() Configuration.idStringLength = 8 val query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING) @@ -74,9 +74,8 @@ class DocumentQueryTest: assertEquals(8, query.replace(s"INSERT INTO $TEST_TABLE VALUES (:data::jsonb || '{\"id\":\"", "") .replace("\"}')", "").length, "Random string length incorrect") - } finally { + finally Configuration.idStringLength = 16 - } @Test @DisplayName("insert generates auto random string | SQLite") diff --git a/src/scala/src/test/scala/integration/DocumentFunctions.scala b/src/scala/src/test/scala/integration/DocumentFunctions.scala index 210d6de..33387e3 100644 --- a/src/scala/src/test/scala/integration/DocumentFunctions.scala +++ b/src/scala/src/test/scala/integration/DocumentFunctions.scala @@ -23,7 +23,7 @@ object DocumentFunctions: "Inserting a document with a duplicate key should have thrown an exception") def insertNumAutoId(db: ThrowawayDatabase): Unit = - try { + try Configuration.autoIdStrategy = AutoId.NUMBER Configuration.idField = "key" assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") @@ -36,13 +36,12 @@ object DocumentFunctions: 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 { + finally Configuration.autoIdStrategy = AutoId.DISABLED Configuration.idField = "id" - } def insertUUIDAutoId(db: ThrowawayDatabase): Unit = - try { + try Configuration.autoIdStrategy = AutoId.UUID assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") @@ -51,12 +50,11 @@ object DocumentFunctions: 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 { + finally Configuration.autoIdStrategy = AutoId.DISABLED - } def insertStringAutoId(db: ThrowawayDatabase): Unit = - try { + try Configuration.autoIdStrategy = AutoId.RANDOM_STRING assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") @@ -69,10 +67,9 @@ object DocumentFunctions: 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 { + finally Configuration.autoIdStrategy = AutoId.DISABLED Configuration.idStringLength = 16 - } def saveMatch(db: ThrowawayDatabase): Unit = JsonDocument.load(db) diff --git a/src/scala/src/test/scala/integration/FindFunctions.scala b/src/scala/src/test/scala/integration/FindFunctions.scala index a5680a1..78e0d37 100644 --- a/src/scala/src/test/scala/integration/FindFunctions.scala +++ b/src/scala/src/test/scala/integration/FindFunctions.scala @@ -47,13 +47,12 @@ object FindFunctions: def byIdNumber(db: ThrowawayDatabase): Unit = Configuration.idField = "key" - try { + 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 { + finally Configuration.idField = "id" - } def byIdNotFound(db: ThrowawayDatabase): Unit = JsonDocument.load(db) diff --git a/src/scala/src/test/scala/integration/JsonFunctions.scala b/src/scala/src/test/scala/integration/JsonFunctions.scala index 938b064..609814c 100644 --- a/src/scala/src/test/scala/integration/JsonFunctions.scala +++ b/src/scala/src/test/scala/integration/JsonFunctions.scala @@ -71,13 +71,12 @@ object JsonFunctions: def byIdNumber (db: ThrowawayDatabase): Unit = Configuration.idField = "key" - try { + try db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) assertEquals(maybeJsonB("""{"key":18,"text":"howdy"}"""), db.conn.jsonById(TEST_TABLE, 18), "The document should have been found by numeric ID") - } finally { + finally Configuration.idField = "id" - } def byIdNotFound (db: ThrowawayDatabase): Unit = JsonDocument.load(db) -- 2.47.2 From ae39a585fc1b2873301f418d174e9d79bbaa9e67 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 1 Apr 2025 20:51:10 -0400 Subject: [PATCH 83/88] Add Scala Json write* functions --- src/scala/src/main/scala/Json.scala | 351 +++++++++++++++++- .../src/main/scala/extensions/package.scala | 106 ++++++ 2 files changed, 455 insertions(+), 2 deletions(-) diff --git a/src/scala/src/main/scala/Json.scala b/src/scala/src/main/scala/Json.scala index d38c968..9c2c4e5 100644 --- a/src/scala/src/main/scala/Json.scala +++ b/src/scala/src/main/scala/Json.scala @@ -3,6 +3,7 @@ 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.* @@ -21,7 +22,7 @@ object Json: CoreJson.all(tableName, orderBy.asJava, conn) /** - * Retrieve all documents in the given table, ordering results by the optional given fields + * 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 @@ -32,7 +33,7 @@ object Json: CoreJson.all(tableName, conn) /** - * Retrieve all documents in the given table (creates connection) + * 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) @@ -42,6 +43,41 @@ object Json: 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 * @@ -65,6 +101,29 @@ object Json: 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 * @@ -120,6 +179,64 @@ object Json: 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) * @@ -158,6 +275,46 @@ object Json: 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) * @@ -196,6 +353,46 @@ object Json: 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 * @@ -264,6 +461,76 @@ object Json: 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) * @@ -302,6 +569,46 @@ object Json: 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) * @@ -339,3 +646,43 @@ object Json: */ 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/extensions/package.scala b/src/scala/src/main/scala/extensions/package.scala index f9fab6b..68034e1 100644 --- a/src/scala/src/main/scala/extensions/package.scala +++ b/src/scala/src/main/scala/extensions/package.scala @@ -530,6 +530,112 @@ extension (conn: 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 ~~~ /** -- 2.47.2 From a2b5d8f3280d704c18c6acfae0aae163601ffbb4 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 1 Apr 2025 22:45:05 -0400 Subject: [PATCH 84/88] Add Scala Json write tests --- .../scala/integration/JsonFunctions.scala | 427 ++++++++++++++---- .../scala/integration/PostgreSQLJsonIT.scala | 145 ++++++ .../test/scala/integration/SQLiteJsonIT.scala | 99 ++++ 3 files changed, 594 insertions(+), 77 deletions(-) diff --git a/src/scala/src/test/scala/integration/JsonFunctions.scala b/src/scala/src/test/scala/integration/JsonFunctions.scala index 609814c..7a4c3e8 100644 --- a/src/scala/src/test/scala/integration/JsonFunctions.scala +++ b/src/scala/src/test/scala/integration/JsonFunctions.scala @@ -5,6 +5,7 @@ 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.* /** @@ -39,9 +40,7 @@ object JsonFunctions: private def docId(id: String): String = maybeJsonB(s"""{"id":"$id"""") - def allDefault(db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - val json = db.conn.jsonAll(TEST_TABLE) + private def checkAllDefault(json: String): Unit = assertTrue(json.startsWith("["), s"JSON should start with '[' ($json)") Configuration.dialect() match case Dialect.SQLITE => @@ -50,7 +49,6 @@ object JsonFunctions: 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)") @@ -59,33 +57,82 @@ object JsonFunctions: assertTrue(json.contains(docId("five")), s"Document 'five' not found in JSON ($json)") assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") - def allEmpty(db: ThrowawayDatabase): Unit = - assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") - - def byIdString (db: ThrowawayDatabase): Unit = + def allDefault(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonById(TEST_TABLE, "two") + 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 byIdNumber (db: ThrowawayDatabase): Unit = + 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")) - assertEquals(maybeJsonB("""{"key":18,"text":"howdy"}"""), db.conn.jsonById(TEST_TABLE, 18), - "The document should have been found by numeric ID") + checkByIdNumber(db.conn.jsonById(TEST_TABLE, 18)) finally Configuration.idField = "id" - def byIdNotFound (db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + 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" - def byFieldsMatch (db: ThrowawayDatabase): Unit = + private def checkByIdNotFound(json: String): Unit = + assertEquals("{}", json, "There should have been no document returned") + + def byIdNotFound(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonByFields(TEST_TABLE, - Field.any("value", ("blue" :: "purple" :: Nil).asJava) :: Field.exists("sub") :: Nil, Some(FieldMatch.ALL)) + 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") @@ -94,9 +141,20 @@ object JsonFunctions: assertTrue(json.contains(docId("four")), s"The incorrect document was returned ($json)") assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") - def byFieldsMatchOrdered (db: ThrowawayDatabase): Unit = + def byFieldsMatch(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonByFields(TEST_TABLE, Field.equal("value", "purple") :: Nil, None, Field.named("id") :: Nil) + 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") @@ -109,9 +167,19 @@ object JsonFunctions: assertTrue(fiveIdx < fourIdx, s"Document 'five' should have been before 'four' ($json)") assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") - def byFieldsMatchNumIn (db: ThrowawayDatabase): Unit = + def byFieldsMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonByFields(TEST_TABLE, Field.any("numValue", (2 :: 4 :: 6 :: 8 :: Nil).asJava) :: Nil) + 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") @@ -120,28 +188,66 @@ object JsonFunctions: assertTrue(json.contains(docId("three")), s"The incorrect document was returned ($json)") assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") - def byFieldsNoMatch (db: ThrowawayDatabase): Unit = + def byFieldsMatchNumIn(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - assertEquals("[]", db.conn.jsonByFields(TEST_TABLE, Field.greater("numValue", 100) :: Nil), - "There should have been no documents returned") + checkByFieldsMatchNumIn(db.conn.jsonByFields(TEST_TABLE, + Field.any("numValue", (2 :: 4 :: 6 :: 8 :: Nil).asJava) :: Nil)) - def byFieldsMatchInArray (db: ThrowawayDatabase): Unit = - ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } - val json = db.conn.jsonByFields(TEST_TABLE, Field.inArray("values", TEST_TABLE, ("c" :: 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 byFieldsNoMatchInArray (db: ThrowawayDatabase): Unit = + def byFieldsMatchInArray(db: ThrowawayDatabase): Unit = ArrayDocument.testDocuments.foreach { doc => db.conn.insert(TEST_TABLE, doc) } - assertEquals("[]", - db.conn.jsonByFields(TEST_TABLE, Field.inArray("values", TEST_TABLE, ("j" :: Nil).asJava) :: Nil), - "There should have been no documents returned") + checkByFieldsMatchInArray(db.conn.jsonByFields(TEST_TABLE, + Field.inArray("values", TEST_TABLE, ("c" :: Nil).asJava) :: Nil)) - def byContainsMatch (db: ThrowawayDatabase): Unit = - JsonDocument.load(db) - val json = db.conn.jsonByContains(TEST_TABLE, Map.Map1("value", "purple")) + 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 => @@ -152,10 +258,18 @@ object JsonFunctions: assertTrue(json.contains(docId("five")), s"Document 'five' not found ($json)") assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") - def byContainsMatchOrdered (db: ThrowawayDatabase): Unit = + def byContainsMatch(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonByContains(TEST_TABLE, Map.Map1("sub", Map.Map1("foo", "green")), - Field.named("value") :: Nil) + 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") @@ -168,14 +282,34 @@ object JsonFunctions: assertTrue(twoIdx < fourIdx, s"Document 'two' should have been before 'four' ($json)") assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") - def byContainsNoMatch (db: ThrowawayDatabase): Unit = + def byContainsMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - assertEquals("[]", db.conn.jsonByContains(TEST_TABLE, Map.Map1("value", "indigo")), - "There should have been no documents returned") + checkByContainsMatchOrdered(db.conn.jsonByContains(TEST_TABLE, Map.Map1("sub", Map.Map1("foo", "green")), + Field.named("value") :: Nil)) - def byJsonPathMatch (db: ThrowawayDatabase): Unit = + def writeByContainsMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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 => @@ -186,9 +320,18 @@ object JsonFunctions: assertTrue(json.contains(docId("five")), s"Document 'five' not found ($json)") assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") - def byJsonPathMatchOrdered (db: ThrowawayDatabase): Unit = + def byJsonPathMatch(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", Field.named("id") :: Nil) + 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") @@ -201,21 +344,48 @@ object JsonFunctions: assertTrue(fiveIdx < fourIdx, s"Document 'five' should have been before 'four' ($json)") assertTrue(json.endsWith("]"), s"JSON should end with ']' ($json)") - def byJsonPathNoMatch (db: ThrowawayDatabase): Unit = + def byJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - assertEquals("[]", db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no documents returned") + checkByJsonPathMatchOrdered(db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", Field.named("id") :: Nil)) - def firstByFieldsMatchOne (db: ThrowawayDatabase): Unit = + def writeByJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("value", "another") :: Nil) + 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 firstByFieldsMatchMany (db: ThrowawayDatabase): Unit = + def firstByFieldsMatchOne(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("sub.foo", "green") :: Nil) + 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), @@ -224,29 +394,65 @@ object JsonFunctions: assertTrue(json.contains(docId("two")) || json.contains(docId("four")), s"Expected document 'two' or 'four' ($json)") - def firstByFieldsMatchOrdered (db: ThrowawayDatabase): Unit = + def firstByFieldsMatchMany(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("sub.foo", "green") :: Nil, None, - Field.named("n:numValue DESC") :: Nil) + 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 firstByFieldsNoMatch (db: ThrowawayDatabase): Unit = + def firstByFieldsMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - assertEquals("{}", db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("value", "absent") :: Nil), - "There should have been no document returned") + checkFirstByFieldsMatchOrdered(db.conn.jsonFirstByFields(TEST_TABLE, Field.equal("sub.foo", "green") :: Nil, None, + Field.named("n:numValue DESC") :: Nil)) - def firstByContainsMatchOne (db: ThrowawayDatabase): Unit = + def writeFirstByFieldsMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "FIRST!")) + 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 firstByContainsMatchMany (db: ThrowawayDatabase): Unit = + def firstByContainsMatchOne(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "purple")) + 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), @@ -255,29 +461,66 @@ object JsonFunctions: assertTrue(json.contains(docId("four")) || json.contains(docId("five")), s"Expected document 'four' or 'five' ($json)") - def firstByContainsMatchOrdered (db: ThrowawayDatabase): Unit = + def firstByContainsMatchMany(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "purple"), - Field.named("sub.bar NULLS FIRST") :: Nil) + 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 firstByContainsNoMatch (db: ThrowawayDatabase): Unit = + def firstByContainsMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - assertEquals("{}", db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "indigo")), - "There should have been no document returned") + checkFirstByContainsMatchOrdered(db.conn.jsonFirstByContains(TEST_TABLE, Map.Map1("value", "purple"), + Field.named("sub.bar NULLS FIRST") :: Nil)) - def firstByJsonPathMatchOne (db: ThrowawayDatabase): Unit = + def writeFirstByContainsMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + 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 firstByJsonPathMatchMany (db: ThrowawayDatabase): Unit = + def firstByJsonPathMatchOne(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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), @@ -286,14 +529,44 @@ object JsonFunctions: assertTrue(json.contains(docId("four")) || json.contains(docId("five")), s"Expected document 'four' or 'five' ($json)") - def firstByJsonPathMatchOrdered (db: ThrowawayDatabase): Unit = + def firstByJsonPathMatchMany(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", Field.named("id DESC") :: Nil) + 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 firstByJsonPathNoMatch (db: ThrowawayDatabase): Unit = + def firstByJsonPathMatchOrdered(db: ThrowawayDatabase): Unit = JsonDocument.load(db) - assertEquals("{}", db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no document returned") + 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/PostgreSQLJsonIT.scala b/src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala index cc7f04c..553bbde 100644 --- a/src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala +++ b/src/scala/src/test/scala/integration/PostgreSQLJsonIT.scala @@ -154,3 +154,148 @@ class PostgreSQLJsonIT: @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/SQLiteJsonIT.scala b/src/scala/src/test/scala/integration/SQLiteJsonIT.scala index b58a35e..1de44ef 100644 --- a/src/scala/src/test/scala/integration/SQLiteJsonIT.scala +++ b/src/scala/src/test/scala/integration/SQLiteJsonIT.scala @@ -110,3 +110,102 @@ class SQLiteJsonIT: 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)) + } -- 2.47.2 From a2fae8b679bcf445c357bf278a238565e29ea998 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 2 Apr 2025 19:56:46 -0400 Subject: [PATCH 85/88] WIP on KotlinX Json functions --- src/core/src/main/kotlin/java/Custom.kt | 2 - src/kotlinx/src/main/kotlin/Custom.kt | 35 +++++++++++++++++ .../kotlin/integration/CustomFunctions.kt | 39 +++++++++++++++++++ .../test/kotlin/integration/JsonFunctions.kt | 19 +++++++-- .../kotlin/integration/PostgreSQLCustomIT.kt | 15 +++++++ .../test/kotlin/integration/SQLiteCustomIT.kt | 15 +++++++ 6 files changed, 120 insertions(+), 5 deletions(-) diff --git a/src/core/src/main/kotlin/java/Custom.kt b/src/core/src/main/kotlin/java/Custom.kt index 025b8b9..2220ece 100644 --- a/src/core/src/main/kotlin/java/Custom.kt +++ b/src/core/src/main/kotlin/java/Custom.kt @@ -96,7 +96,6 @@ object Custom { * @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 */ @Throws(DocumentException::class) @@ -116,7 +115,6 @@ object Custom { * @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) diff --git a/src/kotlinx/src/main/kotlin/Custom.kt b/src/kotlinx/src/main/kotlin/Custom.kt index 3b9a4be..3b15c0e 100644 --- a/src/kotlinx/src/main/kotlin/Custom.kt +++ b/src/kotlinx/src/main/kotlin/Custom.kt @@ -2,6 +2,7 @@ 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 @@ -70,6 +71,40 @@ object Custom { 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 * diff --git a/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt index c32d52f..96d41f3 100644 --- a/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt +++ b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt @@ -1,12 +1,17 @@ package solutions.bitbadger.documents.kotlinx.tests.integration import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.java.extensions.countAll +import solutions.bitbadger.documents.java.extensions.insert +import solutions.bitbadger.documents.java.extensions.writeCustomJsonArray 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 @@ -59,6 +64,40 @@ object CustomFunctions { ) } + 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), diff --git a/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt index be8e339..34d6779 100644 --- a/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt +++ b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt @@ -6,6 +6,8 @@ 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 @@ -42,9 +44,7 @@ object JsonFunctions { private fun docId(id: String) = maybeJsonB("{\"id\":\"$id\"") - fun allDefault(db: ThrowawayDatabase) { - JsonDocument.load(db) - val json = db.conn.jsonAll(TEST_TABLE) + private fun checkAllDefault(json: String) { assertTrue(json.startsWith("["), "JSON should start with '[' ($json)") when (Configuration.dialect()) { Dialect.SQLITE -> { @@ -65,6 +65,19 @@ object JsonFunctions { 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()) +// } + fun allEmpty(db: ThrowawayDatabase) = assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") diff --git a/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt index 7407c71..2d2f271 100644 --- a/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLCustomIT.kt @@ -35,6 +35,21 @@ class PostgreSQLCustomIT { 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() = diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt index af2e850..5c6194d 100644 --- a/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteCustomIT.kt @@ -34,6 +34,21 @@ class SQLiteCustomIT { 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() = -- 2.47.2 From f2dd740933b9b495696e3ba043b4f96a3a2a520e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 2 Apr 2025 23:23:34 -0400 Subject: [PATCH 86/88] WIP on KotlinX Json tests --- src/core/src/main/kotlin/java/Results.kt | 2 +- src/kotlinx/src/main/kotlin/Json.kt | 383 +++++++++++++ .../src/main/kotlin/extensions/Connection.kt | 164 +++++- .../test/kotlin/integration/JsonFunctions.kt | 515 ++++++++++++++---- .../kotlin/integration/PostgreSQLJsonIT.kt | 147 ++++- .../test/kotlin/integration/SQLiteJsonIT.kt | 101 +++- 6 files changed, 1194 insertions(+), 118 deletions(-) diff --git a/src/core/src/main/kotlin/java/Results.kt b/src/core/src/main/kotlin/java/Results.kt index 0e98fc3..3f49229 100644 --- a/src/core/src/main/kotlin/java/Results.kt +++ b/src/core/src/main/kotlin/java/Results.kt @@ -100,7 +100,7 @@ object Results { */ @JvmStatic fun jsonFromDocument(field: String, rs: ResultSet) = - rs.getString(field) + rs.getString(field) ?: "{}" /** * Retrieve the JSON text of a document, specifying the field in which the document is found diff --git a/src/kotlinx/src/main/kotlin/Json.kt b/src/kotlinx/src/main/kotlin/Json.kt index a8d14bd..75a6de7 100644 --- a/src/kotlinx/src/main/kotlin/Json.kt +++ b/src/kotlinx/src/main/kotlin/Json.kt @@ -3,6 +3,7 @@ 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 @@ -45,6 +46,41 @@ object Json { 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 * @@ -68,6 +104,29 @@ object Json { 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 * @@ -117,6 +176,63 @@ object Json { 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) * @@ -167,6 +283,65 @@ object Json { 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) * @@ -205,6 +380,50 @@ object Json { 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 * @@ -258,6 +477,63 @@ object Json { 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) * @@ -308,6 +584,65 @@ object Json { 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) * @@ -345,4 +680,52 @@ object Json { */ 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/extensions/Connection.kt b/src/kotlinx/src/main/kotlin/extensions/Connection.kt index 090a0c1..e437e21 100644 --- a/src/kotlinx/src/main/kotlin/extensions/Connection.kt +++ b/src/kotlinx/src/main/kotlin/extensions/Connection.kt @@ -2,6 +2,7 @@ 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 @@ -32,7 +33,23 @@ fun Connection.customJsonArray( query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String -) = Custom.jsonArray(query, parameters, mapFunc) +) = 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 @@ -59,7 +76,7 @@ fun Connection.customJsonSingle( query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> String -) = Custom.jsonSingle(query, parameters, mapFunc) +) = Custom.jsonSingle(query, parameters, this, mapFunc) /** * Execute a query that returns no results @@ -448,8 +465,7 @@ inline fun Connection.jsonFirstByContains( ) = Json.firstByContains(tableName, criteria, orderBy, this) /** - * Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only; creates - * connection) + * 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 @@ -460,6 +476,134 @@ inline fun Connection.jsonFirstByContains( 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 ~~~ /** @@ -485,8 +629,7 @@ inline fun Connection.patchByFields( fields: Collection>, patch: TPatch, howMatched: FieldMatch? = null -) = - Patch.byFields(tableName, fields, patch, howMatched, this) +) = Patch.byFields(tableName, fields, patch, howMatched, this) /** * Patch documents using a JSON containment query (PostgreSQL only) @@ -500,8 +643,7 @@ inline fun Connection.patchByContains( tableName: String, criteria: TContains, patch: TPatch -) = - Patch.byContains(tableName, criteria, patch, this) +) = Patch.byContains(tableName, criteria, patch, this) /** * Patch documents using a JSON Path match query (PostgreSQL only) @@ -539,8 +681,7 @@ fun Connection.removeFieldsByFields( fields: Collection>, toRemove: Collection, howMatched: FieldMatch? = null -) = - RemoveFields.byFields(tableName, fields, toRemove, howMatched, this) +) = RemoveFields.byFields(tableName, fields, toRemove, howMatched, this) /** * Remove fields from documents using a JSON containment query (PostgreSQL only) @@ -554,8 +695,7 @@ inline fun Connection.removeFieldsByContains( tableName: String, criteria: TContains, toRemove: Collection -) = - RemoveFields.byContains(tableName, criteria, toRemove, this) +) = RemoveFields.byContains(tableName, criteria, toRemove, this) /** * Remove fields from documents using a JSON Path match query (PostgreSQL only) diff --git a/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt index 34d6779..9aaecf4 100644 --- a/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt +++ b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt @@ -70,49 +70,93 @@ object JsonFunctions { 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()) -// } + 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) = - assertEquals("[]", db.conn.jsonAll(TEST_TABLE), "There should have been no documents returned") + checkAllEmpty(db.conn.jsonAll(TEST_TABLE)) - fun byIdString(db: ThrowawayDatabase) { - JsonDocument.load(db) - val json = db.conn.jsonById(TEST_TABLE, "two") + 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")) - assertEquals( - maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), db.conn.jsonById(TEST_TABLE, 18), - "The document should have been found by numeric ID" - ) + checkByIdString(db.conn.jsonById(TEST_TABLE, 18)) } finally { Configuration.idField = "id" } } - fun byIdNotFound(db: ThrowawayDatabase) { - JsonDocument.load(db) - assertEquals("{}", db.conn.jsonById(TEST_TABLE, "x"), "There should have been no document returned") + 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" + } } - fun byFieldsMatch(db: ThrowawayDatabase) { + private fun checkByIdNotFound(json: String) = + assertEquals("{}", json, "There should have been no document returned") + + fun byIdNotFound(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByFields( - TEST_TABLE, listOf(Field.any("value", listOf("blue", "purple")), Field.exists("sub")), FieldMatch.ALL - ) + 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 -> { @@ -121,13 +165,30 @@ object JsonFunctions { 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 byFieldsMatchOrdered(db: ThrowawayDatabase) { + fun writeByFieldsMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByFields( - TEST_TABLE, listOf(Field.equal("value", "purple")), orderBy = listOf(Field.named("id")) + 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" @@ -143,11 +204,27 @@ object JsonFunctions { 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 byFieldsMatchNumIn(db: ThrowawayDatabase) { + fun writeByFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.any("numValue", listOf(2, 4, 6, 8)))) + 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 -> { @@ -156,37 +233,77 @@ object JsonFunctions { 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) - assertEquals( - "[]", db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100))), - "There should have been no documents returned" - ) + checkByFieldsNoMatch(db.conn.jsonByFields(TEST_TABLE, listOf(Field.greater("numValue", 100)))) } - fun byFieldsMatchInArray(db: ThrowawayDatabase) { - ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } - val json = db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) + 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 byFieldsNoMatchInArray(db: ThrowawayDatabase) { + fun byFieldsMatchInArray(db: ThrowawayDatabase) { ArrayDocument.testDocuments.forEach { db.conn.insert(TEST_TABLE, it) } - assertEquals( - "[]", db.conn.jsonByFields( - TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("j"))) - ), "There should have been no documents returned" + checkByFieldsMatchInArray( + db.conn.jsonByFields(TEST_TABLE, listOf(Field.inArray("values", TEST_TABLE, listOf("c")))) ) } - fun byContainsMatch(db: ThrowawayDatabase) { - JsonDocument.load(db) - val json = db.conn.jsonByContains>(TEST_TABLE, mapOf("value" to "purple")) + 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 -> { @@ -201,11 +318,20 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } - fun byContainsMatchOrdered(db: ThrowawayDatabase) { + fun byContainsMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByContains>>( - TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) - ) + 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" @@ -220,20 +346,43 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } } - } - fun byContainsNoMatch(db: ThrowawayDatabase) { + fun byContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "[]", - db.conn.jsonByContains>(TEST_TABLE, mapOf("value" to "indigo")), - "There should have been no documents returned" + checkByContainsMatchOrdered( + db.conn.jsonByContains>>( + TEST_TABLE, mapOf("sub" to mapOf("foo" to "green")), listOf(Field.named("value")) + ) ) } - fun byJsonPathMatch(db: ThrowawayDatabase) { + fun writeByContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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 -> { @@ -248,9 +397,20 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } - fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { + fun byJsonPathMatch(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) + 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" @@ -266,29 +426,58 @@ object JsonFunctions { assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)") } } - } - fun byJsonPathNoMatch(db: ThrowawayDatabase) { + fun byJsonPathMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "[]", - db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no documents returned" + checkByJsonPathMatchOrdered( + db.conn.jsonByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id"))) ) } - fun firstByFieldsMatchOne(db: ThrowawayDatabase) { + fun writeByJsonPathMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "another"))) + 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 firstByFieldsMatchMany(db: ThrowawayDatabase) { + fun writeFirstByFieldsMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("sub.foo", "green"))) + 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), @@ -299,40 +488,85 @@ object JsonFunctions { "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 firstByFieldsMatchOrdered(db: ThrowawayDatabase) { + fun writeFirstByFieldsMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByFields( - TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) - ) + 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 firstByFieldsNoMatch(db: ThrowawayDatabase) { + fun firstByFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "{}", - db.conn.jsonFirstByFields(TEST_TABLE, listOf(Field.equal("value", "absent"))), - "There should have been no document returned" + checkFirstByFieldsMatchOrdered( + db.conn.jsonFirstByFields( + TEST_TABLE, listOf(Field.equal("sub.foo", "green")), orderBy = listOf(Field.named("n:numValue DESC")) + ) ) } - fun firstByContainsMatchOne(db: ThrowawayDatabase) { + fun writeFirstByFieldsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "FIRST!")) + 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 firstByContainsMatchMany(db: ThrowawayDatabase) { + fun writeFirstByContainsMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "purple")) + 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), @@ -343,40 +577,85 @@ object JsonFunctions { "Expected document 'four' or 'five' ($json)" ) } + + fun firstByContainsMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByContainsMatchMany( + db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "purple")) + ) } - fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { + fun writeFirstByContainsMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByContains>( - TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) - ) + 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 firstByContainsNoMatch(db: ThrowawayDatabase) { + fun firstByContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "{}", - db.conn.jsonFirstByContains>(TEST_TABLE, mapOf("value" to "indigo")), - "There should have been no document returned" + checkFirstByContainsMatchOrdered( + db.conn.jsonFirstByContains>( + TEST_TABLE, mapOf("value" to "purple"), listOf(Field.named("sub.bar NULLS FIRST")) + ) ) } - fun firstByJsonPathMatchOne(db: ThrowawayDatabase) { + fun writeFirstByContainsMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ == 10)") + 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 firstByJsonPathMatchMany(db: ThrowawayDatabase) { + fun writeFirstByJsonPathMatchOne(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)") + 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), @@ -387,24 +666,54 @@ object JsonFunctions { "Expected document 'four' or 'five' ($json)" ) } + + fun firstByJsonPathMatchMany(db: ThrowawayDatabase) { + JsonDocument.load(db) + checkFirstByJsonPathMatchMany(db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)")) } - fun firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { + fun writeFirstByJsonPathMatchMany(db: ThrowawayDatabase) { JsonDocument.load(db) - val json = db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) + 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 firstByJsonPathNoMatch(db: ThrowawayDatabase) { + fun firstByJsonPathMatchOrdered(db: ThrowawayDatabase) { JsonDocument.load(db) - assertEquals( - "{}", - db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 100)"), - "There should have been no document returned" + checkFirstByJsonPathMatchOrdered( + db.conn.jsonFirstByJsonPath(TEST_TABLE, "$.numValue ? (@ > 10)", listOf(Field.named("id DESC"))) ) } -} \ No newline at end of file + 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/PostgreSQLJsonIT.kt b/src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt index c17b2f4..b41be7c 100644 --- a/src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/PostgreSQLJsonIT.kt @@ -153,4 +153,149 @@ class PostgreSQLJsonIT { @DisplayName("firstByJsonPath returns null when no document matches") fun firstByJsonPathNoMatch() = PgDB().use(JsonFunctions::firstByJsonPathNoMatch) -} \ No newline at end of file + + @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/SQLiteJsonIT.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt index 91bcb67..b6e8b04 100644 --- a/src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteJsonIT.kt @@ -109,4 +109,103 @@ class SQLiteJsonIT { fun firstByJsonPathFails() { assertThrows { SQLiteDB().use(JsonFunctions::firstByJsonPathMatchOne) } } -} \ No newline at end of file + + @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) } + } +} -- 2.47.2 From e3a8c10eaeab1fb426f1c911dd55f53e546d295a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 3 Apr 2025 06:51:27 -0400 Subject: [PATCH 87/88] Finish KotlinX Json tests --- .../src/test/kotlin/integration/CustomFunctions.kt | 10 +++------- .../src/test/kotlin/integration/JsonFunctions.kt | 2 +- src/kotlinx/src/test/kotlin/integration/PgDB.kt | 13 +++---------- src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt | 5 +---- .../test/kotlin/integration/ThrowawayDatabase.kt | 10 +++++++--- 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt index 96d41f3..a98e90e 100644 --- a/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt +++ b/src/kotlinx/src/test/kotlin/integration/CustomFunctions.kt @@ -1,9 +1,6 @@ package solutions.bitbadger.documents.kotlinx.tests.integration import solutions.bitbadger.documents.* -import solutions.bitbadger.documents.java.extensions.countAll -import solutions.bitbadger.documents.java.extensions.insert -import solutions.bitbadger.documents.java.extensions.writeCustomJsonArray import solutions.bitbadger.documents.kotlinx.Results import solutions.bitbadger.documents.kotlinx.extensions.* import solutions.bitbadger.documents.kotlinx.tests.ArrayDocument @@ -46,7 +43,7 @@ object CustomFunctions { fun jsonArraySingle(db: ThrowawayDatabase) { db.conn.insert(TEST_TABLE, ArrayDocument("one", listOf("2", "3"))) assertEquals( - JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), + 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" ) @@ -55,9 +52,8 @@ object CustomFunctions { 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\"]}]"), + 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" diff --git a/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt index 9aaecf4..8ba92e9 100644 --- a/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt +++ b/src/kotlinx/src/test/kotlin/integration/JsonFunctions.kt @@ -121,7 +121,7 @@ object JsonFunctions { Configuration.idField = "key" try { db.conn.insert(TEST_TABLE, NumIdDocument(18, "howdy")) - checkByIdString(db.conn.jsonById(TEST_TABLE, 18)) + checkByIdNumber(db.conn.jsonById(TEST_TABLE, 18)) } finally { Configuration.idField = "id" } diff --git a/src/kotlinx/src/test/kotlin/integration/PgDB.kt b/src/kotlinx/src/test/kotlin/integration/PgDB.kt index 1256d1e..a372702 100644 --- a/src/kotlinx/src/test/kotlin/integration/PgDB.kt +++ b/src/kotlinx/src/test/kotlin/integration/PgDB.kt @@ -8,16 +8,11 @@ import solutions.bitbadger.documents.kotlinx.tests.TEST_TABLE /** * A wrapper for a throwaway PostgreSQL database */ -class PgDB : ThrowawayDatabase { - - private var dbName = "" +class PgDB : ThrowawayDatabase() { init { - dbName = "throwaway_${AutoId.generateRandomString(8)}" Configuration.connectionString = connString("postgres") - Configuration.dbConn().use { - it.customNonQuery("CREATE DATABASE $dbName") - } + Configuration.dbConn().use { it.customNonQuery("CREATE DATABASE $dbName") } Configuration.connectionString = connString(dbName) } @@ -30,9 +25,7 @@ class PgDB : ThrowawayDatabase { override fun close() { conn.close() Configuration.connectionString = connString("postgres") - Configuration.dbConn().use { - it.customNonQuery("DROP DATABASE $dbName") - } + Configuration.dbConn().use { it.customNonQuery("DROP DATABASE $dbName") } Configuration.connectionString = null } diff --git a/src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt b/src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt index 8ea554d..ab69d33 100644 --- a/src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt +++ b/src/kotlinx/src/test/kotlin/integration/SQLiteDB.kt @@ -9,12 +9,9 @@ import java.io.File /** * A wrapper for a throwaway SQLite database */ -class SQLiteDB : ThrowawayDatabase { - - private var dbName = "" +class SQLiteDB : ThrowawayDatabase() { init { - dbName = "test-db-${AutoId.generateRandomString(8)}.db" Configuration.connectionString = "jdbc:sqlite:$dbName" } diff --git a/src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt b/src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt index 858ccc5..437e7c6 100644 --- a/src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt +++ b/src/kotlinx/src/test/kotlin/integration/ThrowawayDatabase.kt @@ -1,14 +1,18 @@ package solutions.bitbadger.documents.kotlinx.tests.integration +import solutions.bitbadger.documents.AutoId import java.sql.Connection /** * Common interface for PostgreSQL and SQLite throwaway databases */ -interface ThrowawayDatabase : AutoCloseable { +abstract class ThrowawayDatabase : AutoCloseable { + + /** The name of the throwaway database */ + protected val dbName = "throwaway_${AutoId.generateRandomString(8)}" /** The database connection for the throwaway database */ - val conn: Connection + abstract val conn: Connection /** * Determine if a database object exists @@ -16,5 +20,5 @@ interface ThrowawayDatabase : AutoCloseable { * @param name The name of the object whose existence should be checked * @return True if the object exists, false if not */ - fun dbObjectExists(name: String): Boolean + abstract fun dbObjectExists(name: String): Boolean } -- 2.47.2 From e8cdb2d4792cd9cc85f53c2aa61fab62f2fcc58c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 15 Apr 2025 21:19:46 -0400 Subject: [PATCH 88/88] Add Maven Central publishing --- pom.xml | 23 +++++++++++++++++++++-- src/core/pom.xml | 36 ++++++++++++++++++++++++++++++++++-- src/groovy/pom.xml | 36 ++++++++++++++++++++++++++++++++++-- src/kotlinx/pom.xml | 31 ++++++++++++++++++++++++++++--- src/scala/pom.xml | 36 ++++++++++++++++++++++++++++++++++-- 5 files changed, 151 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 9eeda2c..eb24808 100644 --- a/pom.xml +++ b/pom.xml @@ -6,12 +6,12 @@ solutions.bitbadger documents - 4.0.0-RC1 + 1.0.0-RC1 pom ${project.groupId}:${project.artifactId} Expose a document store interface for PostgreSQL and SQLite - https://bitbadger.solutions/open-source/solutions.bitbadger.documents + https://relationaldocs.bitbadger.solutions/jvm/ @@ -62,6 +62,25 @@ ./src/scala + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.6 + + + sign-artifacts + verify + + sign + + + + + + + org.junit.jupiter diff --git a/src/core/pom.xml b/src/core/pom.xml index 79623a0..63f0086 100644 --- a/src/core/pom.xml +++ b/src/core/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-RC1 + 1.0.0-RC1 ../../pom.xml @@ -15,7 +15,29 @@ ${project.groupId}:${project.artifactId} Expose a document store interface for PostgreSQL and SQLite (Core Library) - https://bitbadger.solutions/open-source/relational-documents/jvm/ + 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 + @@ -125,6 +147,16 @@ + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + Deployment-core-${project.version} + central + + diff --git a/src/groovy/pom.xml b/src/groovy/pom.xml index ef52157..5c8dde5 100644 --- a/src/groovy/pom.xml +++ b/src/groovy/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-RC1 + 1.0.0-RC1 ../../pom.xml @@ -15,7 +15,29 @@ ${project.groupId}:${project.artifactId} Expose a document store interface for PostgreSQL and SQLite (Groovy Library) - https://bitbadger.solutions/open-source/relational-documents/jvm/ + 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 + @@ -116,6 +138,16 @@ + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + Deployment-groovy-${project.version} + central + + diff --git a/src/kotlinx/pom.xml b/src/kotlinx/pom.xml index d19c981..39164b8 100644 --- a/src/kotlinx/pom.xml +++ b/src/kotlinx/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-RC1 + 1.0.0-RC1 ../../pom.xml @@ -15,14 +15,29 @@ ${project.groupId}:${project.artifactId} Expose a document store interface for PostgreSQL and SQLite (KotlinX Serialization Library) - https://bitbadger.solutions/open-source/relational-documents/jvm/ + 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 @@ -156,6 +171,16 @@ + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + Deployment-kotlinx-${project.version} + central + + diff --git a/src/scala/pom.xml b/src/scala/pom.xml index 2ffcc5d..09ac775 100644 --- a/src/scala/pom.xml +++ b/src/scala/pom.xml @@ -6,7 +6,7 @@ solutions.bitbadger documents - 4.0.0-RC1 + 1.0.0-RC1 ../../pom.xml @@ -15,7 +15,29 @@ ${project.groupId}:${project.artifactId} Expose a document store interface for PostgreSQL and SQLite (Scala Library) - https://bitbadger.solutions/open-source/relational-documents/jvm/ + 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 @@ -110,6 +132,16 @@ --> + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + true + + Deployment-scala-${project.version} + central + + -- 2.47.2