Initial Development #1
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@ -7,8 +7,10 @@
|
|||||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||||
<outputRelativeToContentRoot value="true" />
|
<outputRelativeToContentRoot value="true" />
|
||||||
<module name="common" />
|
<module name="common" />
|
||||||
<module name="sqlite" />
|
|
||||||
</profile>
|
</profile>
|
||||||
</annotationProcessing>
|
</annotationProcessing>
|
||||||
|
<bytecodeTargetLevel>
|
||||||
|
<module name="sqlite" target="1.8" />
|
||||||
|
</bytecodeTargetLevel>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
5
.idea/misc.xml
generated
5
.idea/misc.xml
generated
@ -8,6 +8,11 @@
|
|||||||
<option value="$PROJECT_DIR$/src/sqlite/pom.xml" />
|
<option value="$PROJECT_DIR$/src/sqlite/pom.xml" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="ignoredFiles">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/src/sqlite/pom.xml" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
@ -28,13 +28,15 @@ enum class AutoId {
|
|||||||
/**
|
/**
|
||||||
* Generate a string of random hex characters
|
* 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
|
* @return A string of random hex characters of the requested length
|
||||||
*/
|
*/
|
||||||
fun generateRandomString(length: Int): String =
|
fun generateRandomString(length: Int? = null): String =
|
||||||
kotlin.random.Random.nextBytes((length + 2) / 2)
|
(length ?: Configuration.idStringLength).let { len ->
|
||||||
.joinToString("") { String.format("%02x", it) }
|
kotlin.random.Random.nextBytes((len + 2) / 2)
|
||||||
.substring(0, length)
|
.joinToString("") { String.format("%02x", it) }
|
||||||
|
.substring(0, len)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a document needs an automatic ID applied
|
* Determine if a document needs an automatic ID applied
|
||||||
|
@ -6,4 +6,20 @@ package solutions.bitbadger.documents.common
|
|||||||
* @property op The operation for the field comparison
|
* @property op The operation for the field comparison
|
||||||
* @property value The value against which the comparison will be made
|
* @property value The value against which the comparison will be made
|
||||||
*/
|
*/
|
||||||
class Comparison<T>(val op: Op, val value: T)
|
class Comparison<T>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -26,8 +26,15 @@ object Configuration {
|
|||||||
/** The length of automatic random hex character string */
|
/** The length of automatic random hex character string */
|
||||||
var idStringLength = 16
|
var idStringLength = 16
|
||||||
|
|
||||||
|
/** The derived dialect value from the connection string */
|
||||||
|
private var dialectValue: Dialect? = null
|
||||||
|
|
||||||
/** The connection string for the JDBC connection */
|
/** The connection string for the JDBC connection */
|
||||||
var connectionString: String? = null
|
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
|
* Retrieve a new connection to the configured database
|
||||||
@ -42,23 +49,14 @@ object Configuration {
|
|||||||
return DriverManager.getConnection(connectionString)
|
return DriverManager.getConnection(connectionString)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var dialectValue: Dialect? = null
|
/**
|
||||||
|
* The dialect in use
|
||||||
/** The dialect in use */
|
*
|
||||||
val dialect: Dialect
|
* @param process The process being attempted
|
||||||
get() {
|
* @return The dialect for the current connection
|
||||||
if (dialectValue == null) {
|
* @throws DocumentException If the dialect has not been set
|
||||||
if (connectionString == null) {
|
*/
|
||||||
throw IllegalArgumentException("Please provide a connection string before attempting data access")
|
fun dialect(process: String? = null): Dialect =
|
||||||
}
|
dialectValue ?: throw DocumentException(
|
||||||
val it = connectionString!!
|
"Database mode not set" + if (process == null) "" else "; cannot $process")
|
||||||
dialectValue = when {
|
|
||||||
it.contains("sqlite") -> Dialect.SQLITE
|
|
||||||
it.contains("postgresql") -> Dialect.POSTGRESQL
|
|
||||||
else -> throw IllegalArgumentException("Cannot determine dialect from [$it]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialectValue!!
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,22 @@ enum class Dialect {
|
|||||||
/** PostgreSQL */
|
/** PostgreSQL */
|
||||||
POSTGRESQL,
|
POSTGRESQL,
|
||||||
/** SQLite */
|
/** 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]")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 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)
|
* @property qualifier A table qualifier to use to address the `data` field (useful for multi-table queries)
|
||||||
*/
|
*/
|
||||||
class Field<T>(
|
class Field<T> private constructor(
|
||||||
val name: String,
|
val name: String,
|
||||||
val comparison: Comparison<T>,
|
val comparison: Comparison<T>,
|
||||||
val parameterName: String? = null,
|
val parameterName: String? = null,
|
||||||
@ -42,7 +42,53 @@ class Field<T>(
|
|||||||
fun path(dialect: Dialect, format: FieldFormat = FieldFormat.SQL): String =
|
fun path(dialect: Dialect, format: FieldFormat = FieldFormat.SQL): String =
|
||||||
(if (qualifier == null) "" else "${qualifier}.") + nameToPath(name, dialect, format)
|
(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<String, *> ?: 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 {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a field equality comparison
|
* Create a field equality comparison
|
||||||
*
|
*
|
||||||
@ -50,8 +96,8 @@ class Field<T>(
|
|||||||
* @param value The value for the comparison
|
* @param value The value for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> equal(name: String, value: T): Field<T> =
|
fun <T> equal(name: String, value: T) =
|
||||||
Field<T>(name, Comparison(Op.EQUAL, value))
|
Field(name, Comparison(Op.EQUAL, value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a field greater-than comparison
|
* Create a field greater-than comparison
|
||||||
@ -60,7 +106,7 @@ class Field<T>(
|
|||||||
* @param value The value for the comparison
|
* @param value The value for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> greater(name: String, value: T): Field<T> =
|
fun <T> greater(name: String, value: T) =
|
||||||
Field(name, Comparison(Op.GREATER, value))
|
Field(name, Comparison(Op.GREATER, value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,7 +116,7 @@ class Field<T>(
|
|||||||
* @param value The value for the comparison
|
* @param value The value for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> greaterOrEqual(name: String, value: T): Field<T> =
|
fun <T> greaterOrEqual(name: String, value: T) =
|
||||||
Field(name, Comparison(Op.GREATER_OR_EQUAL, value))
|
Field(name, Comparison(Op.GREATER_OR_EQUAL, value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,7 +126,7 @@ class Field<T>(
|
|||||||
* @param value The value for the comparison
|
* @param value The value for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> less(name: String, value: T): Field<T> =
|
fun <T> less(name: String, value: T) =
|
||||||
Field(name, Comparison(Op.LESS, value))
|
Field(name, Comparison(Op.LESS, value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,7 +136,7 @@ class Field<T>(
|
|||||||
* @param value The value for the comparison
|
* @param value The value for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> lessOrEqual(name: String, value: T): Field<T> =
|
fun <T> lessOrEqual(name: String, value: T) =
|
||||||
Field(name, Comparison(Op.LESS_OR_EQUAL, value))
|
Field(name, Comparison(Op.LESS_OR_EQUAL, value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,7 +146,7 @@ class Field<T>(
|
|||||||
* @param value The value for the comparison
|
* @param value The value for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> notEqual(name: String, value: T): Field<T> =
|
fun <T> notEqual(name: String, value: T) =
|
||||||
Field(name, Comparison(Op.NOT_EQUAL, value))
|
Field(name, Comparison(Op.NOT_EQUAL, value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,7 +157,7 @@ class Field<T>(
|
|||||||
* @param maxValue The upper value for the comparison
|
* @param maxValue The upper value for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> between(name: String, minValue: T, maxValue: T): Field<Pair<T, T>> =
|
fun <T> between(name: String, minValue: T, maxValue: T) =
|
||||||
Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue)))
|
Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue)))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,7 +167,7 @@ class Field<T>(
|
|||||||
* @param values The values for the comparison
|
* @param values The values for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> any(name: String, values: List<T>): Field<List<T>> =
|
fun <T> any(name: String, values: List<T>) =
|
||||||
Field(name, Comparison(Op.IN, values))
|
Field(name, Comparison(Op.IN, values))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,16 +178,16 @@ class Field<T>(
|
|||||||
* @param values The values for the comparison
|
* @param values The values for the comparison
|
||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> inArray(name: String, tableName: String, values: List<T>): Field<Pair<String, List<T>>> =
|
fun <T> inArray(name: String, tableName: String, values: List<T>) =
|
||||||
Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values)))
|
Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values)))
|
||||||
|
|
||||||
fun exists(name: String): Field<String> =
|
fun exists(name: String) =
|
||||||
Field(name, Comparison(Op.EXISTS, ""))
|
Field(name, Comparison(Op.EXISTS, ""))
|
||||||
|
|
||||||
fun notExists(name: String): Field<String> =
|
fun notExists(name: String) =
|
||||||
Field(name, Comparison(Op.NOT_EXISTS, ""))
|
Field(name, Comparison(Op.NOT_EXISTS, ""))
|
||||||
|
|
||||||
fun named(name: String): Field<String> =
|
fun named(name: String) =
|
||||||
Field(name, Comparison(Op.EQUAL, ""))
|
Field(name, Comparison(Op.EQUAL, ""))
|
||||||
|
|
||||||
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
|
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
|
|
||||||
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
|
|
||||||
fun main() {
|
|
||||||
val name = "Kotlin"
|
|
||||||
//TIP Press <shortcut actionId="ShowIntentionActions"/> 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 <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
|
|
||||||
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
|
|
||||||
println("i = $i")
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,6 +12,19 @@ import java.sql.Types
|
|||||||
*/
|
*/
|
||||||
object Parameters {
|
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<Field<*>>): Collection<Field<*>> {
|
||||||
|
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
|
* Replace the parameter names in the query with question marks
|
||||||
*
|
*
|
||||||
|
@ -9,9 +9,84 @@ object Query {
|
|||||||
* @param where The `WHERE` clause for the statement
|
* @param where The `WHERE` clause for the statement
|
||||||
* @return The two parts of the query combined with `WHERE`
|
* @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"
|
"$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<Field<*>>, 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 <TKey> 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 <TKey> 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<Field<*>>) =
|
||||||
|
Query.statementWhere(statement, Where.byFields(fields, howMatched))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions to create queries to define tables and indexes
|
||||||
|
*/
|
||||||
object Definition {
|
object Definition {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,6 +99,18 @@ object Query {
|
|||||||
fun ensureTableFor(tableName: String, dataType: String): String =
|
fun ensureTableFor(tableName: String, dataType: String): String =
|
||||||
"CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)"
|
"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
|
* Split a schema and table name
|
||||||
*
|
*
|
||||||
@ -71,8 +158,27 @@ object Query {
|
|||||||
* @param tableName The table into which to insert (may include schema)
|
* @param tableName The table into which to insert (may include schema)
|
||||||
* @return A query to insert a document
|
* @return A query to insert a document
|
||||||
*/
|
*/
|
||||||
fun insert(tableName: String): String =
|
fun insert(tableName: String, autoId: AutoId? = null): String {
|
||||||
"INSERT INTO $tableName VALUES (:data)"
|
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")
|
* 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
|
* @return A query to save a document
|
||||||
*/
|
*/
|
||||||
fun save(tableName: String): String =
|
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)
|
* Query to count documents in a table (this query has no `WHERE` clause)
|
||||||
@ -120,6 +227,66 @@ object Query {
|
|||||||
fun update(tableName: String): String =
|
fun update(tableName: String): String =
|
||||||
"UPDATE $tableName SET data = :data"
|
"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 <TKey> 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<Field<*>>, 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 <TKey> 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 <TKey> byJsonPath(tableName: String) =
|
||||||
|
patch(tableName, Where.jsonPathMatches())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to delete documents from a table (this query has no `WHERE` clause)
|
* Query to delete documents from a table (this query has no `WHERE` clause)
|
||||||
*
|
*
|
||||||
|
@ -56,7 +56,7 @@ object Results {
|
|||||||
* @return The count from the row
|
* @return The count from the row
|
||||||
*/
|
*/
|
||||||
fun toCount(rs: ResultSet): Long =
|
fun toCount(rs: ResultSet): Long =
|
||||||
when (Configuration.dialect) {
|
when (Configuration.dialect()) {
|
||||||
Dialect.POSTGRESQL -> rs.getInt("it").toLong()
|
Dialect.POSTGRESQL -> rs.getInt("it").toLong()
|
||||||
Dialect.SQLITE -> rs.getLong("it")
|
Dialect.SQLITE -> rs.getLong("it")
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ object Results {
|
|||||||
* @return The true/false value from the row
|
* @return The true/false value from the row
|
||||||
*/
|
*/
|
||||||
fun toExists(rs: ResultSet): Boolean =
|
fun toExists(rs: ResultSet): Boolean =
|
||||||
when (Configuration.dialect) {
|
when (Configuration.dialect()) {
|
||||||
Dialect.POSTGRESQL -> rs.getBoolean("it")
|
Dialect.POSTGRESQL -> rs.getBoolean("it")
|
||||||
Dialect.SQLITE -> toCount(rs) > 0L
|
Dialect.SQLITE -> toCount(rs) > 0L
|
||||||
}
|
}
|
||||||
|
@ -67,14 +67,30 @@ class QueryTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("insert generates correctly")
|
@DisplayName("insert generates correctly")
|
||||||
fun insert() {
|
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
|
@Test
|
||||||
@DisplayName("save generates correctly")
|
@DisplayName("save generates correctly")
|
||||||
fun save() {
|
fun save() {
|
||||||
assertEquals("INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data",
|
try {
|
||||||
Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly")
|
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
|
@Test
|
||||||
|
@ -1,99 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
||||||
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">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<groupId>solutions.bitbadger.documents</groupId>
|
|
||||||
<artifactId>sqlite</artifactId>
|
|
||||||
<version>4.0-ALPHA</version>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<kotlin.code.style>official</kotlin.code.style>
|
|
||||||
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>mavenCentral</id>
|
|
||||||
<url>https://repo1.maven.org/maven2/</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
|
||||||
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-maven-plugin</artifactId>
|
|
||||||
<version>2.1.10</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>compile</id>
|
|
||||||
<phase>compile</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>compile</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
<execution>
|
|
||||||
<id>test-compile</id>
|
|
||||||
<phase>test-compile</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>test-compile</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
|
||||||
<version>2.22.2</version>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-failsafe-plugin</artifactId>
|
|
||||||
<version>2.22.2</version>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.codehaus.mojo</groupId>
|
|
||||||
<artifactId>exec-maven-plugin</artifactId>
|
|
||||||
<version>1.6.0</version>
|
|
||||||
<configuration>
|
|
||||||
<mainClass>MainKt</mainClass>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-test-junit5</artifactId>
|
|
||||||
<version>2.1.10</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.junit.jupiter</groupId>
|
|
||||||
<artifactId>junit-jupiter</artifactId>
|
|
||||||
<version>5.10.0</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
|
||||||
<artifactId>kotlin-stdlib</artifactId>
|
|
||||||
<version>2.1.10</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.xerial</groupId>
|
|
||||||
<artifactId>sqlite-jdbc</artifactId>
|
|
||||||
<version>3.46.1.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>solutions.bitbadger.documents</groupId>
|
|
||||||
<artifactId>common</artifactId>
|
|
||||||
<version>4.0-ALPHA</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
</project>
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Field<*>>): 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<String, Collection<*>>
|
|
||||||
?: 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 <TKey> 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>): 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 <TKey> 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<Field<*>>): 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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user