Initial Development #1

Merged
danieljsummers merged 88 commits from v1-rc into main 2025-04-16 01:29:20 +00:00
18 changed files with 315 additions and 115 deletions
Showing only changes of commit 250e216ae8 - Show all commits

View File

@ -0,0 +1,20 @@
package solutions.bitbadger.documents
import java.sql.Connection
/**
* Common interface for PostgreSQL and SQLite throwaway databases
*/
interface ThrowawayDatabase : AutoCloseable {
/** The database connection for the throwaway database */
val conn: Connection
/**
* Determine if a database object exists
*
* @param name The name of the object whose existence should be checked
* @return True if the object exists, false if not
*/
fun dbObjectExists(name: String): Boolean
}

View File

@ -3,6 +3,9 @@ package solutions.bitbadger.documents
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.sql.Connection import java.sql.Connection
/** The test table name to use for integration tests */
const val TEST_TABLE = "test_table"
@Serializable @Serializable
data class NumIdDocument(val key: Int, val text: String) data class NumIdDocument(val key: Int, val text: String)
@ -14,9 +17,9 @@ data class ArrayDocument(val id: String, val values: List<String>) {
companion object { companion object {
/** A set of documents used for integration tests */ /** A set of documents used for integration tests */
val testDocuments = listOf( val testDocuments = listOf(
ArrayDocument("first", listOf("a", "b", "c" )), ArrayDocument("first", listOf("a", "b", "c")),
ArrayDocument("second", listOf("c", "d", "e")), ArrayDocument("second", listOf("c", "d", "e")),
ArrayDocument("third", listOf("x", "y", "z"))) ArrayDocument("third", listOf("x", "y", "z")))
} }
} }
@ -34,10 +37,7 @@ data class JsonDocument(val id: String, val value: String, val numValue: Int, va
JsonDocument("four", "purple", 17, SubDocument("green", "red")), JsonDocument("four", "purple", 17, SubDocument("green", "red")),
JsonDocument("five", "purple", 18, null)) JsonDocument("five", "purple", 18, null))
fun load(conn: Connection, tableName: String = "test_table") = fun load(db: ThrowawayDatabase, tableName: String = TEST_TABLE) =
testDocuments.forEach { conn.insert(tableName, it) } testDocuments.forEach { db.conn.insert(tableName, it) }
} }
} }
/** The test table name to use for integration tests */
const val TEST_TABLE = "test_table"

View File

@ -2,8 +2,8 @@ package solutions.bitbadger.documents.common
import solutions.bitbadger.documents.* import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.Count import solutions.bitbadger.documents.query.Count
import solutions.bitbadger.documents.query.Delete
import solutions.bitbadger.documents.query.Find import solutions.bitbadger.documents.query.Find
import java.sql.Connection
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertNull import kotlin.test.assertNull
@ -13,67 +13,67 @@ import kotlin.test.assertNull
*/ */
object Custom { object Custom {
fun listEmpty(conn: Connection) { fun listEmpty(db: ThrowawayDatabase) {
JsonDocument.load(conn, TEST_TABLE) JsonDocument.load(db)
conn.customNonQuery("DELETE FROM $TEST_TABLE") db.conn.deleteByFields(TEST_TABLE, listOf(Field.exists(Configuration.idField)))
val result = conn.customList<JsonDocument>(Find.all(TEST_TABLE), mapFunc = Results::fromData) val result = db.conn.customList<JsonDocument>(Find.all(TEST_TABLE), mapFunc = Results::fromData)
assertEquals(0, result.size, "There should have been no results") assertEquals(0, result.size, "There should have been no results")
} }
fun listAll(conn: Connection) { fun listAll(db: ThrowawayDatabase) {
JsonDocument.load(conn, TEST_TABLE) JsonDocument.load(db)
val result = conn.customList<JsonDocument>(Find.all(TEST_TABLE), mapFunc = Results::fromData) val result = db.conn.customList<JsonDocument>(Find.all(TEST_TABLE), mapFunc = Results::fromData)
assertEquals(5, result.size, "There should have been 5 results") assertEquals(5, result.size, "There should have been 5 results")
} }
fun singleNone(conn: Connection) = fun singleNone(db: ThrowawayDatabase) =
assertNull( assertNull(
conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), db.conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData),
"There should not have been a document returned" "There should not have been a document returned"
) )
fun singleOne(conn: Connection) { fun singleOne(db: ThrowawayDatabase) {
JsonDocument.load(conn, TEST_TABLE) JsonDocument.load(db)
assertNotNull( assertNotNull(
conn.customSingle<JsonDocument>(Find.all(TEST_TABLE), mapFunc = Results::fromData), db.conn.customSingle<JsonDocument>(Find.all(TEST_TABLE), mapFunc = Results::fromData),
"There should not have been a document returned" "There should not have been a document returned"
) )
} }
fun nonQueryChanges(conn: Connection) { fun nonQueryChanges(db: ThrowawayDatabase) {
JsonDocument.load(conn, TEST_TABLE) JsonDocument.load(db)
assertEquals( assertEquals(
5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount),
"There should have been 5 documents in the table" "There should have been 5 documents in the table"
) )
conn.customNonQuery("DELETE FROM $TEST_TABLE") db.conn.customNonQuery("DELETE FROM $TEST_TABLE")
assertEquals( assertEquals(
0L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), 0L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount),
"There should have been no documents in the table" "There should have been no documents in the table"
) )
} }
fun nonQueryNoChanges(conn: Connection) { fun nonQueryNoChanges(db: ThrowawayDatabase) {
JsonDocument.load(conn, TEST_TABLE) JsonDocument.load(db)
assertEquals( assertEquals(
5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount),
"There should have been 5 documents in the table" "There should have been 5 documents in the table"
) )
conn.customNonQuery( db.conn.customNonQuery(
"DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", Delete.byId(TEST_TABLE, "eighty-two"),
listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) listOf(Parameter(":id", ParameterType.STRING, "eighty-two"))
) )
assertEquals( assertEquals(
5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount),
"There should still have been 5 documents in the table" "There should still have been 5 documents in the table"
) )
} }
fun scalar(conn: Connection) { fun scalar(db: ThrowawayDatabase) {
JsonDocument.load(conn, TEST_TABLE) JsonDocument.load(db)
assertEquals( assertEquals(
3L, 3L,
conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount), db.conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount),
"The number 3 should have been returned" "The number 3 should have been returned"
) )
} }

View File

@ -0,0 +1,28 @@
package solutions.bitbadger.documents.common
import solutions.bitbadger.documents.TEST_TABLE
import solutions.bitbadger.documents.ThrowawayDatabase
import solutions.bitbadger.documents.ensureFieldIndex
import solutions.bitbadger.documents.ensureTable
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* Integration tests for the `Definition` object / `ensure*` connection extension functions
*/
object Definition {
fun ensureTable(db: ThrowawayDatabase) {
assertFalse(db.dbObjectExists("ensured"), "The 'ensured' table should not exist")
assertFalse(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should not exist")
db.conn.ensureTable("ensured")
assertTrue(db.dbObjectExists("ensured"), "The 'ensured' table should exist")
assertTrue(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should now exist")
}
fun ensureFieldIndex(db: ThrowawayDatabase) {
assertFalse(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should not exist")
db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category"))
assertTrue(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should now exist")
}
}

View File

@ -1,7 +1,6 @@
package solutions.bitbadger.documents.common package solutions.bitbadger.documents.common
import solutions.bitbadger.documents.* import solutions.bitbadger.documents.*
import java.sql.Connection
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.fail import kotlin.test.fail
@ -10,37 +9,37 @@ import kotlin.test.fail
*/ */
object Document { object Document {
fun insertDefault(conn: Connection) { fun insertDefault(db: ThrowawayDatabase) {
assertEquals(0L, conn.countAll(TEST_TABLE), "There should be no documents in the table") assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table")
val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble"))
conn.insert(TEST_TABLE, doc) db.conn.insert(TEST_TABLE, doc)
val after = conn.findAll<JsonDocument>(TEST_TABLE) val after = db.conn.findAll<JsonDocument>(TEST_TABLE)
assertEquals(1, after.size, "There should be one document in the table") assertEquals(1, after.size, "There should be one document in the table")
assertEquals(doc, after[0], "The document should be what was inserted") assertEquals(doc, after[0], "The document should be what was inserted")
} }
fun insertDupe(conn: Connection) { fun insertDupe(db: ThrowawayDatabase) {
conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) db.conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null))
try { try {
conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) db.conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null))
fail("Inserting a document with a duplicate key should have thrown an exception") fail("Inserting a document with a duplicate key should have thrown an exception")
} catch (_: Exception) { } catch (_: Exception) {
// yay // yay
} }
} }
fun insertNumAutoId(conn: Connection) { fun insertNumAutoId(db: ThrowawayDatabase) {
try { try {
Configuration.autoIdStrategy = AutoId.NUMBER Configuration.autoIdStrategy = AutoId.NUMBER
Configuration.idField = "key" Configuration.idField = "key"
assertEquals(0L, conn.countAll(TEST_TABLE), "There should be no documents in the table") assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table")
conn.insert(TEST_TABLE, NumIdDocument(0, "one")) db.conn.insert(TEST_TABLE, NumIdDocument(0, "one"))
conn.insert(TEST_TABLE, NumIdDocument(0, "two")) db.conn.insert(TEST_TABLE, NumIdDocument(0, "two"))
conn.insert(TEST_TABLE, NumIdDocument(77, "three")) db.conn.insert(TEST_TABLE, NumIdDocument(77, "three"))
conn.insert(TEST_TABLE, NumIdDocument(0, "four")) db.conn.insert(TEST_TABLE, NumIdDocument(0, "four"))
val after = conn.findAll<NumIdDocument>(TEST_TABLE, listOf(Field.named("key"))) val after = db.conn.findAll<NumIdDocument>(TEST_TABLE, listOf(Field.named("key")))
assertEquals(4, after.size, "There should have been 4 documents returned") assertEquals(4, after.size, "There should have been 4 documents returned")
assertEquals( assertEquals(
"1|2|77|78", after.joinToString("|") { it.key.toString() }, "1|2|77|78", after.joinToString("|") { it.key.toString() },
@ -52,6 +51,38 @@ object Document {
} }
} }
// TODO: UUID, Random String fun insertUUIDAutoId(db: ThrowawayDatabase) {
try {
Configuration.autoIdStrategy = AutoId.UUID
assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table")
db.conn.insert(TEST_TABLE, JsonDocument.emptyDoc)
val after = db.conn.findAll<JsonDocument>(TEST_TABLE)
assertEquals(1, after.size, "There should have been 1 document returned")
assertEquals(32, after[0].id.length, "The ID was not generated correctly")
} finally {
Configuration.autoIdStrategy = AutoId.DISABLED
}
}
fun insertStringAutoId(db: ThrowawayDatabase) {
try {
Configuration.autoIdStrategy = AutoId.RANDOM_STRING
assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table")
db.conn.insert(TEST_TABLE, JsonDocument.emptyDoc)
Configuration.idStringLength = 21
db.conn.insert(TEST_TABLE, JsonDocument.emptyDoc)
val after = db.conn.findAll<JsonDocument>(TEST_TABLE)
assertEquals(2, after.size, "There should have been 2 documents returned")
assertEquals(16, after[0].id.length, "The first document's ID was not generated correctly")
assertEquals(21, after[1].id.length, "The second document's ID was not generated correctly")
} finally {
Configuration.autoIdStrategy = AutoId.DISABLED
Configuration.idStringLength = 16
}
}
} }

View File

@ -14,35 +14,35 @@ class CustomIT {
@Test @Test
@DisplayName("list succeeds with empty list") @DisplayName("list succeeds with empty list")
fun listEmpty() = fun listEmpty() =
PgDB().use { Custom.listEmpty(it.conn) } PgDB().use(Custom::listEmpty)
@Test @Test
@DisplayName("list succeeds with a non-empty list") @DisplayName("list succeeds with a non-empty list")
fun listAll() = fun listAll() =
PgDB().use { Custom.listAll(it.conn) } PgDB().use(Custom::listAll)
@Test @Test
@DisplayName("single succeeds when document not found") @DisplayName("single succeeds when document not found")
fun singleNone() = fun singleNone() =
PgDB().use { Custom.singleNone(it.conn) } PgDB().use(Custom::singleNone)
@Test @Test
@DisplayName("single succeeds when a document is found") @DisplayName("single succeeds when a document is found")
fun singleOne() = fun singleOne() =
PgDB().use { Custom.singleOne(it.conn) } PgDB().use(Custom::singleOne)
@Test @Test
@DisplayName("nonQuery makes changes") @DisplayName("nonQuery makes changes")
fun nonQueryChanges() = fun nonQueryChanges() =
PgDB().use { Custom.nonQueryChanges(it.conn) } PgDB().use(Custom::nonQueryChanges)
@Test @Test
@DisplayName("nonQuery makes no changes when where clause matches nothing") @DisplayName("nonQuery makes no changes when where clause matches nothing")
fun nonQueryNoChanges() = fun nonQueryNoChanges() =
PgDB().use { Custom.nonQueryNoChanges(it.conn) } PgDB().use(Custom::nonQueryNoChanges)
@Test @Test
@DisplayName("scalar succeeds") @DisplayName("scalar succeeds")
fun scalar() = fun scalar() =
PgDB().use { Custom.scalar(it.conn) } PgDB().use(Custom::scalar)
} }

View File

@ -0,0 +1,22 @@
package solutions.bitbadger.documents.postgresql
import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.common.Definition
import kotlin.test.Test
/**
* PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions
*/
@DisplayName("PostgreSQL - Definition")
class DefinitionIT {
@Test
@DisplayName("ensureTable creates table and index")
fun ensureTable() =
PgDB().use(Definition::ensureTable)
@Test
@DisplayName("ensureFieldIndex creates an index")
fun ensureFieldIndex() =
PgDB().use(Definition::ensureFieldIndex)
}

View File

@ -13,15 +13,25 @@ class DocumentIT {
@Test @Test
@DisplayName("insert works with default values") @DisplayName("insert works with default values")
fun insertDefault() = fun insertDefault() =
PgDB().use { Document.insertDefault(it.conn) } PgDB().use(Document::insertDefault)
@Test @Test
@DisplayName("insert fails with duplicate key") @DisplayName("insert fails with duplicate key")
fun insertDupe() = fun insertDupe() =
PgDB().use { Document.insertDupe(it.conn) } PgDB().use(Document::insertDupe)
@Test @Test
@DisplayName("insert succeeds with numeric auto IDs") @DisplayName("insert succeeds with numeric auto IDs")
fun insertNumAutoId() = fun insertNumAutoId() =
PgDB().use { Document.insertNumAutoId(it.conn) } PgDB().use(Document::insertNumAutoId)
@Test
@DisplayName("insert succeeds with UUID auto ID")
fun insertUUIDAutoId() =
PgDB().use(Document::insertUUIDAutoId)
@Test
@DisplayName("insert succeeds with random string auto ID")
fun insertStringAutoId() =
PgDB().use(Document::insertStringAutoId)
} }

View File

@ -1,14 +1,11 @@
package solutions.bitbadger.documents.postgresql package solutions.bitbadger.documents.postgresql
import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.customNonQuery
import solutions.bitbadger.documents.ensureTable
/** /**
* A wrapper for a throwaway PostgreSQL database * A wrapper for a throwaway PostgreSQL database
*/ */
class PgDB : AutoCloseable { class PgDB : ThrowawayDatabase {
private var dbName = "" private var dbName = ""
@ -21,10 +18,10 @@ class PgDB : AutoCloseable {
Configuration.connectionString = connString(dbName) Configuration.connectionString = connString(dbName)
} }
val conn = Configuration.dbConn() override val conn = Configuration.dbConn()
init { init {
conn.ensureTable(tableName) conn.ensureTable(TEST_TABLE)
} }
override fun close() { override fun close() {
@ -36,10 +33,11 @@ class PgDB : AutoCloseable {
Configuration.connectionString = null Configuration.connectionString = null
} }
companion object { override fun dbObjectExists(name: String) =
conn.customScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = :name) AS it",
listOf(Parameter(":name", ParameterType.STRING, name)), Results::toExists)
/** The table used for test documents */ companion object {
val tableName = "test_table"
/** /**
* Create a connection string for the given database * Create a connection string for the given database

View File

@ -13,35 +13,35 @@ class CustomIT {
@Test @Test
@DisplayName("list succeeds with empty list") @DisplayName("list succeeds with empty list")
fun listEmpty() = fun listEmpty() =
SQLiteDB().use { Custom.listEmpty(it.conn) } SQLiteDB().use(Custom::listEmpty)
@Test @Test
@DisplayName("list succeeds with a non-empty list") @DisplayName("list succeeds with a non-empty list")
fun listAll() = fun listAll() =
SQLiteDB().use { Custom.listAll(it.conn) } SQLiteDB().use(Custom::listAll)
@Test @Test
@DisplayName("single succeeds when document not found") @DisplayName("single succeeds when document not found")
fun singleNone() = fun singleNone() =
SQLiteDB().use { Custom.singleNone(it.conn) } SQLiteDB().use(Custom::singleNone)
@Test @Test
@DisplayName("single succeeds when a document is found") @DisplayName("single succeeds when a document is found")
fun singleOne() = fun singleOne() =
SQLiteDB().use { Custom.singleOne(it.conn) } SQLiteDB().use(Custom::singleOne)
@Test @Test
@DisplayName("nonQuery makes changes") @DisplayName("nonQuery makes changes")
fun nonQueryChanges() = fun nonQueryChanges() =
SQLiteDB().use { Custom.nonQueryChanges(it.conn) } SQLiteDB().use(Custom::nonQueryChanges)
@Test @Test
@DisplayName("nonQuery makes no changes when where clause matches nothing") @DisplayName("nonQuery makes no changes when where clause matches nothing")
fun nonQueryNoChanges() = fun nonQueryNoChanges() =
SQLiteDB().use { Custom.nonQueryNoChanges(it.conn) } SQLiteDB().use(Custom::nonQueryNoChanges)
@Test @Test
@DisplayName("scalar succeeds") @DisplayName("scalar succeeds")
fun scalar() = fun scalar() =
SQLiteDB().use { Custom.scalar(it.conn) } SQLiteDB().use(Custom::scalar)
} }

View File

@ -2,6 +2,7 @@ package solutions.bitbadger.documents.sqlite
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.* import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.common.Definition
import java.sql.Connection import java.sql.Connection
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertFalse import kotlin.test.assertFalse
@ -13,34 +14,13 @@ import kotlin.test.assertTrue
@DisplayName("SQLite - Definition") @DisplayName("SQLite - Definition")
class DefinitionIT { class DefinitionIT {
/**
* Determine if a database item exists
*
* @param item The items whose existence should be checked
* @param conn The current database connection
* @return True if the item exists in the given database, false if not
*/
private fun itExists(item: String, conn: Connection) =
conn.customScalar("SELECT EXISTS (SELECT 1 FROM ${SQLiteDB.CATALOG} WHERE name = :name) AS it",
listOf(Parameter(":name", ParameterType.STRING, item)), Results::toExists)
@Test @Test
@DisplayName("ensureTable creates table and index") @DisplayName("ensureTable creates table and index")
fun ensureTable() = fun ensureTable() =
SQLiteDB().use { db -> SQLiteDB().use(Definition::ensureTable)
assertFalse(itExists("ensured", db.conn), "The 'ensured' table should not exist")
assertFalse(itExists("idx_ensured_key", db.conn), "The PK index for the 'ensured' table should not exist")
db.conn.ensureTable("ensured")
assertTrue(itExists("ensured", db.conn), "The 'ensured' table should exist")
assertTrue(itExists("idx_ensured_key", db.conn), "The PK index for the 'ensured' table should now exist")
}
@Test @Test
@DisplayName("ensureFieldIndex creates an index") @DisplayName("ensureFieldIndex creates an index")
fun ensureFieldIndex() = fun ensureFieldIndex() =
SQLiteDB().use { db -> SQLiteDB().use(Definition::ensureFieldIndex)
assertFalse(itExists("idx_${TEST_TABLE}_test", db.conn), "The test index should not exist")
db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category"))
assertTrue(itExists("idx_${TEST_TABLE}_test", db.conn), "The test index should now exist")
}
} }

View File

@ -13,16 +13,25 @@ class DocumentIT {
@Test @Test
@DisplayName("insert works with default values") @DisplayName("insert works with default values")
fun insertDefault() = fun insertDefault() =
SQLiteDB().use { Document.insertDefault(it.conn) } SQLiteDB().use(Document::insertDefault)
@Test @Test
@DisplayName("insert fails with duplicate key") @DisplayName("insert fails with duplicate key")
fun insertDupe() = fun insertDupe() =
SQLiteDB().use { Document.insertDupe(it.conn) } SQLiteDB().use(Document::insertDupe)
@Test @Test
@DisplayName("insert succeeds with numeric auto IDs") @DisplayName("insert succeeds with numeric auto IDs")
fun insertNumAutoId() = fun insertNumAutoId() =
SQLiteDB().use { Document.insertNumAutoId(it.conn) } SQLiteDB().use(Document::insertNumAutoId)
@Test
@DisplayName("insert succeeds with UUID auto ID")
fun insertUUIDAutoId() =
SQLiteDB().use(Document::insertUUIDAutoId)
@Test
@DisplayName("insert succeeds with random string auto ID")
fun insertStringAutoId() =
SQLiteDB().use(Document::insertStringAutoId)
} }

View File

@ -1,15 +1,13 @@
package solutions.bitbadger.documents.sqlite package solutions.bitbadger.documents.sqlite
import solutions.bitbadger.documents.AutoId import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.TEST_TABLE
import solutions.bitbadger.documents.ensureTable
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
*/ */
class SQLiteDB : AutoCloseable { class SQLiteDB : ThrowawayDatabase {
private var dbName = "" private var dbName = ""
@ -18,7 +16,7 @@ class SQLiteDB : AutoCloseable {
Configuration.connectionString = "jdbc:sqlite:$dbName" Configuration.connectionString = "jdbc:sqlite:$dbName"
} }
val conn = Configuration.dbConn() override val conn = Configuration.dbConn()
init { init {
conn.ensureTable(TEST_TABLE) conn.ensureTable(TEST_TABLE)
@ -29,9 +27,7 @@ class SQLiteDB : AutoCloseable {
File(dbName).delete() File(dbName).delete()
} }
companion object { override fun dbObjectExists(name: String) =
conn.customScalar("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = :name) AS it",
/** The catalog table for SQLite's schema */ listOf(Parameter(":name", ParameterType.STRING, name)), Results::toExists)
const val CATALOG = "sqlite_master"
}
} }

View File

@ -21,4 +21,7 @@ class Comparison<T>(val op: Op, val value: T) {
} }
return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long
} }
override fun toString() =
"$op $value"
} }

View File

@ -114,3 +114,24 @@ inline fun <reified TDoc> Connection.findAll(tableName: String) =
*/ */
inline fun <reified TDoc> Connection.findAll(tableName: String, orderBy: Collection<Field<*>>) = inline fun <reified TDoc> Connection.findAll(tableName: String, orderBy: Collection<Field<*>>) =
Find.all<TDoc>(tableName, orderBy, this) Find.all<TDoc>(tableName, orderBy, this)
// ~~~ DOCUMENT DELETION QUERIES ~~~
/**
* Delete a document by its ID
*
* @param tableName The name of the table from which documents should be deleted
* @param docId The ID of the document to be deleted
*/
fun <TKey> Connection.byId(tableName: String, docId: TKey) =
Delete.byId(tableName, docId, this)
/**
* Delete documents using a field comparison
*
* @param tableName The name of the table from which documents should be deleted
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
*/
fun Connection.deleteByFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
Delete.byField(tableName, fields, howMatched, this)

55
src/main/kotlin/Delete.kt Normal file
View File

@ -0,0 +1,55 @@
package solutions.bitbadger.documents
import solutions.bitbadger.documents.query.Delete
import java.sql.Connection
/**
* Functions to delete documents
*/
object Delete {
/**
* Delete a document by its ID
*
* @param tableName The name of the table from which documents should be deleted
* @param docId The ID of the document to be deleted
* @param conn The connection on which the deletion should be executed
*/
fun <TKey> byId(tableName: String, docId: TKey, conn: Connection) =
conn.customNonQuery(
Delete.byId(tableName, docId),
Parameters.addFields(listOf(Field.equal(Configuration.idField, docId)))
)
/**
* Delete a document by its ID
*
* @param tableName The name of the table from which documents should be deleted
* @param docId The ID of the document to be deleted
*/
fun <TKey> byId(tableName: String, docId: TKey) =
Configuration.dbConn().use { byId(tableName, docId, it) }
/**
* Delete documents using a field comparison
*
* @param tableName The name of the table from which documents should be deleted
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param conn The connection on which the deletion should be executed
*/
fun byField(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null, conn: Connection) {
val named = Parameters.nameFields(fields)
conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named))
}
/**
* Delete documents using a field comparison
*
* @param tableName The name of the table from which documents should be deleted
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
*/
fun byField(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
Configuration.dbConn().use { byField(tableName, fields, howMatched, it) }
}

View File

@ -92,6 +92,9 @@ class Field<T> private constructor(
} }
} }
override fun toString() =
"Field ${parameterName ?: "<unnamed>"} $comparison${qualifier?.let { " (qualifier $it)"} ?: ""}"
companion object { companion object {
/** /**

View File

@ -39,6 +39,30 @@ object Parameters {
inline fun <reified T> json(name: String, value: T) = inline fun <reified T> json(name: String, value: T) =
Parameter(name, ParameterType.JSON, Configuration.json.encodeToString(value)) Parameter(name, ParameterType.JSON, Configuration.json.encodeToString(value))
/**
* Add field parameters to the given set of parameters
*
* @param fields The fields being compared in the query
* @param existing Any existing parameters for the query (optional, defaults to empty collection)
* @return A collection of parameters for the query
*/
fun addFields(
fields: Collection<Field<*>>,
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
* *
@ -47,7 +71,7 @@ object Parameters {
* @return The query, with name parameters changed to `?`s * @return The query, with name parameters changed to `?`s
*/ */
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, "?") }.also(::println) parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") }
/** /**
* Apply the given parameters to the given query, returning a prepared statement * Apply the given parameters to the given query, returning a prepared statement