Initial Development #1
							
								
								
									
										6
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								pom.xml
									
									
									
									
									
								
							| @ -107,6 +107,12 @@ | ||||
|             <artifactId>kotlinx-serialization-json</artifactId> | ||||
|             <version>${serialization.version}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>org.xerial</groupId> | ||||
|             <artifactId>sqlite-jdbc</artifactId> | ||||
|             <version>3.46.1.2</version> | ||||
|             <scope>integration-test</scope> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
| 
 | ||||
| </project> | ||||
| @ -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 | ||||
| 
 | ||||
| 
 | ||||
| class CustomSQLiteIT { | ||||
| 
 | ||||
|     private val tbl = "test_table"; | ||||
| 
 | ||||
|     @BeforeEach | ||||
|     fun setUp() { | ||||
|         Configuration.connectionString = "jdbc:sqlite:memory" | ||||
|     } | ||||
| import kotlin.test.assertNotNull | ||||
| import kotlin.test.assertNull | ||||
| 
 | ||||
| /** | ||||
|      * Reset the dialect | ||||
|  * SQLite integration tests for the `Custom` object / `custom*` connection extension functions | ||||
|  */ | ||||
|     @AfterEach | ||||
|     fun cleanUp() { | ||||
|         Configuration.dialectValue = null | ||||
|     } | ||||
| @DisplayName("SQLite - Custom") | ||||
| class CustomSQLiteIT { | ||||
| 
 | ||||
|     @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<TestDocument>(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<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData) | ||||
|             assertEquals(0, result.size, "There should have been no results") | ||||
|         } | ||||
| 
 | ||||
|     @Test | ||||
|     @DisplayName("list succeeds with a non-empty list") | ||||
|     fun listAll() = | ||||
|         SQLiteDB().use { db -> | ||||
|             JsonDocument.load(db.conn, SQLiteDB.tableName) | ||||
|             val result = db.conn.customList<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData) | ||||
|             assertEquals(5, result.size, "There should have been 5 results") | ||||
|         } | ||||
| 
 | ||||
|     @Test | ||||
|     @DisplayName("single succeeds when document not found") | ||||
|     fun singleNone() = | ||||
|         SQLiteDB().use { db -> | ||||
|             assertNull( | ||||
|                 db.conn.customSingle(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData), | ||||
|                 "There should not have been a document returned" | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|     @Test | ||||
|     @DisplayName("single succeeds when a document is found") | ||||
|     fun singleOne() { | ||||
|         SQLiteDB().use { db -> | ||||
|             JsonDocument.load(db.conn, SQLiteDB.tableName) | ||||
|             assertNotNull( | ||||
|                 db.conn.customSingle<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData), | ||||
|                 "There should not have been a document returned" | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @Serializable | ||||
| data class TestDocument(val id: String) | ||||
|     @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" | ||||
|             ) | ||||
|         } | ||||
| } | ||||
|  | ||||
							
								
								
									
										45
									
								
								src/integration-test/kotlin/DefinitionSQLiteIT.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/integration-test/kotlin/DefinitionSQLiteIT.kt
									
									
									
									
									
										Normal 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") | ||||
|         } | ||||
| } | ||||
							
								
								
									
										66
									
								
								src/integration-test/kotlin/DocumentSQLiteIT.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/integration-test/kotlin/DocumentSQLiteIT.kt
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										36
									
								
								src/integration-test/kotlin/SQLiteDB.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/integration-test/kotlin/SQLiteDB.kt
									
									
									
									
									
										Normal 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" | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/integration-test/kotlin/Types.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/integration-test/kotlin/Types.kt
									
									
									
									
									
										Normal 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) } | ||||
|     } | ||||
| } | ||||
| @ -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 <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) | ||||
| 
 | ||||
| /** | ||||
| @ -24,7 +26,7 @@ inline fun <reified TDoc> Connection.customList( | ||||
|  * @return The document if one matches the query, `null` otherwise | ||||
|  */ | ||||
| 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) | ||||
| 
 | ||||
| /** | ||||
| @ -33,7 +35,7 @@ inline fun <reified TDoc> 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<Parameter<*>>) = | ||||
| fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) = | ||||
|     Custom.nonQuery(query, parameters, this) | ||||
| 
 | ||||
| /** | ||||
| @ -46,6 +48,69 @@ fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*> | ||||
|  */ | ||||
| inline fun <reified T> Connection.customScalar( | ||||
|     query: String, | ||||
|     parameters: Collection<Parameter<*>>, | ||||
|     parameters: Collection<Parameter<*>> = 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<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
									
								
							
							
						
						
									
										29
									
								
								src/main/kotlin/Count.kt
									
									
									
									
									
										Normal 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) } | ||||
| } | ||||
| @ -18,7 +18,7 @@ object Custom { | ||||
|      * @return A list of results for the given query | ||||
|      */ | ||||
|     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) } | ||||
| 
 | ||||
|     /** | ||||
| @ -30,7 +30,7 @@ object Custom { | ||||
|      * @return A list of results for the given query | ||||
|      */ | ||||
|     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) } | ||||
| 
 | ||||
|     /** | ||||
| @ -43,7 +43,7 @@ object Custom { | ||||
|      * @return The document if one matches the query, `null` otherwise | ||||
|      */ | ||||
|     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() | ||||
| 
 | ||||
|     /** | ||||
| @ -55,7 +55,7 @@ object Custom { | ||||
|      * @return The document if one matches the query, `null` otherwise | ||||
|      */ | ||||
|     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) } | ||||
| 
 | ||||
|     /** | ||||
| @ -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<Parameter<*>>, conn: Connection) { | ||||
|     fun nonQuery(query: String, parameters: Collection<Parameter<*>> = 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<Parameter<*>>) = | ||||
|     fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) = | ||||
|         Configuration.dbConn().use { nonQuery(query, parameters, it) } | ||||
| 
 | ||||
|     /** | ||||
| @ -88,7 +88,7 @@ object Custom { | ||||
|      * @return The scalar value from the query | ||||
|      */ | ||||
|     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 -> | ||||
|         stmt.executeQuery().use { rs -> | ||||
|             rs.next() | ||||
| @ -105,6 +105,6 @@ object Custom { | ||||
|      * @return The scalar value from the query | ||||
|      */ | ||||
|     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) } | ||||
| } | ||||
|  | ||||
							
								
								
									
										51
									
								
								src/main/kotlin/Definition.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/main/kotlin/Definition.kt
									
									
									
									
									
										Normal 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) } | ||||
| } | ||||
							
								
								
									
										54
									
								
								src/main/kotlin/Document.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/main/kotlin/Document.kt
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										52
									
								
								src/main/kotlin/Find.kt
									
									
									
									
									
										Normal 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) } | ||||
| 
 | ||||
| } | ||||
| @ -12,4 +12,7 @@ class Parameter<T>(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" | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
|      * | ||||
| @ -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) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
| @ -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<String>, dialect: Dialect): String { | ||||
|     fun ensureIndexOn( | ||||
|         tableName: String, | ||||
|         indexName: String, | ||||
|         fields: Collection<String>, | ||||
|         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") | ||||
| } | ||||
|  | ||||
| @ -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<Field<*>>, howMatched: FieldM | ||||
|  * @param dialect The SQL dialect for the generated clause | ||||
|  * @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 "" | ||||
|     val orderFields = fields.joinToString(", ") { | ||||
|         val (field, direction) = | ||||
| @ -56,18 +58,18 @@ fun orderBy(fields: Collection<Field<*>>, 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 ?: ""}" | ||||
|     } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user