Initial Development #1

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

View File

@ -107,6 +107,12 @@
<artifactId>kotlinx-serialization-json</artifactId> <artifactId>kotlinx-serialization-json</artifactId>
<version>${serialization.version}</version> <version>${serialization.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.1.2</version>
<scope>integration-test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,43 +1,103 @@
package solutions.bitbadger.documents package solutions.bitbadger.documents
import kotlinx.serialization.Serializable
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import solutions.bitbadger.documents.query.Count
import solutions.bitbadger.documents.query.Definition
import solutions.bitbadger.documents.query.Find import solutions.bitbadger.documents.query.Find
import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class CustomSQLiteIT {
private val tbl = "test_table";
@BeforeEach
fun setUp() {
Configuration.connectionString = "jdbc:sqlite:memory"
}
/** /**
* Reset the dialect * SQLite integration tests for the `Custom` object / `custom*` connection extension functions
*/ */
@AfterEach @DisplayName("SQLite - Custom")
fun cleanUp() { class CustomSQLiteIT {
Configuration.dialectValue = null
}
@Test @Test
@DisplayName("list succeeds with empty list") @DisplayName("list succeeds with empty list")
fun listEmpty() { fun listEmpty() =
Configuration.dbConn().use { conn -> SQLiteDB().use { db ->
conn.customNonQuery(Definition.ensureTable(tbl), listOf()) JsonDocument.load(db.conn, SQLiteDB.tableName)
conn.customNonQuery(Definition.ensureKey(tbl, Dialect.SQLITE), listOf()) db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}")
val result = conn.customList<TestDocument>(Find.all(tbl), listOf(), Results::fromData) val result = db.conn.customList<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData)
assertEquals(0, result.size, "There should have been no results") 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"
)
} }
} }
@Serializable @Test
data class TestDocument(val id: String) @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

@ -0,0 +1,45 @@
package solutions.bitbadger.documents
import org.junit.jupiter.api.DisplayName
import java.sql.Connection
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* SQLite integration tests for the `Definition` object / `ensure*` connection extension functions
*/
@DisplayName("SQLite - Definition")
class DefinitionSQLiteIT {
/**
* 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")
}
@Test
@DisplayName("ensureFieldIndex creates an index")
fun ensureFieldIndex() =
SQLiteDB().use { db ->
assertFalse(itExists("idx_${SQLiteDB.tableName}_test", db.conn), "The test index should not exist")
db.conn.ensureFieldIndex(SQLiteDB.tableName, "test", listOf("id", "category"))
assertTrue(itExists("idx_${SQLiteDB.tableName}_test", db.conn), "The test index should now exist")
}
}

View File

@ -0,0 +1,66 @@
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

@ -0,0 +1,36 @@
package solutions.bitbadger.documents
import java.io.File
/**
* A wrapper for a throwaway SQLite database
*/
class SQLiteDB : AutoCloseable {
private var dbName = "";
init {
dbName = "test-db-${AutoId.generateRandomString(8)}.db"
Configuration.connectionString = "jdbc:sqlite:$dbName"
}
val conn = Configuration.dbConn()
init {
conn.ensureTable(tableName)
}
override fun close() {
conn.close()
File(dbName).delete()
}
companion object {
/** The catalog table for SQLite's schema */
val catalog = "sqlite_master"
/** The table used for test documents */
val tableName = "test_table"
}
}

View File

@ -0,0 +1,40 @@
package solutions.bitbadger.documents
import kotlinx.serialization.Serializable
import java.sql.Connection
@Serializable
data class NumIdDocument(val key: Int, val text: String)
@Serializable
data class SubDocument(val foo: String, val bar: String)
@Serializable
data class ArrayDocument(val id: String, val values: List<String>) {
companion object {
/** A set of documents used for integration tests */
val testDocuments = listOf(
ArrayDocument("first", listOf("a", "b", "c" )),
ArrayDocument("second", listOf("c", "d", "e")),
ArrayDocument("third", listOf("x", "y", "z")))
}
}
@Serializable
data class JsonDocument(val id: String, val value: String, val numValue: Int, val sub: SubDocument?) {
companion object {
/** An empty JsonDocument */
val emptyDoc = JsonDocument("", "", 0, null)
/** Documents to use for testing */
val testDocuments = listOf(
JsonDocument("one", "FIRST!", 0, null),
JsonDocument("two", "another", 10, SubDocument("green", "blue")),
JsonDocument("three", "", 4, null),
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) }
}
}

View File

@ -3,6 +3,8 @@ package solutions.bitbadger.documents
import java.sql.Connection import java.sql.Connection
import java.sql.ResultSet import java.sql.ResultSet
// ~~~ CUSTOM QUERIES ~~~
/** /**
* Execute a query that returns a list of results * Execute a query that returns a list of results
* *
@ -12,7 +14,7 @@ import java.sql.ResultSet
* @return A list of results for the given query * @return A list of results for the given query
*/ */
inline fun <reified TDoc> Connection.customList( inline fun <reified TDoc> Connection.customList(
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> TDoc query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
) = Custom.list(query, parameters, this, mapFunc) ) = Custom.list(query, parameters, this, mapFunc)
/** /**
@ -24,7 +26,7 @@ inline fun <reified TDoc> Connection.customList(
* @return The document if one matches the query, `null` otherwise * @return The document if one matches the query, `null` otherwise
*/ */
inline fun <reified TDoc> Connection.customSingle( inline fun <reified TDoc> Connection.customSingle(
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> TDoc query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
) = Custom.single(query, parameters, this, mapFunc) ) = Custom.single(query, parameters, this, mapFunc)
/** /**
@ -33,7 +35,7 @@ inline fun <reified TDoc> Connection.customSingle(
* @param query The query to retrieve the results * @param query The query to retrieve the results
* @param parameters Parameters to use for the query * @param parameters Parameters to use for the query
*/ */
fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>>) = fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) =
Custom.nonQuery(query, parameters, this) Custom.nonQuery(query, parameters, this)
/** /**
@ -46,6 +48,69 @@ fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>
*/ */
inline fun <reified T> Connection.customScalar( inline fun <reified T> Connection.customScalar(
query: String, query: String,
parameters: Collection<Parameter<*>>, parameters: Collection<Parameter<*>> = listOf(),
mapFunc: (ResultSet) -> T & Any mapFunc: (ResultSet) -> T & Any
) = Custom.scalar(query, parameters, this, mapFunc) ) = Custom.scalar(query, parameters, this, mapFunc)
// ~~~ DEFINITION QUERIES ~~~
/**
* Create a document table if necessary
*
* @param tableName The table whose existence should be ensured (may include schema)
*/
fun Connection.ensureTable(tableName: String) =
Definition.ensureTable(tableName, this)
/**
* Create an index on field(s) within documents in the specified table if necessary
*
* @param tableName The table to be indexed (may include schema)
* @param indexName The name of the index to create
* @param fields One or more fields to be indexed<
*/
fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
Definition.ensureFieldIndex(tableName, indexName, fields, this)
// ~~~ DOCUMENT MANIPULATION QUERIES ~~~
/**
* Insert a new document
*
* @param tableName The table into which the document should be inserted (may include schema)
* @param document The document to be inserted
*/
inline fun <reified TDoc> Connection.insert(tableName: String, document: TDoc) =
Document.insert(tableName, document, this)
// ~~~ DOCUMENT COUNT QUERIES ~~~
/**
* Count all documents in the table
*
* @param tableName The name of the table in which documents should be counted
* @return A count of the documents in the table
*/
fun Connection.countAll(tableName: String) =
Count.all(tableName, this)
// ~~~ DOCUMENT RETRIEVAL QUERIES ~~~
/**
* Retrieve all documents in the given table
*
* @param tableName The table from which documents should be retrieved
* @return A list of documents from the given table
*/
inline fun <reified TDoc> Connection.findAll(tableName: String) =
Find.all<TDoc>(tableName, this)
/**
* Retrieve all documents in the given table
*
* @param tableName The table from which documents should be retrieved
* @return A list of documents from the given table
*/
inline fun <reified TDoc> Connection.findAll(tableName: String, orderBy: Collection<Field<*>>) =
Find.all<TDoc>(tableName, orderBy, this)

29
src/main/kotlin/Count.kt Normal file
View File

@ -0,0 +1,29 @@
package solutions.bitbadger.documents
import solutions.bitbadger.documents.query.Count
import java.sql.Connection
/**
* Functions to count documents
*/
object Count {
/**
* Count all documents in the table
*
* @param tableName The name of the table in which documents should be counted
* @param conn The connection over which documents should be counted
* @return A count of the documents in the table
*/
fun all(tableName: String, conn: Connection) =
conn.customScalar(Count.all(tableName), mapFunc = Results::toCount)
/**
* Count all documents in the table
*
* @param tableName The name of the table in which documents should be counted
* @return A count of the documents in the table
*/
fun all(tableName: String) =
Configuration.dbConn().use { all(tableName, it) }
}

View File

@ -18,7 +18,7 @@ object Custom {
* @return A list of results for the given query * @return A list of results for the given query
*/ */
inline fun <reified TDoc> list( inline fun <reified TDoc> list(
query: String, parameters: Collection<Parameter<*>>, conn: Connection, mapFunc: (ResultSet) -> TDoc query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc
) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, mapFunc) } ) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, mapFunc) }
/** /**
@ -30,7 +30,7 @@ object Custom {
* @return A list of results for the given query * @return A list of results for the given query
*/ */
inline fun <reified TDoc> list( inline fun <reified TDoc> list(
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> TDoc query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) } ) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) }
/** /**
@ -43,7 +43,7 @@ object Custom {
* @return The document if one matches the query, `null` otherwise * @return The document if one matches the query, `null` otherwise
*/ */
inline fun <reified TDoc> single( inline fun <reified TDoc> single(
query: String, parameters: Collection<Parameter<*>>, conn: Connection, mapFunc: (ResultSet) -> TDoc query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc
) = list("$query LIMIT 1", parameters, conn, mapFunc).singleOrNull() ) = list("$query LIMIT 1", parameters, conn, mapFunc).singleOrNull()
/** /**
@ -55,7 +55,7 @@ object Custom {
* @return The document if one matches the query, `null` otherwise * @return The document if one matches the query, `null` otherwise
*/ */
inline fun <reified TDoc> single( inline fun <reified TDoc> single(
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> TDoc query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) } ) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) }
/** /**
@ -65,7 +65,7 @@ object Custom {
* @param conn The connection over which the query should be executed * @param conn The connection over which the query should be executed
* @param parameters Parameters to use for the query * @param parameters Parameters to use for the query
*/ */
fun nonQuery(query: String, parameters: Collection<Parameter<*>>, conn: Connection) { fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection) {
Parameters.apply(conn, query, parameters).use { it.executeUpdate() } Parameters.apply(conn, query, parameters).use { it.executeUpdate() }
} }
@ -75,7 +75,7 @@ object Custom {
* @param query The query to retrieve the results * @param query The query to retrieve the results
* @param parameters Parameters to use for the query * @param parameters Parameters to use for the query
*/ */
fun nonQuery(query: String, parameters: Collection<Parameter<*>>) = fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) =
Configuration.dbConn().use { nonQuery(query, parameters, it) } Configuration.dbConn().use { nonQuery(query, parameters, it) }
/** /**
@ -88,7 +88,7 @@ object Custom {
* @return The scalar value from the query * @return The scalar value from the query
*/ */
inline fun <reified T> scalar( inline fun <reified T> scalar(
query: String, parameters: Collection<Parameter<*>>, conn: Connection, mapFunc: (ResultSet) -> T & Any query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection, mapFunc: (ResultSet) -> T & Any
) = Parameters.apply(conn, query, parameters).use { stmt -> ) = Parameters.apply(conn, query, parameters).use { stmt ->
stmt.executeQuery().use { rs -> stmt.executeQuery().use { rs ->
rs.next() rs.next()
@ -105,6 +105,6 @@ object Custom {
* @return The scalar value from the query * @return The scalar value from the query
*/ */
inline fun <reified T> scalar( inline fun <reified T> scalar(
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> T & Any query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> T & Any
) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) } ) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) }
} }

View File

@ -0,0 +1,51 @@
package solutions.bitbadger.documents
import java.sql.Connection
import solutions.bitbadger.documents.query.Definition
/**
* Functions to define tables and indexes
*/
object Definition {
/**
* Create a document table if necessary
*
* @param tableName The table whose existence should be ensured (may include schema)
* @param conn The connection on which the query should be executed
*/
fun ensureTable(tableName: String, conn: Connection) =
Configuration.dialect("ensure $tableName exists").let {
conn.customNonQuery(Definition.ensureTable(tableName, it))
conn.customNonQuery(Definition.ensureKey(tableName, it))
}
/**
* Create a document table if necessary
*
* @param tableName The table whose existence should be ensured (may include schema)
*/
fun ensureTable(tableName: String) =
Configuration.dbConn().use { ensureTable(tableName, it) }
/**
* Create an index on field(s) within documents in the specified table if necessary
*
* @param tableName The table to be indexed (may include schema)
* @param indexName The name of the index to create
* @param fields One or more fields to be indexed<
* @param conn The connection on which the query should be executed
*/
fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>, conn: Connection) =
conn.customNonQuery(Definition.ensureIndexOn(tableName, indexName, fields))
/**
* Create an index on field(s) within documents in the specified table if necessary
* @param tableName The table to be indexed (may include schema)
* @param indexName The name of the index to create
* @param fields One or more fields to be indexed<
* @param conn The connection on which the query should be executed
*/
fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) }
}

View File

@ -0,0 +1,54 @@
package solutions.bitbadger.documents
import java.sql.Connection
import solutions.bitbadger.documents.query.Document
/**
* Functions for manipulating documents
*/
object Document {
/**
* Insert a new document
*
* @param tableName The table into which the document should be inserted (may include schema)
* @param document The document to be inserted
* @param conn The connection on which the query should be executed
*/
inline fun <reified TDoc> insert(tableName: String, document: TDoc, conn: Connection) {
val strategy = Configuration.autoIdStrategy
val query = if (strategy == AutoId.DISABLED) {
Document.insert(tableName)
} else {
val idField = Configuration.idField
val dialect = Configuration.dialect("Create auto-ID insert query")
val dataParam = if (AutoId.needsAutoId(strategy, document, idField)) {
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 {
when (dialect) {
Dialect.POSTGRESQL -> ":data::jsonb || ('{\"$idField\":$it}')::jsonb"
Dialect.SQLITE -> "json_set(:data, '$.$idField', $it)"
}
}
} else {
":data"
}
Document.insert(tableName).replace(":data", dataParam)
}
conn.customNonQuery(query, listOf(Parameters.json(":data", document)))
}
/**
* Insert a new document
*
* @param tableName The table into which the document should be inserted (may include schema)
* @param document The document to be inserted
*/
inline fun <reified TDoc> insert(tableName: String, document: TDoc) =
Configuration.dbConn().use { insert(tableName, document, it) }
}

52
src/main/kotlin/Find.kt Normal file
View File

@ -0,0 +1,52 @@
package solutions.bitbadger.documents
import java.sql.Connection
import solutions.bitbadger.documents.query.Find
import solutions.bitbadger.documents.query.orderBy
/**
* Functions to find and retrieve documents
*/
object Find {
/**
* Retrieve all documents in the given table
*
* @param tableName The table from which documents should be retrieved
* @param conn The connection over which documents should be retrieved
* @return A list of documents from the given table
*/
inline fun <reified TDoc> all(tableName: String, conn: Connection) =
conn.customList<TDoc>(Find.all(tableName), mapFunc = Results::fromData)
/**
* Retrieve all documents in the given table
*
* @param tableName The table from which documents should be retrieved
* @return A list of documents from the given table
*/
inline fun <reified TDoc> all(tableName: String) =
Configuration.dbConn().use { all<TDoc>(tableName, it) }
/**
* Retrieve all documents in the given table
*
* @param tableName The table from which documents should be retrieved
* @param orderBy Fields by which the query should be ordered
* @param conn The connection over which documents should be retrieved
* @return A list of documents from the given table
*/
inline fun <reified TDoc> all(tableName: String, orderBy: Collection<Field<*>>, conn: Connection) =
conn.customList<TDoc>(Find.all(tableName) + orderBy(orderBy), mapFunc = Results::fromData)
/**
* Retrieve all documents in the given table
*
* @param tableName The table from which documents should be retrieved
* @param orderBy Fields by which the query should be ordered
* @return A list of documents from the given table
*/
inline fun <reified TDoc> all(tableName: String, orderBy: Collection<Field<*>>) =
Configuration.dbConn().use { all<TDoc>(tableName, orderBy, it) }
}

View File

@ -12,4 +12,7 @@ class Parameter<T>(val name: String, val type: ParameterType, val value: T) {
if (!name.startsWith(':') && !name.startsWith('@')) if (!name.startsWith(':') && !name.startsWith('@'))
throw DocumentException("Name must start with : or @ ($name)") throw DocumentException("Name must start with : or @ ($name)")
} }
override fun toString() =
"$type[$name] = $value"
} }

View File

@ -29,6 +29,16 @@ object Parameters {
} }
} }
/**
* Create a parameter by encoding a JSON object
*
* @param name The parameter name
* @param value The object to be encoded as JSON
* @return A parameter with the value encoded
*/
inline fun <reified T> json(name: String, value: T) =
Parameter(name, ParameterType.JSON, Configuration.json.encodeToString(value))
/** /**
* Replace the parameter names in the query with question marks * Replace the parameter names in the query with question marks
* *
@ -93,7 +103,7 @@ object Parameters {
} }
} }
ParameterType.JSON -> stmt.setString(idx, Configuration.json.encodeToString(param.value)) ParameterType.JSON -> stmt.setString(idx, param.value as String)
} }
} }
} }

View File

@ -24,10 +24,11 @@ object Definition {
* SQL statement to create a document table in the current dialect * SQL statement to create a document table in the current dialect
* *
* @param tableName The name of the table to create (may include schema) * @param tableName The name of the table to create (may include schema)
* @param dialect The dialect to generate (optional, used in place of current)
* @return A query to create a document table * @return A query to create a document table
*/ */
fun ensureTable(tableName: String) = fun ensureTable(tableName: String, dialect: Dialect? = null) =
when (Configuration.dialect("create table creation query")) { when (dialect ?: Configuration.dialect("create table creation query")) {
Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB") Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB")
Dialect.SQLITE -> ensureTableFor(tableName, "TEXT") Dialect.SQLITE -> ensureTableFor(tableName, "TEXT")
} }
@ -47,15 +48,21 @@ object Definition {
* @param tableName The table on which an index should be created (may include schema) * @param tableName The table on which an index should be created (may include schema)
* @param indexName The name of the index to be created * @param indexName The name of the index to be created
* @param fields One or more fields to include in the index * @param fields One or more fields to include in the index
* @param dialect The SQL dialect to use when creating this index * @param dialect The SQL dialect to use when creating this index (optional, used in place of current)
* @return A query to create the field index * @return A query to create the field index
*/ */
fun ensureIndexOn(tableName: String, indexName: String, fields: Collection<String>, dialect: Dialect): String { fun ensureIndexOn(
tableName: String,
indexName: String,
fields: Collection<String>,
dialect: Dialect? = null
): String {
val (_, tbl) = splitSchemaAndTable(tableName) val (_, tbl) = splitSchemaAndTable(tableName)
val mode = dialect ?: Configuration.dialect("create index $tbl.$indexName")
val jsonFields = fields.joinToString(", ") { val jsonFields = fields.joinToString(", ") {
val parts = it.split(' ') val parts = it.split(' ')
val direction = if (parts.size > 1) " ${parts[1]}" else "" val direction = if (parts.size > 1) " ${parts[1]}" else ""
"(" + Field.nameToPath(parts[0], dialect, FieldFormat.SQL) + ")$direction" "(" + Field.nameToPath(parts[0], mode, FieldFormat.SQL) + ")$direction"
} }
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)" return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)"
} }
@ -64,9 +71,9 @@ object Definition {
* SQL statement to create a key index for a document table * SQL statement to create a key index for a document table
* *
* @param tableName The table on which a key index should be created (may include schema) * @param tableName The table on which a key index should be created (may include schema)
* @param dialect The SQL dialect to use when creating this index * @param dialect The SQL dialect to use when creating this index (optional, used in place of current)
* @return A query to create the key index * @return A query to create the key index
*/ */
fun ensureKey(tableName: String, dialect: Dialect) = fun ensureKey(tableName: String, dialect: Dialect? = null) =
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX") ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
} }

View File

@ -1,5 +1,6 @@
package solutions.bitbadger.documents.query package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.Dialect
import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.Field
import solutions.bitbadger.documents.FieldMatch import solutions.bitbadger.documents.FieldMatch
@ -44,7 +45,8 @@ fun byFields(statement: String, fields: Collection<Field<*>>, howMatched: FieldM
* @param dialect The SQL dialect for the generated clause * @param dialect The SQL dialect for the generated clause
* @return An `ORDER BY` clause for the given fields * @return An `ORDER BY` clause for the given fields
*/ */
fun orderBy(fields: Collection<Field<*>>, dialect: Dialect): String { fun orderBy(fields: Collection<Field<*>>, dialect: Dialect? = null): String {
val mode = dialect ?: Configuration.dialect("generate ORDER BY clause")
if (fields.isEmpty()) return "" if (fields.isEmpty()) return ""
val orderFields = fields.joinToString(", ") { val orderFields = fields.joinToString(", ") {
val (field, direction) = val (field, direction) =
@ -56,18 +58,18 @@ fun orderBy(fields: Collection<Field<*>>, dialect: Dialect): String {
} }
val path = when { val path = when {
field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld -> field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld ->
when (dialect) { when (mode) {
Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric" Dialect.POSTGRESQL -> "(${fld.path(mode)})::numeric"
Dialect.SQLITE -> fld.path(dialect) Dialect.SQLITE -> fld.path(mode)
} }
} }
field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(dialect).let { p -> field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(mode).let { p ->
when (dialect) { when (mode) {
Dialect.POSTGRESQL -> "LOWER($p)" Dialect.POSTGRESQL -> "LOWER($p)"
Dialect.SQLITE -> "$p COLLATE NOCASE" Dialect.SQLITE -> "$p COLLATE NOCASE"
} }
} }
else -> field.path(dialect) else -> field.path(mode)
} }
"$path${direction ?: ""}" "$path${direction ?: ""}"
} }