diff --git a/src/integration-test/kotlin/ThrowawayDatabase.kt b/src/integration-test/kotlin/ThrowawayDatabase.kt new file mode 100644 index 0000000..901f50b --- /dev/null +++ b/src/integration-test/kotlin/ThrowawayDatabase.kt @@ -0,0 +1,20 @@ +package solutions.bitbadger.documents + +import java.sql.Connection + +/** + * Common interface for PostgreSQL and SQLite throwaway databases + */ +interface ThrowawayDatabase : AutoCloseable { + + /** The database connection for the throwaway database */ + val conn: Connection + + /** + * Determine if a database object exists + * + * @param name The name of the object whose existence should be checked + * @return True if the object exists, false if not + */ + fun dbObjectExists(name: String): Boolean +} diff --git a/src/integration-test/kotlin/Types.kt b/src/integration-test/kotlin/Types.kt index 4421a20..c191561 100644 --- a/src/integration-test/kotlin/Types.kt +++ b/src/integration-test/kotlin/Types.kt @@ -3,6 +3,9 @@ package solutions.bitbadger.documents import kotlinx.serialization.Serializable import java.sql.Connection +/** The test table name to use for integration tests */ +const val TEST_TABLE = "test_table" + @Serializable data class NumIdDocument(val key: Int, val text: String) @@ -14,9 +17,9 @@ data class ArrayDocument(val id: String, val values: List) { companion object { /** A set of documents used for integration tests */ val testDocuments = listOf( - ArrayDocument("first", listOf("a", "b", "c" )), + ArrayDocument("first", listOf("a", "b", "c")), ArrayDocument("second", listOf("c", "d", "e")), - ArrayDocument("third", listOf("x", "y", "z"))) + ArrayDocument("third", listOf("x", "y", "z"))) } } @@ -34,10 +37,7 @@ data class JsonDocument(val id: String, val value: String, val numValue: Int, va JsonDocument("four", "purple", 17, SubDocument("green", "red")), JsonDocument("five", "purple", 18, null)) - fun load(conn: Connection, tableName: String = "test_table") = - testDocuments.forEach { conn.insert(tableName, it) } + fun load(db: ThrowawayDatabase, tableName: String = TEST_TABLE) = + testDocuments.forEach { db.conn.insert(tableName, it) } } } - -/** The test table name to use for integration tests */ -const val TEST_TABLE = "test_table" diff --git a/src/integration-test/kotlin/common/Custom.kt b/src/integration-test/kotlin/common/Custom.kt index faaed25..be28838 100644 --- a/src/integration-test/kotlin/common/Custom.kt +++ b/src/integration-test/kotlin/common/Custom.kt @@ -2,8 +2,8 @@ package solutions.bitbadger.documents.common import solutions.bitbadger.documents.* import solutions.bitbadger.documents.query.Count +import solutions.bitbadger.documents.query.Delete import solutions.bitbadger.documents.query.Find -import java.sql.Connection import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -13,67 +13,67 @@ import kotlin.test.assertNull */ object Custom { - fun listEmpty(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) - conn.customNonQuery("DELETE FROM $TEST_TABLE") - val result = conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) + fun listEmpty(db: ThrowawayDatabase) { + JsonDocument.load(db) + db.conn.deleteByFields(TEST_TABLE, listOf(Field.exists(Configuration.idField))) + val result = db.conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) assertEquals(0, result.size, "There should have been no results") } - fun listAll(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) - val result = conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) + fun listAll(db: ThrowawayDatabase) { + JsonDocument.load(db) + val result = db.conn.customList(Find.all(TEST_TABLE), mapFunc = Results::fromData) assertEquals(5, result.size, "There should have been 5 results") } - fun singleNone(conn: Connection) = + fun singleNone(db: ThrowawayDatabase) = assertNull( - conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), + db.conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), "There should not have been a document returned" ) - fun singleOne(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) + fun singleOne(db: ThrowawayDatabase) { + JsonDocument.load(db) assertNotNull( - conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), + db.conn.customSingle(Find.all(TEST_TABLE), mapFunc = Results::fromData), "There should not have been a document returned" ) } - fun nonQueryChanges(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) + fun nonQueryChanges(db: ThrowawayDatabase) { + JsonDocument.load(db) assertEquals( - 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been 5 documents in the table" ) - conn.customNonQuery("DELETE FROM $TEST_TABLE") + db.conn.customNonQuery("DELETE FROM $TEST_TABLE") assertEquals( - 0L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 0L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been no documents in the table" ) } - fun nonQueryNoChanges(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) + fun nonQueryNoChanges(db: ThrowawayDatabase) { + JsonDocument.load(db) assertEquals( - 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should have been 5 documents in the table" ) - conn.customNonQuery( - "DELETE FROM $TEST_TABLE WHERE data->>'id' = :id", + db.conn.customNonQuery( + Delete.byId(TEST_TABLE, "eighty-two"), listOf(Parameter(":id", ParameterType.STRING, "eighty-two")) ) assertEquals( - 5L, conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), + 5L, db.conn.customScalar(Count.all(TEST_TABLE), mapFunc = Results::toCount), "There should still have been 5 documents in the table" ) } - fun scalar(conn: Connection) { - JsonDocument.load(conn, TEST_TABLE) + fun scalar(db: ThrowawayDatabase) { + JsonDocument.load(db) assertEquals( 3L, - conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount), + db.conn.customScalar("SELECT 3 AS it FROM $TEST_TABLE LIMIT 1", mapFunc = Results::toCount), "The number 3 should have been returned" ) } diff --git a/src/integration-test/kotlin/common/Definition.kt b/src/integration-test/kotlin/common/Definition.kt new file mode 100644 index 0000000..c8f7715 --- /dev/null +++ b/src/integration-test/kotlin/common/Definition.kt @@ -0,0 +1,28 @@ +package solutions.bitbadger.documents.common + +import solutions.bitbadger.documents.TEST_TABLE +import solutions.bitbadger.documents.ThrowawayDatabase +import solutions.bitbadger.documents.ensureFieldIndex +import solutions.bitbadger.documents.ensureTable +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Integration tests for the `Definition` object / `ensure*` connection extension functions + */ +object Definition { + + fun ensureTable(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("ensured"), "The 'ensured' table should not exist") + assertFalse(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should not exist") + db.conn.ensureTable("ensured") + assertTrue(db.dbObjectExists("ensured"), "The 'ensured' table should exist") + assertTrue(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should now exist") + } + + fun ensureFieldIndex(db: ThrowawayDatabase) { + assertFalse(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should not exist") + db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category")) + assertTrue(db.dbObjectExists("idx_${TEST_TABLE}_test"), "The test index should now exist") + } +} diff --git a/src/integration-test/kotlin/common/Document.kt b/src/integration-test/kotlin/common/Document.kt index 63a4ed6..88a0f50 100644 --- a/src/integration-test/kotlin/common/Document.kt +++ b/src/integration-test/kotlin/common/Document.kt @@ -1,7 +1,6 @@ package solutions.bitbadger.documents.common import solutions.bitbadger.documents.* -import java.sql.Connection import kotlin.test.assertEquals import kotlin.test.fail @@ -10,37 +9,37 @@ import kotlin.test.fail */ object Document { - fun insertDefault(conn: Connection) { - assertEquals(0L, conn.countAll(TEST_TABLE), "There should be no documents in the table") + fun insertDefault(db: ThrowawayDatabase) { + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble")) - conn.insert(TEST_TABLE, doc) - val after = conn.findAll(TEST_TABLE) + db.conn.insert(TEST_TABLE, doc) + val after = db.conn.findAll(TEST_TABLE) assertEquals(1, after.size, "There should be one document in the table") assertEquals(doc, after[0], "The document should be what was inserted") } - fun insertDupe(conn: Connection) { - conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) + fun insertDupe(db: ThrowawayDatabase) { + db.conn.insert(TEST_TABLE, JsonDocument("a", "", 0, null)) try { - conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) + db.conn.insert(TEST_TABLE, JsonDocument("a", "b", 22, null)) fail("Inserting a document with a duplicate key should have thrown an exception") } catch (_: Exception) { // yay } } - fun insertNumAutoId(conn: Connection) { + fun insertNumAutoId(db: ThrowawayDatabase) { try { Configuration.autoIdStrategy = AutoId.NUMBER Configuration.idField = "key" - assertEquals(0L, conn.countAll(TEST_TABLE), "There should be no documents in the table") + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") - conn.insert(TEST_TABLE, NumIdDocument(0, "one")) - conn.insert(TEST_TABLE, NumIdDocument(0, "two")) - conn.insert(TEST_TABLE, NumIdDocument(77, "three")) - conn.insert(TEST_TABLE, NumIdDocument(0, "four")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "one")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "two")) + db.conn.insert(TEST_TABLE, NumIdDocument(77, "three")) + db.conn.insert(TEST_TABLE, NumIdDocument(0, "four")) - val after = conn.findAll(TEST_TABLE, listOf(Field.named("key"))) + val after = db.conn.findAll(TEST_TABLE, listOf(Field.named("key"))) assertEquals(4, after.size, "There should have been 4 documents returned") assertEquals( "1|2|77|78", after.joinToString("|") { it.key.toString() }, @@ -52,6 +51,38 @@ object Document { } } - // TODO: UUID, Random String + fun insertUUIDAutoId(db: ThrowawayDatabase) { + try { + Configuration.autoIdStrategy = AutoId.UUID + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + db.conn.insert(TEST_TABLE, JsonDocument.emptyDoc) + + val after = db.conn.findAll(TEST_TABLE) + assertEquals(1, after.size, "There should have been 1 document returned") + assertEquals(32, after[0].id.length, "The ID was not generated correctly") + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + } + } + + fun insertStringAutoId(db: ThrowawayDatabase) { + try { + Configuration.autoIdStrategy = AutoId.RANDOM_STRING + assertEquals(0L, db.conn.countAll(TEST_TABLE), "There should be no documents in the table") + + db.conn.insert(TEST_TABLE, JsonDocument.emptyDoc) + + Configuration.idStringLength = 21 + db.conn.insert(TEST_TABLE, JsonDocument.emptyDoc) + + val after = db.conn.findAll(TEST_TABLE) + assertEquals(2, after.size, "There should have been 2 documents returned") + assertEquals(16, after[0].id.length, "The first document's ID was not generated correctly") + assertEquals(21, after[1].id.length, "The second document's ID was not generated correctly") + } finally { + Configuration.autoIdStrategy = AutoId.DISABLED + Configuration.idStringLength = 16 + } + } } diff --git a/src/integration-test/kotlin/postgresql/CustomIT.kt b/src/integration-test/kotlin/postgresql/CustomIT.kt index d356e57..8538446 100644 --- a/src/integration-test/kotlin/postgresql/CustomIT.kt +++ b/src/integration-test/kotlin/postgresql/CustomIT.kt @@ -14,35 +14,35 @@ class CustomIT { @Test @DisplayName("list succeeds with empty list") fun listEmpty() = - PgDB().use { Custom.listEmpty(it.conn) } + PgDB().use(Custom::listEmpty) @Test @DisplayName("list succeeds with a non-empty list") fun listAll() = - PgDB().use { Custom.listAll(it.conn) } + PgDB().use(Custom::listAll) @Test @DisplayName("single succeeds when document not found") fun singleNone() = - PgDB().use { Custom.singleNone(it.conn) } + PgDB().use(Custom::singleNone) @Test @DisplayName("single succeeds when a document is found") fun singleOne() = - PgDB().use { Custom.singleOne(it.conn) } + PgDB().use(Custom::singleOne) @Test @DisplayName("nonQuery makes changes") fun nonQueryChanges() = - PgDB().use { Custom.nonQueryChanges(it.conn) } + PgDB().use(Custom::nonQueryChanges) @Test @DisplayName("nonQuery makes no changes when where clause matches nothing") fun nonQueryNoChanges() = - PgDB().use { Custom.nonQueryNoChanges(it.conn) } + PgDB().use(Custom::nonQueryNoChanges) @Test @DisplayName("scalar succeeds") fun scalar() = - PgDB().use { Custom.scalar(it.conn) } + PgDB().use(Custom::scalar) } diff --git a/src/integration-test/kotlin/postgresql/DefinitionIT.kt b/src/integration-test/kotlin/postgresql/DefinitionIT.kt new file mode 100644 index 0000000..826fd28 --- /dev/null +++ b/src/integration-test/kotlin/postgresql/DefinitionIT.kt @@ -0,0 +1,22 @@ +package solutions.bitbadger.documents.postgresql + +import org.junit.jupiter.api.DisplayName +import solutions.bitbadger.documents.common.Definition +import kotlin.test.Test + +/** + * PostgreSQL integration tests for the `Definition` object / `ensure*` connection extension functions + */ +@DisplayName("PostgreSQL - Definition") +class DefinitionIT { + + @Test + @DisplayName("ensureTable creates table and index") + fun ensureTable() = + PgDB().use(Definition::ensureTable) + + @Test + @DisplayName("ensureFieldIndex creates an index") + fun ensureFieldIndex() = + PgDB().use(Definition::ensureFieldIndex) +} diff --git a/src/integration-test/kotlin/postgresql/DocumentIT.kt b/src/integration-test/kotlin/postgresql/DocumentIT.kt index 003b23d..ee318a7 100644 --- a/src/integration-test/kotlin/postgresql/DocumentIT.kt +++ b/src/integration-test/kotlin/postgresql/DocumentIT.kt @@ -13,15 +13,25 @@ class DocumentIT { @Test @DisplayName("insert works with default values") fun insertDefault() = - PgDB().use { Document.insertDefault(it.conn) } + PgDB().use(Document::insertDefault) @Test @DisplayName("insert fails with duplicate key") fun insertDupe() = - PgDB().use { Document.insertDupe(it.conn) } + PgDB().use(Document::insertDupe) @Test @DisplayName("insert succeeds with numeric auto IDs") fun insertNumAutoId() = - PgDB().use { Document.insertNumAutoId(it.conn) } + PgDB().use(Document::insertNumAutoId) + + @Test + @DisplayName("insert succeeds with UUID auto ID") + fun insertUUIDAutoId() = + PgDB().use(Document::insertUUIDAutoId) + + @Test + @DisplayName("insert succeeds with random string auto ID") + fun insertStringAutoId() = + PgDB().use(Document::insertStringAutoId) } diff --git a/src/integration-test/kotlin/postgresql/PgDB.kt b/src/integration-test/kotlin/postgresql/PgDB.kt index cfc4da3..d4cd76e 100644 --- a/src/integration-test/kotlin/postgresql/PgDB.kt +++ b/src/integration-test/kotlin/postgresql/PgDB.kt @@ -1,14 +1,11 @@ package solutions.bitbadger.documents.postgresql -import solutions.bitbadger.documents.AutoId -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.customNonQuery -import solutions.bitbadger.documents.ensureTable +import solutions.bitbadger.documents.* /** * A wrapper for a throwaway PostgreSQL database */ -class PgDB : AutoCloseable { +class PgDB : ThrowawayDatabase { private var dbName = "" @@ -21,10 +18,10 @@ class PgDB : AutoCloseable { Configuration.connectionString = connString(dbName) } - val conn = Configuration.dbConn() + override val conn = Configuration.dbConn() init { - conn.ensureTable(tableName) + conn.ensureTable(TEST_TABLE) } override fun close() { @@ -36,10 +33,11 @@ class PgDB : AutoCloseable { Configuration.connectionString = null } - companion object { + override fun dbObjectExists(name: String) = + conn.customScalar("SELECT EXISTS (SELECT 1 FROM pg_class WHERE relname = :name) AS it", + listOf(Parameter(":name", ParameterType.STRING, name)), Results::toExists) - /** The table used for test documents */ - val tableName = "test_table" + companion object { /** * Create a connection string for the given database diff --git a/src/integration-test/kotlin/sqlite/CustomIT.kt b/src/integration-test/kotlin/sqlite/CustomIT.kt index 723376b..7b8dbd5 100644 --- a/src/integration-test/kotlin/sqlite/CustomIT.kt +++ b/src/integration-test/kotlin/sqlite/CustomIT.kt @@ -13,35 +13,35 @@ class CustomIT { @Test @DisplayName("list succeeds with empty list") fun listEmpty() = - SQLiteDB().use { Custom.listEmpty(it.conn) } + SQLiteDB().use(Custom::listEmpty) @Test @DisplayName("list succeeds with a non-empty list") fun listAll() = - SQLiteDB().use { Custom.listAll(it.conn) } + SQLiteDB().use(Custom::listAll) @Test @DisplayName("single succeeds when document not found") fun singleNone() = - SQLiteDB().use { Custom.singleNone(it.conn) } + SQLiteDB().use(Custom::singleNone) @Test @DisplayName("single succeeds when a document is found") fun singleOne() = - SQLiteDB().use { Custom.singleOne(it.conn) } + SQLiteDB().use(Custom::singleOne) @Test @DisplayName("nonQuery makes changes") fun nonQueryChanges() = - SQLiteDB().use { Custom.nonQueryChanges(it.conn) } + SQLiteDB().use(Custom::nonQueryChanges) @Test @DisplayName("nonQuery makes no changes when where clause matches nothing") fun nonQueryNoChanges() = - SQLiteDB().use { Custom.nonQueryNoChanges(it.conn) } + SQLiteDB().use(Custom::nonQueryNoChanges) @Test @DisplayName("scalar succeeds") fun scalar() = - SQLiteDB().use { Custom.scalar(it.conn) } + SQLiteDB().use(Custom::scalar) } diff --git a/src/integration-test/kotlin/sqlite/DefinitionIT.kt b/src/integration-test/kotlin/sqlite/DefinitionIT.kt index a952d5c..4bb14f7 100644 --- a/src/integration-test/kotlin/sqlite/DefinitionIT.kt +++ b/src/integration-test/kotlin/sqlite/DefinitionIT.kt @@ -2,6 +2,7 @@ package solutions.bitbadger.documents.sqlite import org.junit.jupiter.api.DisplayName import solutions.bitbadger.documents.* +import solutions.bitbadger.documents.common.Definition import java.sql.Connection import kotlin.test.Test import kotlin.test.assertFalse @@ -13,34 +14,13 @@ import kotlin.test.assertTrue @DisplayName("SQLite - Definition") class DefinitionIT { - /** - * Determine if a database item exists - * - * @param item The items whose existence should be checked - * @param conn The current database connection - * @return True if the item exists in the given database, false if not - */ - private fun itExists(item: String, conn: Connection) = - conn.customScalar("SELECT EXISTS (SELECT 1 FROM ${SQLiteDB.CATALOG} WHERE name = :name) AS it", - listOf(Parameter(":name", ParameterType.STRING, item)), Results::toExists) - @Test @DisplayName("ensureTable creates table and index") fun ensureTable() = - SQLiteDB().use { db -> - assertFalse(itExists("ensured", db.conn), "The 'ensured' table should not exist") - assertFalse(itExists("idx_ensured_key", db.conn), "The PK index for the 'ensured' table should not exist") - db.conn.ensureTable("ensured") - assertTrue(itExists("ensured", db.conn), "The 'ensured' table should exist") - assertTrue(itExists("idx_ensured_key", db.conn), "The PK index for the 'ensured' table should now exist") - } + SQLiteDB().use(Definition::ensureTable) @Test @DisplayName("ensureFieldIndex creates an index") fun ensureFieldIndex() = - SQLiteDB().use { db -> - assertFalse(itExists("idx_${TEST_TABLE}_test", db.conn), "The test index should not exist") - db.conn.ensureFieldIndex(TEST_TABLE, "test", listOf("id", "category")) - assertTrue(itExists("idx_${TEST_TABLE}_test", db.conn), "The test index should now exist") - } + SQLiteDB().use(Definition::ensureFieldIndex) } diff --git a/src/integration-test/kotlin/sqlite/DocumentIT.kt b/src/integration-test/kotlin/sqlite/DocumentIT.kt index e2a7d98..f8c00d2 100644 --- a/src/integration-test/kotlin/sqlite/DocumentIT.kt +++ b/src/integration-test/kotlin/sqlite/DocumentIT.kt @@ -13,16 +13,25 @@ class DocumentIT { @Test @DisplayName("insert works with default values") fun insertDefault() = - SQLiteDB().use { Document.insertDefault(it.conn) } + SQLiteDB().use(Document::insertDefault) @Test @DisplayName("insert fails with duplicate key") fun insertDupe() = - SQLiteDB().use { Document.insertDupe(it.conn) } + SQLiteDB().use(Document::insertDupe) @Test @DisplayName("insert succeeds with numeric auto IDs") fun insertNumAutoId() = - SQLiteDB().use { Document.insertNumAutoId(it.conn) } + SQLiteDB().use(Document::insertNumAutoId) + @Test + @DisplayName("insert succeeds with UUID auto ID") + fun insertUUIDAutoId() = + SQLiteDB().use(Document::insertUUIDAutoId) + + @Test + @DisplayName("insert succeeds with random string auto ID") + fun insertStringAutoId() = + SQLiteDB().use(Document::insertStringAutoId) } diff --git a/src/integration-test/kotlin/sqlite/SQLiteDB.kt b/src/integration-test/kotlin/sqlite/SQLiteDB.kt index 0351911..5528e45 100644 --- a/src/integration-test/kotlin/sqlite/SQLiteDB.kt +++ b/src/integration-test/kotlin/sqlite/SQLiteDB.kt @@ -1,15 +1,13 @@ package solutions.bitbadger.documents.sqlite -import solutions.bitbadger.documents.AutoId -import solutions.bitbadger.documents.Configuration -import solutions.bitbadger.documents.TEST_TABLE -import solutions.bitbadger.documents.ensureTable +import solutions.bitbadger.documents.* import java.io.File +import java.sql.Connection /** * A wrapper for a throwaway SQLite database */ -class SQLiteDB : AutoCloseable { +class SQLiteDB : ThrowawayDatabase { private var dbName = "" @@ -18,7 +16,7 @@ class SQLiteDB : AutoCloseable { Configuration.connectionString = "jdbc:sqlite:$dbName" } - val conn = Configuration.dbConn() + override val conn = Configuration.dbConn() init { conn.ensureTable(TEST_TABLE) @@ -29,9 +27,7 @@ class SQLiteDB : AutoCloseable { File(dbName).delete() } - companion object { - - /** The catalog table for SQLite's schema */ - const val CATALOG = "sqlite_master" - } + override fun dbObjectExists(name: String) = + conn.customScalar("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE name = :name) AS it", + listOf(Parameter(":name", ParameterType.STRING, name)), Results::toExists) } diff --git a/src/main/kotlin/Comparison.kt b/src/main/kotlin/Comparison.kt index c4bdd05..73ff2fe 100644 --- a/src/main/kotlin/Comparison.kt +++ b/src/main/kotlin/Comparison.kt @@ -21,4 +21,7 @@ class Comparison(val op: Op, val value: T) { } return toCheck is Byte || toCheck is Short || toCheck is Int || toCheck is Long } + + override fun toString() = + "$op $value" } diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 8c87011..733fe49 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -114,3 +114,24 @@ inline fun Connection.findAll(tableName: String) = */ inline fun Connection.findAll(tableName: String, orderBy: Collection>) = Find.all(tableName, orderBy, this) + +// ~~~ DOCUMENT DELETION QUERIES ~~~ + +/** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + */ +fun Connection.byId(tableName: String, docId: TKey) = + Delete.byId(tableName, docId, this) + +/** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + */ +fun Connection.deleteByFields(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Delete.byField(tableName, fields, howMatched, this) diff --git a/src/main/kotlin/Delete.kt b/src/main/kotlin/Delete.kt new file mode 100644 index 0000000..3adfe5e --- /dev/null +++ b/src/main/kotlin/Delete.kt @@ -0,0 +1,55 @@ +package solutions.bitbadger.documents + +import solutions.bitbadger.documents.query.Delete +import java.sql.Connection + +/** + * Functions to delete documents + */ +object Delete { + + /** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + * @param conn The connection on which the deletion should be executed + */ + fun byId(tableName: String, docId: TKey, conn: Connection) = + conn.customNonQuery( + Delete.byId(tableName, docId), + Parameters.addFields(listOf(Field.equal(Configuration.idField, docId))) + ) + + /** + * Delete a document by its ID + * + * @param tableName The name of the table from which documents should be deleted + * @param docId The ID of the document to be deleted + */ + fun byId(tableName: String, docId: TKey) = + Configuration.dbConn().use { byId(tableName, docId, it) } + + /** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + * @param conn The connection on which the deletion should be executed + */ + fun byField(tableName: String, fields: Collection>, howMatched: FieldMatch? = null, conn: Connection) { + val named = Parameters.nameFields(fields) + conn.customNonQuery(Delete.byFields(tableName, named, howMatched), Parameters.addFields(named)) + } + + /** + * Delete documents using a field comparison + * + * @param tableName The name of the table from which documents should be deleted + * @param fields The fields which should be compared + * @param howMatched How the fields should be matched + */ + fun byField(tableName: String, fields: Collection>, howMatched: FieldMatch? = null) = + Configuration.dbConn().use { byField(tableName, fields, howMatched, it) } +} diff --git a/src/main/kotlin/Field.kt b/src/main/kotlin/Field.kt index 924944d..e1796aa 100644 --- a/src/main/kotlin/Field.kt +++ b/src/main/kotlin/Field.kt @@ -92,6 +92,9 @@ class Field private constructor( } } + override fun toString() = + "Field ${parameterName ?: ""} $comparison${qualifier?.let { " (qualifier $it)"} ?: ""}" + companion object { /** diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index 2f8466c..961cdcb 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -39,6 +39,30 @@ object Parameters { inline fun json(name: String, value: T) = Parameter(name, ParameterType.JSON, Configuration.json.encodeToString(value)) + /** + * Add field parameters to the given set of parameters + * + * @param fields The fields being compared in the query + * @param existing Any existing parameters for the query (optional, defaults to empty collection) + * @return A collection of parameters for the query + */ + fun addFields( + fields: Collection>, + existing: MutableCollection> = mutableListOf() + ): MutableCollection> { + existing.addAll( + fields + .filter { it.comparison.op != Op.EXISTS && it.comparison.op != Op.NOT_EXISTS } + .map { + Parameter( + it.parameterName!!, + if (it.comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING, + it.comparison.value + ) + }) + return existing + } + /** * Replace the parameter names in the query with question marks * @@ -47,7 +71,7 @@ object Parameters { * @return The query, with name parameters changed to `?`s */ fun replaceNamesInQuery(query: String, parameters: Collection>) = - parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") }.also(::println) + parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") } /** * Apply the given parameters to the given query, returning a prepared statement