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 ?: ""}"
}