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")
- }
-}