Initial Development #1

Merged
danieljsummers merged 88 commits from v1-rc into main 2025-04-16 01:29:20 +00:00
15 changed files with 378 additions and 186 deletions
Showing only changes of commit b2d700e658 - Show all commits

View File

@ -113,6 +113,12 @@
<version>3.46.1.2</version> <version>3.46.1.2</version>
<scope>integration-test</scope> <scope>integration-test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.5</version>
<scope>integration-test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,103 +0,0 @@
package solutions.bitbadger.documents
import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.query.Count
import solutions.bitbadger.documents.query.Find
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
/**
* SQLite integration tests for the `Custom` object / `custom*` connection extension functions
*/
@DisplayName("SQLite - Custom")
class CustomSQLiteIT {
@Test
@DisplayName("list succeeds with empty list")
fun listEmpty() =
SQLiteDB().use { db ->
JsonDocument.load(db.conn, SQLiteDB.tableName)
db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}")
val result = db.conn.customList<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData)
assertEquals(0, result.size, "There should have been no results")
}
@Test
@DisplayName("list succeeds with a non-empty list")
fun listAll() =
SQLiteDB().use { db ->
JsonDocument.load(db.conn, SQLiteDB.tableName)
val result = db.conn.customList<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData)
assertEquals(5, result.size, "There should have been 5 results")
}
@Test
@DisplayName("single succeeds when document not found")
fun singleNone() =
SQLiteDB().use { db ->
assertNull(
db.conn.customSingle(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData),
"There should not have been a document returned"
)
}
@Test
@DisplayName("single succeeds when a document is found")
fun singleOne() {
SQLiteDB().use { db ->
JsonDocument.load(db.conn, SQLiteDB.tableName)
assertNotNull(
db.conn.customSingle<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData),
"There should not have been a document returned"
)
}
}
@Test
@DisplayName("nonQuery makes changes")
fun nonQueryChanges() =
SQLiteDB().use { db ->
JsonDocument.load(db.conn, SQLiteDB.tableName)
assertEquals(
5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount),
"There should have been 5 documents in the table"
)
db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}")
assertEquals(
0L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount),
"There should have been no documents in the table"
)
}
@Test
@DisplayName("nonQuery makes no changes when where clause matches nothing")
fun nonQueryNoChanges() =
SQLiteDB().use { db ->
JsonDocument.load(db.conn, SQLiteDB.tableName)
assertEquals(
5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount),
"There should have been 5 documents in the table"
)
db.conn.customNonQuery(
"DELETE FROM ${SQLiteDB.tableName} WHERE data->>'id' = :id",
listOf(Parameter(":id", ParameterType.STRING, "eighty-two"))
)
assertEquals(
5L, db.conn.customScalar(Count.all(SQLiteDB.tableName), mapFunc = Results::toCount),
"There should still have been 5 documents in the table"
)
}
@Test
@DisplayName("scalar succeeds")
fun scalar() =
SQLiteDB().use { db ->
assertEquals(
3L,
db.conn.customScalar("SELECT 3 AS it FROM ${SQLiteDB.catalog} LIMIT 1", mapFunc = Results::toCount),
"The number 3 should have been returned"
)
}
}

View File

@ -1,66 +0,0 @@
package solutions.bitbadger.documents
import org.junit.jupiter.api.DisplayName
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.fail
/**
* SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions
*/
@DisplayName("SQLite - Document")
class DocumentSQLiteIT {
@Test
@DisplayName("insert works with default values")
fun insertDefault() =
SQLiteDB().use { db ->
assertEquals(0L, db.conn.countAll(SQLiteDB.tableName), "There should be no documents in the table")
val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble"))
db.conn.insert(SQLiteDB.tableName, doc)
val after = db.conn.findAll<JsonDocument>(SQLiteDB.tableName)
assertEquals(1, after.size, "There should be one document in the table")
assertEquals(doc, after[0], "The document should be what was inserted")
}
@Test
@DisplayName("insert fails with duplicate key")
fun insertDupe() =
SQLiteDB().use { db ->
db.conn.insert(SQLiteDB.tableName, JsonDocument("a", "", 0, null))
try {
db.conn.insert(SQLiteDB.tableName, JsonDocument("a", "b", 22, null))
fail("Inserting a document with a duplicate key should have thrown an exception")
} catch (_: Exception) {
// yay
}
}
@Test
@DisplayName("insert succeeds with numeric auto IDs")
fun insertNumAutoId() =
SQLiteDB().use { db ->
try {
Configuration.autoIdStrategy = AutoId.NUMBER
Configuration.idField = "key"
assertEquals(0L, db.conn.countAll(SQLiteDB.tableName), "There should be no documents in the table")
db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "one"))
db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "two"))
db.conn.insert(SQLiteDB.tableName, NumIdDocument(77, "three"))
db.conn.insert(SQLiteDB.tableName, NumIdDocument(0, "four"))
val after = db.conn.findAll<NumIdDocument>(SQLiteDB.tableName, 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() },
"The IDs were not generated correctly"
)
} finally {
Configuration.autoIdStrategy = AutoId.DISABLED
Configuration.idField = "id"
}
}
// TODO: UUID, Random String
}

View File

@ -38,3 +38,6 @@ data class JsonDocument(val id: String, val value: String, val numValue: Int, va
testDocuments.forEach { conn.insert(tableName, it) } testDocuments.forEach { conn.insert(tableName, it) }
} }
} }
/** The test table name to use for integration tests */
val testTableName = "test_table"

View File

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

View File

@ -0,0 +1,59 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.sqlite.SQLiteDB
import java.sql.Connection
import kotlin.test.assertEquals
import kotlin.test.fail
/**
* Integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions
*/
object Document {
fun insertDefault(conn: Connection) {
assertEquals(0L, conn.countAll(testTableName), "There should be no documents in the table")
val doc = JsonDocument("turkey", "", 0, SubDocument("gobble", "gobble"))
conn.insert(testTableName, doc)
val after = conn.findAll<JsonDocument>(testTableName)
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(testTableName, JsonDocument("a", "", 0, null))
try {
conn.insert(testTableName, 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) {
try {
Configuration.autoIdStrategy = AutoId.NUMBER
Configuration.idField = "key"
assertEquals(0L, conn.countAll(SQLiteDB.tableName), "There should be no documents in the table")
conn.insert(SQLiteDB.tableName, NumIdDocument(0, "one"))
conn.insert(SQLiteDB.tableName, NumIdDocument(0, "two"))
conn.insert(SQLiteDB.tableName, NumIdDocument(77, "three"))
conn.insert(SQLiteDB.tableName, NumIdDocument(0, "four"))
val after = conn.findAll<NumIdDocument>(SQLiteDB.tableName, 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() },
"The IDs were not generated correctly"
)
} finally {
Configuration.autoIdStrategy = AutoId.DISABLED
Configuration.idField = "id"
}
}
// TODO: UUID, Random String
}

View File

@ -0,0 +1,48 @@
package solutions.bitbadger.documents.postgresql
import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.common.Custom
import kotlin.test.Test
/**
* PostgreSQL integration tests for the `Custom` object / `custom*` connection extension functions
*/
@DisplayName("PostgreSQL - Custom")
class CustomIT {
@Test
@DisplayName("list succeeds with empty list")
fun listEmpty() =
PgDB().use { Custom.listEmpty(it.conn) }
@Test
@DisplayName("list succeeds with a non-empty list")
fun listAll() =
PgDB().use { Custom.listAll(it.conn) }
@Test
@DisplayName("single succeeds when document not found")
fun singleNone() =
PgDB().use { Custom.singleNone(it.conn) }
@Test
@DisplayName("single succeeds when a document is found")
fun singleOne() =
PgDB().use { Custom.singleOne(it.conn) }
@Test
@DisplayName("nonQuery makes changes")
fun nonQueryChanges() =
PgDB().use { Custom.nonQueryChanges(it.conn) }
@Test
@DisplayName("nonQuery makes no changes when where clause matches nothing")
fun nonQueryNoChanges() =
PgDB().use { Custom.nonQueryNoChanges(it.conn) }
@Test
@DisplayName("scalar succeeds")
fun scalar() =
PgDB().use { Custom.scalar(it.conn) }
}

View File

@ -0,0 +1,27 @@
package solutions.bitbadger.documents.postgresql
import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.common.Document
import kotlin.test.Test
/**
* PostgreSQL integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions
*/
@DisplayName("PostgreSQL - Document")
class DocumentIT {
@Test
@DisplayName("insert works with default values")
fun insertDefault() =
PgDB().use { Document.insertDefault(it.conn) }
@Test
@DisplayName("insert fails with duplicate key")
fun insertDupe() =
PgDB().use { Document.insertDupe(it.conn) }
@Test
@DisplayName("insert succeeds with numeric auto IDs")
fun insertNumAutoId() =
PgDB().use { Document.insertNumAutoId(it.conn) }
}

View File

@ -0,0 +1,53 @@
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
/**
* A wrapper for a throwaway PostgreSQL database
*/
class PgDB : AutoCloseable {
private var dbName = ""
init {
dbName = "throwaway_${AutoId.generateRandomString(8)}"
Configuration.connectionString = connString("postgres")
Configuration.dbConn().use {
it.customNonQuery("CREATE DATABASE $dbName")
}
Configuration.connectionString = connString(dbName)
}
val conn = Configuration.dbConn()
init {
conn.ensureTable(tableName)
}
override fun close() {
conn.close()
Configuration.connectionString = connString("postgres")
Configuration.dbConn().use {
it.customNonQuery("DROP DATABASE $dbName")
}
Configuration.connectionString = null
}
companion object {
/** The table used for test documents */
val tableName = "test_table"
/**
* Create a connection string for the given database
*
* @param database The database to which the library should connect
* @return The connection string for the database
*/
private fun connString(database: String) =
"jdbc:postgresql://localhost/$database?user=postgres&password=postgres"
}
}

View File

@ -0,0 +1,47 @@
package solutions.bitbadger.documents.sqlite
import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.common.Custom
import kotlin.test.Test
/**
* SQLite integration tests for the `Custom` object / `custom*` connection extension functions
*/
@DisplayName("SQLite - Custom")
class CustomIT {
@Test
@DisplayName("list succeeds with empty list")
fun listEmpty() =
SQLiteDB().use { Custom.listEmpty(it.conn) }
@Test
@DisplayName("list succeeds with a non-empty list")
fun listAll() =
SQLiteDB().use { Custom.listAll(it.conn) }
@Test
@DisplayName("single succeeds when document not found")
fun singleNone() =
SQLiteDB().use { Custom.singleNone(it.conn) }
@Test
@DisplayName("single succeeds when a document is found")
fun singleOne() =
SQLiteDB().use { Custom.singleOne(it.conn) }
@Test
@DisplayName("nonQuery makes changes")
fun nonQueryChanges() =
SQLiteDB().use { Custom.nonQueryChanges(it.conn) }
@Test
@DisplayName("nonQuery makes no changes when where clause matches nothing")
fun nonQueryNoChanges() =
SQLiteDB().use { Custom.nonQueryNoChanges(it.conn) }
@Test
@DisplayName("scalar succeeds")
fun scalar() =
SQLiteDB().use { Custom.scalar(it.conn) }
}

View File

@ -1,6 +1,7 @@
package solutions.bitbadger.documents package solutions.bitbadger.documents.sqlite
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.*
import java.sql.Connection import java.sql.Connection
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertFalse import kotlin.test.assertFalse
@ -10,7 +11,7 @@ import kotlin.test.assertTrue
* SQLite integration tests for the `Definition` object / `ensure*` connection extension functions * SQLite integration tests for the `Definition` object / `ensure*` connection extension functions
*/ */
@DisplayName("SQLite - Definition") @DisplayName("SQLite - Definition")
class DefinitionSQLiteIT { class DefinitionIT {
/** /**
* Determine if a database item exists * Determine if a database item exists

View File

@ -0,0 +1,28 @@
package solutions.bitbadger.documents.sqlite
import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.common.Document
import kotlin.test.Test
/**
* SQLite integration tests for the `Document` object / `insert`, `save`, `update` connection extension functions
*/
@DisplayName("SQLite - Document")
class DocumentIT {
@Test
@DisplayName("insert works with default values")
fun insertDefault() =
SQLiteDB().use { Document.insertDefault(it.conn) }
@Test
@DisplayName("insert fails with duplicate key")
fun insertDupe() =
SQLiteDB().use { Document.insertDupe(it.conn) }
@Test
@DisplayName("insert succeeds with numeric auto IDs")
fun insertNumAutoId() =
SQLiteDB().use { Document.insertNumAutoId(it.conn) }
}

View File

@ -1,5 +1,8 @@
package solutions.bitbadger.documents package solutions.bitbadger.documents.sqlite
import solutions.bitbadger.documents.AutoId
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.ensureTable
import java.io.File import java.io.File
/** /**

View File

@ -20,19 +20,25 @@ object Document {
val query = if (strategy == AutoId.DISABLED) { val query = if (strategy == AutoId.DISABLED) {
Document.insert(tableName) Document.insert(tableName)
} else { } else {
val idField = Configuration.idField val idField = Configuration.idField
val dialect = Configuration.dialect("Create auto-ID insert query") val dialect = Configuration.dialect("Create auto-ID insert query")
val dataParam = if (AutoId.needsAutoId(strategy, document, idField)) { val dataParam = if (AutoId.needsAutoId(strategy, document, idField)) {
when (strategy) { when (dialect) {
AutoId.NUMBER -> "(SELECT coalesce(max(data->>'$idField'), 0) + 1 FROM $tableName)" Dialect.POSTGRESQL ->
AutoId.UUID -> "'${AutoId.generateUUID()}'" when (strategy) {
AutoId.RANDOM_STRING -> "'${AutoId.generateRandomString()}'" AutoId.NUMBER -> "' || (SELECT coalesce(max(data->>'$idField')::numeric, 0) + 1 FROM $tableName) || '"
else -> "(:data)->>'$idField'" AutoId.UUID -> "\"${AutoId.generateUUID()}\""
}.let { AutoId.RANDOM_STRING -> "\"${AutoId.generateRandomString()}\""
when (dialect) { else -> "\"' || (:data)->>'$idField' || '\""
Dialect.POSTGRESQL -> ":data::jsonb || ('{\"$idField\":$it}')::jsonb" }.let { ":data::jsonb || ('{\"$idField\":$it}')::jsonb" }
Dialect.SQLITE -> "json_set(:data, '$.$idField', $it)"
} Dialect.SQLITE ->
when (strategy) {
AutoId.NUMBER -> "(SELECT coalesce(max(data->>'$idField'), 0) + 1 FROM $tableName)"
AutoId.UUID -> "'${AutoId.generateUUID()}'"
AutoId.RANDOM_STRING -> "'${AutoId.generateRandomString()}'"
else -> "(:data)->>'$idField'"
}.let { "json_set(:data, '$.$idField', $it)" }
} }
} else { } else {
":data" ":data"

View File

@ -47,7 +47,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, "?") } parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") }.also(::println)
/** /**
* Apply the given parameters to the given query, returning a prepared statement * Apply the given parameters to the given query, returning a prepared statement
@ -103,7 +103,7 @@ object Parameters {
} }
} }
ParameterType.JSON -> stmt.setString(idx, param.value as String) ParameterType.JSON -> stmt.setObject(idx, param.value as String, Types.OTHER)
} }
} }
} }