Initial Development #1
@ -1,7 +1,6 @@
|
|||||||
package solutions.bitbadger.documents
|
package solutions.bitbadger.documents
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.sql.Connection
|
|
||||||
|
|
||||||
/** The test table name to use for integration tests */
|
/** The test table name to use for integration tests */
|
||||||
const val TEST_TABLE = "test_table"
|
const val TEST_TABLE = "test_table"
|
||||||
|
69
src/integration-test/kotlin/common/Count.kt
Normal file
69
src/integration-test/kotlin/common/Count.kt
Normal file
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,6 @@
|
|||||||
package solutions.bitbadger.documents.common
|
package solutions.bitbadger.documents.common
|
||||||
|
|
||||||
import solutions.bitbadger.documents.TEST_TABLE
|
import solutions.bitbadger.documents.*
|
||||||
import solutions.bitbadger.documents.ThrowawayDatabase
|
|
||||||
import solutions.bitbadger.documents.ensureFieldIndex
|
|
||||||
import solutions.bitbadger.documents.ensureTable
|
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@ -25,4 +22,22 @@ object Definition {
|
|||||||
db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category"))
|
db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category"))
|
||||||
assertTrue(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should now exist")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
47
src/integration-test/kotlin/postgresql/CountIT.kt
Normal file
47
src/integration-test/kotlin/postgresql/CountIT.kt
Normal file
@ -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)
|
||||||
|
}
|
@ -19,4 +19,14 @@ class DefinitionIT {
|
|||||||
@DisplayName("ensureFieldIndex creates an index")
|
@DisplayName("ensureFieldIndex creates an index")
|
||||||
fun ensureFieldIndex() =
|
fun ensureFieldIndex() =
|
||||||
PgDB().use(Definition::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)
|
||||||
}
|
}
|
||||||
|
41
src/integration-test/kotlin/sqlite/CountIT.kt
Normal file
41
src/integration-test/kotlin/sqlite/CountIT.kt
Normal file
@ -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<DocumentException> { SQLiteDB().use(Count::byContainsMatch) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("byJsonPath fails")
|
||||||
|
fun byJsonPathMatch() {
|
||||||
|
assertThrows<DocumentException> { SQLiteDB().use(Count::byJsonPathMatch) }
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,10 @@
|
|||||||
package solutions.bitbadger.documents.sqlite
|
package solutions.bitbadger.documents.sqlite
|
||||||
|
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
import solutions.bitbadger.documents.*
|
import solutions.bitbadger.documents.*
|
||||||
import solutions.bitbadger.documents.common.Definition
|
import solutions.bitbadger.documents.common.Definition
|
||||||
import java.sql.Connection
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQLite integration tests for the `Definition` object / `ensure*` connection extension functions
|
* SQLite integration tests for the `Definition` object / `ensure*` connection extension functions
|
||||||
@ -23,4 +21,16 @@ class DefinitionIT {
|
|||||||
@DisplayName("ensureFieldIndex creates an index")
|
@DisplayName("ensureFieldIndex creates an index")
|
||||||
fun ensureFieldIndex() =
|
fun ensureFieldIndex() =
|
||||||
SQLiteDB().use(Definition::ensureFieldIndex)
|
SQLiteDB().use(Definition::ensureFieldIndex)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("ensureDocumentIndex fails for full index")
|
||||||
|
fun ensureDocumentIndexFull() {
|
||||||
|
assertThrows<DocumentException> { SQLiteDB().use(Definition::ensureDocumentIndexFull) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("ensureDocumentIndex fails for optimized index")
|
||||||
|
fun ensureDocumentIndexOptimized() {
|
||||||
|
assertThrows<DocumentException> { SQLiteDB().use(Definition::ensureDocumentIndexOptimized) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package solutions.bitbadger.documents.sqlite
|
|||||||
|
|
||||||
import solutions.bitbadger.documents.*
|
import solutions.bitbadger.documents.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.sql.Connection
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper for a throwaway SQLite database
|
* A wrapper for a throwaway SQLite database
|
||||||
|
@ -1,27 +1,54 @@
|
|||||||
package solutions.bitbadger.documents
|
package solutions.bitbadger.documents
|
||||||
|
|
||||||
|
interface Comparison<T> {
|
||||||
|
|
||||||
|
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 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 SingleComparison<T>(override val op: Op, override val value: T) : Comparison<T> {
|
||||||
|
|
||||||
/** Is the value for this comparison a numeric value? */
|
/** Is the value for this comparison a numeric value? */
|
||||||
val isNumeric: Boolean
|
override val isNumeric: Boolean
|
||||||
get() {
|
get() = value.let { it is Byte || it is Short || it is Int || it is Long }
|
||||||
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 fun toString() =
|
override fun toString() =
|
||||||
"$op $value"
|
"$op $value"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A range comparison against a field in a JSON document
|
||||||
|
*/
|
||||||
|
class BetweenComparison<T>(override val op: Op = Op.BETWEEN, override val value: Pair<T, T>) : Comparison<Pair<T, T>> {
|
||||||
|
|
||||||
|
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<T>(override val op: Op = Op.IN, override val value: Collection<T>) : Comparison<Collection<T>> {
|
||||||
|
|
||||||
|
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<T>(override val op: Op = Op.IN_ARRAY, override val value: Pair<String, Collection<T>>) : Comparison<Pair<String, Collection<T>>> {
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
@ -72,6 +72,16 @@ fun Connection.ensureTable(tableName: String) =
|
|||||||
fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
|
fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
|
||||||
Definition.ensureFieldIndex(tableName, indexName, fields, this)
|
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 ~~~
|
// ~~~ DOCUMENT MANIPULATION QUERIES ~~~
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,6 +104,38 @@ inline fun <reified TDoc> Connection.insert(tableName: String, document: TDoc) =
|
|||||||
fun Connection.countAll(tableName: String) =
|
fun Connection.countAll(tableName: String) =
|
||||||
Count.all(tableName, this)
|
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<Field<*>>, 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 <reified T> 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 ~~~
|
// ~~~ DOCUMENT RETRIEVAL QUERIES ~~~
|
||||||
|
|
||||||
@ -134,4 +176,4 @@ fun <TKey> Connection.byId(tableName: String, docId: TKey) =
|
|||||||
* @param howMatched How the fields should be matched
|
* @param howMatched How the fields should be matched
|
||||||
*/
|
*/
|
||||||
fun Connection.deleteByFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
fun Connection.deleteByFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||||
Delete.byField(tableName, fields, howMatched, this)
|
Delete.byFields(tableName, fields, howMatched, this)
|
||||||
|
@ -26,4 +26,88 @@ object Count {
|
|||||||
*/
|
*/
|
||||||
fun all(tableName: String) =
|
fun all(tableName: String) =
|
||||||
Configuration.dbConn().use { all(tableName, it) }
|
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<Field<*>>,
|
||||||
|
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<Field<*>>, 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 <reified T> 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 <reified T> 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) }
|
||||||
}
|
}
|
||||||
|
@ -41,11 +41,32 @@ object Definition {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an index on field(s) within documents in the specified table if necessary
|
* 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 tableName The table to be indexed (may include schema)
|
||||||
* @param indexName The name of the index to create
|
* @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
|
|
||||||
*/
|
*/
|
||||||
fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
|
fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
|
||||||
Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) }
|
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) }
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ object Delete {
|
|||||||
* @param howMatched How the fields should be matched
|
* @param howMatched How the fields should be matched
|
||||||
* @param conn The connection on which the deletion should be executed
|
* @param conn The connection on which the deletion should be executed
|
||||||
*/
|
*/
|
||||||
fun byField(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null, conn: Connection) {
|
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null, conn: Connection) {
|
||||||
val named = Parameters.nameFields(fields)
|
val named = Parameters.nameFields(fields)
|
||||||
conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named))
|
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 fields The fields which should be compared
|
||||||
* @param howMatched How the fields should be matched
|
* @param howMatched How the fields should be matched
|
||||||
*/
|
*/
|
||||||
fun byField(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
|
||||||
Configuration.dbConn().use { byField(tableName, fields, howMatched, it) }
|
Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) }
|
||||||
}
|
}
|
||||||
|
13
src/main/kotlin/DocumentIndex.kt
Normal file
13
src/main/kotlin/DocumentIndex.kt
Normal file
@ -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")
|
||||||
|
}
|
@ -92,6 +92,47 @@ class Field<T> 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<Parameter<*>>): MutableCollection<Parameter<*>> {
|
||||||
|
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<String, Collection<*>>
|
||||||
|
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() =
|
override fun toString() =
|
||||||
"Field ${parameterName ?: "<unnamed>"} $comparison${qualifier?.let { " (qualifier $it)"} ?: ""}"
|
"Field ${parameterName ?: "<unnamed>"} $comparison${qualifier?.let { " (qualifier $it)"} ?: ""}"
|
||||||
|
|
||||||
@ -106,7 +147,7 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> equal(name: String, value: T, paramName: String? = null) =
|
fun <T> 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
|
* Create a field greater-than comparison
|
||||||
@ -117,7 +158,7 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> greater(name: String, value: T, paramName: String? = null) =
|
fun <T> 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
|
* Create a field greater-than-or-equal-to comparison
|
||||||
@ -128,7 +169,7 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> greaterOrEqual(name: String, value: T, paramName: String? = null) =
|
fun <T> 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
|
* Create a field less-than comparison
|
||||||
@ -139,7 +180,7 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> less(name: String, value: T, paramName: String? = null) =
|
fun <T> 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
|
* Create a field less-than-or-equal-to comparison
|
||||||
@ -150,7 +191,7 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> lessOrEqual(name: String, value: T, paramName: String? = null) =
|
fun <T> 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
|
* Create a field inequality comparison
|
||||||
@ -161,7 +202,7 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> notEqual(name: String, value: T, paramName: String? = null) =
|
fun <T> 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
|
* Create a field range comparison
|
||||||
@ -173,7 +214,7 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> between(name: String, minValue: T, maxValue: T, paramName: String? = null) =
|
fun <T> 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`)
|
* Create a field where any values match (SQL `IN`)
|
||||||
@ -184,7 +225,7 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> any(name: String, values: Collection<T>, paramName: String? = null) =
|
fun <T> any(name: String, values: Collection<T>, 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
|
* Create a field where values should exist in a document's array
|
||||||
@ -196,16 +237,16 @@ class Field<T> private constructor(
|
|||||||
* @return A `Field` with the given comparison
|
* @return A `Field` with the given comparison
|
||||||
*/
|
*/
|
||||||
fun <T> inArray(name: String, tableName: String, values: Collection<T>, paramName: String? = null) =
|
fun <T> inArray(name: String, tableName: String, values: Collection<T>, 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) =
|
fun exists(name: String) =
|
||||||
Field(name, Comparison(Op.EXISTS, ""))
|
Field(name, SingleComparison(Op.EXISTS, ""))
|
||||||
|
|
||||||
fun notExists(name: String) =
|
fun notExists(name: String) =
|
||||||
Field(name, Comparison(Op.NOT_EXISTS, ""))
|
Field(name, SingleComparison(Op.NOT_EXISTS, ""))
|
||||||
|
|
||||||
fun named(name: String) =
|
fun named(name: String) =
|
||||||
Field(name, Comparison(Op.EQUAL, ""))
|
Field(name, SingleComparison(Op.EQUAL, ""))
|
||||||
|
|
||||||
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
|
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
|
||||||
val path = StringBuilder("data")
|
val path = StringBuilder("data")
|
||||||
|
@ -46,22 +46,8 @@ object Parameters {
|
|||||||
* @param existing Any existing parameters for the query (optional, defaults to empty collection)
|
* @param existing Any existing parameters for the query (optional, defaults to empty collection)
|
||||||
* @return A collection of parameters for the query
|
* @return A collection of parameters for the query
|
||||||
*/
|
*/
|
||||||
fun addFields(
|
fun addFields(fields: Collection<Field<*>>, existing: MutableCollection<Parameter<*>> = mutableListOf()) =
|
||||||
fields: Collection<Field<*>>,
|
fields.fold(existing) { acc, field -> field.appendParameter(acc) }
|
||||||
existing: MutableCollection<Parameter<*>> = mutableListOf()
|
|
||||||
): MutableCollection<Parameter<*>> {
|
|
||||||
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
|
* Replace the parameter names in the query with question marks
|
||||||
@ -72,7 +58,7 @@ object Parameters {
|
|||||||
*/
|
*/
|
||||||
fun replaceNamesInQuery(query: String, parameters: Collection<Parameter<*>>) =
|
fun replaceNamesInQuery(query: String, parameters: Collection<Parameter<*>>) =
|
||||||
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
|
* Apply the given parameters to the given query, returning a prepared statement
|
||||||
*
|
*
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
package solutions.bitbadger.documents.query
|
package solutions.bitbadger.documents.query
|
||||||
|
|
||||||
import solutions.bitbadger.documents.Configuration
|
import solutions.bitbadger.documents.*
|
||||||
import solutions.bitbadger.documents.Dialect
|
|
||||||
import solutions.bitbadger.documents.Field
|
|
||||||
import solutions.bitbadger.documents.FieldFormat
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Functions to create queries to define tables and indexes
|
* Functions to create queries to define tables and indexes
|
||||||
@ -76,4 +73,20 @@ object Definition {
|
|||||||
*/
|
*/
|
||||||
fun ensureKey(tableName: String, dialect: Dialect? = null) =
|
fun ensureKey(tableName: String, dialect: Dialect? = null) =
|
||||||
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
|
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})";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,82 +14,82 @@ class ComparisonTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("isNumeric is false for empty list of values")
|
@DisplayName("isNumeric is false for empty list of values")
|
||||||
fun isNumericFalseForEmptyList() =
|
fun isNumericFalseForEmptyList() =
|
||||||
assertFalse(Comparison(Op.IN, listOf<Int>()).isNumeric, "An IN with empty list should not be numeric")
|
assertFalse(InComparison(Op.IN, listOf<Int>()).isNumeric, "An IN with empty list should not be numeric")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("isNumeric is false for IN with strings")
|
@DisplayName("isNumeric is false for IN with strings")
|
||||||
fun isNumericFalseForStringsAndIn() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is true for IN with bytes")
|
@DisplayName("isNumeric is true for IN with bytes")
|
||||||
fun isNumericTrueForByteAndIn() =
|
fun isNumericTrueForByteAndIn() =
|
||||||
assertTrue(Comparison(Op.IN, listOf<Byte>(4, 8)).isNumeric, "An IN with bytes should be numeric")
|
assertTrue(InComparison(Op.IN, listOf<Byte>(4, 8)).isNumeric, "An IN with bytes should be numeric")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("isNumeric is true for IN with shorts")
|
@DisplayName("isNumeric is true for IN with shorts")
|
||||||
fun isNumericTrueForShortAndIn() =
|
fun isNumericTrueForShortAndIn() =
|
||||||
assertTrue(Comparison(Op.IN, listOf<Short>(18, 22)).isNumeric, "An IN with shorts should be numeric")
|
assertTrue(InComparison(Op.IN, listOf<Short>(18, 22)).isNumeric, "An IN with shorts should be numeric")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("isNumeric is true for IN with ints")
|
@DisplayName("isNumeric is true for IN with ints")
|
||||||
fun isNumericTrueForIntAndIn() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is true for IN with longs")
|
@DisplayName("isNumeric is true for IN with longs")
|
||||||
fun isNumericTrueForLongAndIn() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is false for BETWEEN with strings")
|
@DisplayName("isNumeric is false for BETWEEN with strings")
|
||||||
fun isNumericFalseForStringsAndBetween() =
|
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")
|
"A BETWEEN with strings should not be numeric")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("isNumeric is true for BETWEEN with bytes")
|
@DisplayName("isNumeric is true for BETWEEN with bytes")
|
||||||
fun isNumericTrueForByteAndBetween() =
|
fun isNumericTrueForByteAndBetween() =
|
||||||
assertTrue(Comparison(Op.BETWEEN, Pair<Byte, Byte>(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric")
|
assertTrue(BetweenComparison(Op.BETWEEN, Pair<Byte, Byte>(7, 11)).isNumeric, "A BETWEEN with bytes should be numeric")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("isNumeric is true for BETWEEN with shorts")
|
@DisplayName("isNumeric is true for BETWEEN with shorts")
|
||||||
fun isNumericTrueForShortAndBetween() =
|
fun isNumericTrueForShortAndBetween() =
|
||||||
assertTrue(Comparison(Op.BETWEEN, Pair<Short, Short>(0, 9)).isNumeric,
|
assertTrue(BetweenComparison(Op.BETWEEN, Pair<Short, Short>(0, 9)).isNumeric,
|
||||||
"A BETWEEN with shorts should be numeric")
|
"A BETWEEN with shorts should be numeric")
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("isNumeric is true for BETWEEN with ints")
|
@DisplayName("isNumeric is true for BETWEEN with ints")
|
||||||
fun isNumericTrueForIntAndBetween() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is true for BETWEEN with longs")
|
@DisplayName("isNumeric is true for BETWEEN with longs")
|
||||||
fun isNumericTrueForLongAndBetween() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is false for string value")
|
@DisplayName("isNumeric is false for string value")
|
||||||
fun isNumericFalseForString() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is true for byte value")
|
@DisplayName("isNumeric is true for byte value")
|
||||||
fun isNumericTrueForByte() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is true for short value")
|
@DisplayName("isNumeric is true for short value")
|
||||||
fun isNumericTrueForShort() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is true for int value")
|
@DisplayName("isNumeric is true for int value")
|
||||||
fun isNumericTrueForInt() =
|
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
|
@Test
|
||||||
@DisplayName("isNumeric is true for long value")
|
@DisplayName("isNumeric is true for long value")
|
||||||
fun isNumericTrueForLong() =
|
fun isNumericTrueForLong() =
|
||||||
assertTrue(Comparison(Op.EQUAL, 82L).isNumeric, "A long should be numeric")
|
assertTrue(SingleComparison(Op.EQUAL, 82L).isNumeric, "A long should be numeric")
|
||||||
}
|
}
|
||||||
|
24
src/test/kotlin/DocumentIndexTest.kt
Normal file
24
src/test/kotlin/DocumentIndexTest.kt
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import org.junit.jupiter.api.assertThrows
|
|||||||
import solutions.bitbadger.documents.Configuration
|
import solutions.bitbadger.documents.Configuration
|
||||||
import solutions.bitbadger.documents.Dialect
|
import solutions.bitbadger.documents.Dialect
|
||||||
import solutions.bitbadger.documents.DocumentException
|
import solutions.bitbadger.documents.DocumentException
|
||||||
|
import solutions.bitbadger.documents.DocumentIndex
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,4 +102,33 @@ class DefinitionTest {
|
|||||||
Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
|
Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
|
||||||
"CREATE INDEX for nested SQLite field incorrect"
|
"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<DocumentException> { Definition.ensureDocumentIndexOn(tbl, DocumentIndex.FULL) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user