diff --git a/pom.xml b/pom.xml index cab9d3e..3e2ef74 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,12 @@ kotlinx-serialization-json ${serialization.version} + + org.xerial + sqlite-jdbc + 3.46.1.2 + integration-test + \ No newline at end of file diff --git a/src/integration-test/kotlin/CustomSQLiteIT.kt b/src/integration-test/kotlin/CustomSQLiteIT.kt index c15ff76..1b97baf 100644 --- a/src/integration-test/kotlin/CustomSQLiteIT.kt +++ b/src/integration-test/kotlin/CustomSQLiteIT.kt @@ -1,43 +1,103 @@ 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.Test -import solutions.bitbadger.documents.query.Definition +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 { - private val tbl = "test_table"; - - @BeforeEach - fun setUp() { - Configuration.connectionString = "jdbc:sqlite:memory" - } - - /** - * Reset the dialect - */ - @AfterEach - fun cleanUp() { - Configuration.dialectValue = null - } - @Test @DisplayName("list succeeds with empty list") - fun listEmpty() { - Configuration.dbConn().use { conn -> - conn.customNonQuery(Definition.ensureTable(tbl), listOf()) - conn.customNonQuery(Definition.ensureKey(tbl, Dialect.SQLITE), listOf()) - val result = conn.customList(Find.all(tbl), listOf(), Results::fromData) + fun listEmpty() = + SQLiteDB().use { db -> + JsonDocument.load(db.conn, SQLiteDB.tableName) + db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}") + val result = db.conn.customList(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData) assertEquals(0, result.size, "There should have been no results") } - } -} -@Serializable -data class TestDocument(val id: String) + @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(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(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" + ) + } +} diff --git a/src/integration-test/kotlin/DefinitionSQLiteIT.kt b/src/integration-test/kotlin/DefinitionSQLiteIT.kt new file mode 100644 index 0000000..4e27ffd --- /dev/null +++ b/src/integration-test/kotlin/DefinitionSQLiteIT.kt @@ -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") + } +} diff --git a/src/integration-test/kotlin/DocumentSQLiteIT.kt b/src/integration-test/kotlin/DocumentSQLiteIT.kt new file mode 100644 index 0000000..e4dd09a --- /dev/null +++ b/src/integration-test/kotlin/DocumentSQLiteIT.kt @@ -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(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(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 +} diff --git a/src/integration-test/kotlin/SQLiteDB.kt b/src/integration-test/kotlin/SQLiteDB.kt new file mode 100644 index 0000000..2f0118a --- /dev/null +++ b/src/integration-test/kotlin/SQLiteDB.kt @@ -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" + } +} diff --git a/src/integration-test/kotlin/Types.kt b/src/integration-test/kotlin/Types.kt new file mode 100644 index 0000000..a82af38 --- /dev/null +++ b/src/integration-test/kotlin/Types.kt @@ -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) { + 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) } + } +} diff --git a/src/main/kotlin/ConnectionExtensions.kt b/src/main/kotlin/ConnectionExtensions.kt index 553107b..8c87011 100644 --- a/src/main/kotlin/ConnectionExtensions.kt +++ b/src/main/kotlin/ConnectionExtensions.kt @@ -3,6 +3,8 @@ package solutions.bitbadger.documents import java.sql.Connection import java.sql.ResultSet +// ~~~ CUSTOM QUERIES ~~~ + /** * 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 */ inline fun Connection.customList( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Custom.list(query, parameters, this, mapFunc) /** @@ -24,7 +26,7 @@ inline fun Connection.customList( * @return The document if one matches the query, `null` otherwise */ inline fun Connection.customSingle( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Custom.single(query, parameters, this, mapFunc) /** @@ -33,7 +35,7 @@ inline fun Connection.customSingle( * @param query The query to retrieve the results * @param parameters Parameters to use for the query */ -fun Connection.customNonQuery(query: String, parameters: Collection>) = +fun Connection.customNonQuery(query: String, parameters: Collection> = listOf()) = Custom.nonQuery(query, parameters, this) /** @@ -46,6 +48,69 @@ fun Connection.customNonQuery(query: String, parameters: Collection */ inline fun Connection.customScalar( query: String, - parameters: Collection>, + parameters: Collection> = listOf(), mapFunc: (ResultSet) -> T & Any ) = 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) = + 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 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 Connection.findAll(tableName: String) = + Find.all(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 Connection.findAll(tableName: String, orderBy: Collection>) = + Find.all(tableName, orderBy, this) diff --git a/src/main/kotlin/Count.kt b/src/main/kotlin/Count.kt new file mode 100644 index 0000000..c168a22 --- /dev/null +++ b/src/main/kotlin/Count.kt @@ -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) } +} diff --git a/src/main/kotlin/Custom.kt b/src/main/kotlin/Custom.kt index 87aa352..1902aec 100644 --- a/src/main/kotlin/Custom.kt +++ b/src/main/kotlin/Custom.kt @@ -18,7 +18,7 @@ object Custom { * @return A list of results for the given query */ inline fun list( - query: String, parameters: Collection>, conn: Connection, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc ) = 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 */ inline fun list( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) } /** @@ -43,7 +43,7 @@ object Custom { * @return The document if one matches the query, `null` otherwise */ inline fun single( - query: String, parameters: Collection>, conn: Connection, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc ) = list("$query LIMIT 1", parameters, conn, mapFunc).singleOrNull() /** @@ -55,7 +55,7 @@ object Custom { * @return The document if one matches the query, `null` otherwise */ inline fun single( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> TDoc + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> TDoc ) = 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 parameters Parameters to use for the query */ - fun nonQuery(query: String, parameters: Collection>, conn: Connection) { + fun nonQuery(query: String, parameters: Collection> = listOf(), conn: Connection) { Parameters.apply(conn, query, parameters).use { it.executeUpdate() } } @@ -75,7 +75,7 @@ object Custom { * @param query The query to retrieve the results * @param parameters Parameters to use for the query */ - fun nonQuery(query: String, parameters: Collection>) = + fun nonQuery(query: String, parameters: Collection> = listOf()) = Configuration.dbConn().use { nonQuery(query, parameters, it) } /** @@ -88,7 +88,7 @@ object Custom { * @return The scalar value from the query */ inline fun scalar( - query: String, parameters: Collection>, conn: Connection, mapFunc: (ResultSet) -> T & Any + query: String, parameters: Collection> = listOf(), conn: Connection, mapFunc: (ResultSet) -> T & Any ) = Parameters.apply(conn, query, parameters).use { stmt -> stmt.executeQuery().use { rs -> rs.next() @@ -105,6 +105,6 @@ object Custom { * @return The scalar value from the query */ inline fun scalar( - query: String, parameters: Collection>, mapFunc: (ResultSet) -> T & Any + query: String, parameters: Collection> = listOf(), mapFunc: (ResultSet) -> T & Any ) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) } } diff --git a/src/main/kotlin/Definition.kt b/src/main/kotlin/Definition.kt new file mode 100644 index 0000000..9ad8da0 --- /dev/null +++ b/src/main/kotlin/Definition.kt @@ -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, 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) = + Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) } +} diff --git a/src/main/kotlin/Document.kt b/src/main/kotlin/Document.kt new file mode 100644 index 0000000..1b6ab3f --- /dev/null +++ b/src/main/kotlin/Document.kt @@ -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 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 insert(tableName: String, document: TDoc) = + Configuration.dbConn().use { insert(tableName, document, it) } +} diff --git a/src/main/kotlin/Find.kt b/src/main/kotlin/Find.kt new file mode 100644 index 0000000..b9131ea --- /dev/null +++ b/src/main/kotlin/Find.kt @@ -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 all(tableName: String, conn: Connection) = + conn.customList(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 all(tableName: String) = + Configuration.dbConn().use { all(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 all(tableName: String, orderBy: Collection>, conn: Connection) = + conn.customList(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 all(tableName: String, orderBy: Collection>) = + Configuration.dbConn().use { all(tableName, orderBy, it) } + +} diff --git a/src/main/kotlin/Parameter.kt b/src/main/kotlin/Parameter.kt index 97fe766..0503812 100644 --- a/src/main/kotlin/Parameter.kt +++ b/src/main/kotlin/Parameter.kt @@ -12,4 +12,7 @@ class Parameter(val name: String, val type: ParameterType, val value: T) { if (!name.startsWith(':') && !name.startsWith('@')) throw DocumentException("Name must start with : or @ ($name)") } + + override fun toString() = + "$type[$name] = $value" } diff --git a/src/main/kotlin/Parameters.kt b/src/main/kotlin/Parameters.kt index c44cd34..912e9f8 100644 --- a/src/main/kotlin/Parameters.kt +++ b/src/main/kotlin/Parameters.kt @@ -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 json(name: String, value: T) = + Parameter(name, ParameterType.JSON, Configuration.json.encodeToString(value)) + /** * 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) } } } diff --git a/src/main/kotlin/query/Definition.kt b/src/main/kotlin/query/Definition.kt index b2425e4..8180383 100644 --- a/src/main/kotlin/query/Definition.kt +++ b/src/main/kotlin/query/Definition.kt @@ -24,10 +24,11 @@ object Definition { * SQL statement to create a document table in the current dialect * * @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 */ - fun ensureTable(tableName: String) = - when (Configuration.dialect("create table creation query")) { + fun ensureTable(tableName: String, dialect: Dialect? = null) = + when (dialect ?: Configuration.dialect("create table creation query")) { Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB") 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 indexName The name of the index to be created * @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 */ - fun ensureIndexOn(tableName: String, indexName: String, fields: Collection, dialect: Dialect): String { + fun ensureIndexOn( + tableName: String, + indexName: String, + fields: Collection, + dialect: Dialect? = null + ): String { val (_, tbl) = splitSchemaAndTable(tableName) + val mode = dialect ?: Configuration.dialect("create index $tbl.$indexName") val jsonFields = fields.joinToString(", ") { val parts = it.split(' ') 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)" } @@ -64,9 +71,9 @@ object Definition { * 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 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 */ - fun ensureKey(tableName: String, dialect: Dialect) = + fun ensureKey(tableName: String, dialect: Dialect? = null) = ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX") } diff --git a/src/main/kotlin/query/Query.kt b/src/main/kotlin/query/Query.kt index 20e6096..39784bc 100644 --- a/src/main/kotlin/query/Query.kt +++ b/src/main/kotlin/query/Query.kt @@ -1,5 +1,6 @@ package solutions.bitbadger.documents.query +import solutions.bitbadger.documents.Configuration import solutions.bitbadger.documents.Dialect import solutions.bitbadger.documents.Field import solutions.bitbadger.documents.FieldMatch @@ -44,7 +45,8 @@ fun byFields(statement: String, fields: Collection>, howMatched: FieldM * @param dialect The SQL dialect for the generated clause * @return An `ORDER BY` clause for the given fields */ -fun orderBy(fields: Collection>, dialect: Dialect): String { +fun orderBy(fields: Collection>, dialect: Dialect? = null): String { + val mode = dialect ?: Configuration.dialect("generate ORDER BY clause") if (fields.isEmpty()) return "" val orderFields = fields.joinToString(", ") { val (field, direction) = @@ -56,18 +58,18 @@ fun orderBy(fields: Collection>, dialect: Dialect): String { } val path = when { field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld -> - when (dialect) { - Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric" - Dialect.SQLITE -> fld.path(dialect) + when (mode) { + Dialect.POSTGRESQL -> "(${fld.path(mode)})::numeric" + Dialect.SQLITE -> fld.path(mode) } } - field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(dialect).let { p -> - when (dialect) { + field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(mode).let { p -> + when (mode) { Dialect.POSTGRESQL -> "LOWER($p)" Dialect.SQLITE -> "$p COLLATE NOCASE" } } - else -> field.path(dialect) + else -> field.path(mode) } "$path${direction ?: ""}" }