Initial Development #1
20
src/integration-test/kotlin/ThrowawayDatabase.kt
Normal file
20
src/integration-test/kotlin/ThrowawayDatabase.kt
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
|
|
||||||
@ -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"
|
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
28
src/integration-test/kotlin/common/Definition.kt
Normal file
28
src/integration-test/kotlin/common/Definition.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
22
src/integration-test/kotlin/postgresql/DefinitionIT.kt
Normal file
22
src/integration-test/kotlin/postgresql/DefinitionIT.kt
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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
55
src/main/kotlin/Delete.kt
Normal 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) }
|
||||||
|
}
|
@ -92,6 +92,9 @@ class Field<T> private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString() =
|
||||||
|
"Field ${parameterName ?: "<unnamed>"} $comparison${qualifier?.let { " (qualifier $it)"} ?: ""}"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user