Initial Development #1
6
pom.xml
6
pom.xml
@ -107,6 +107,12 @@
|
|||||||
<artifactId>kotlinx-serialization-json</artifactId>
|
<artifactId>kotlinx-serialization-json</artifactId>
|
||||||
<version>${serialization.version}</version>
|
<version>${serialization.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>3.46.1.2</version>
|
||||||
|
<scope>integration-test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
@ -1,43 +1,103 @@
|
|||||||
package solutions.bitbadger.documents
|
package solutions.bitbadger.documents
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.junit.jupiter.api.AfterEach
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import solutions.bitbadger.documents.query.Count
|
||||||
import solutions.bitbadger.documents.query.Definition
|
|
||||||
import solutions.bitbadger.documents.query.Find
|
import solutions.bitbadger.documents.query.Find
|
||||||
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
/**
|
||||||
class CustomSQLiteIT {
|
* SQLite integration tests for the `Custom` object / `custom*` connection extension functions
|
||||||
|
|
||||||
private val tbl = "test_table";
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun setUp() {
|
|
||||||
Configuration.connectionString = "jdbc:sqlite:memory"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the dialect
|
|
||||||
*/
|
*/
|
||||||
@AfterEach
|
@DisplayName("SQLite - Custom")
|
||||||
fun cleanUp() {
|
class CustomSQLiteIT {
|
||||||
Configuration.dialectValue = null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("list succeeds with empty list")
|
@DisplayName("list succeeds with empty list")
|
||||||
fun listEmpty() {
|
fun listEmpty() =
|
||||||
Configuration.dbConn().use { conn ->
|
SQLiteDB().use { db ->
|
||||||
conn.customNonQuery(Definition.ensureTable(tbl), listOf())
|
JsonDocument.load(db.conn, SQLiteDB.tableName)
|
||||||
conn.customNonQuery(Definition.ensureKey(tbl, Dialect.SQLITE), listOf())
|
db.conn.customNonQuery("DELETE FROM ${SQLiteDB.tableName}")
|
||||||
val result = conn.customList<TestDocument>(Find.all(tbl), listOf(), Results::fromData)
|
val result = db.conn.customList<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData)
|
||||||
assertEquals(0, result.size, "There should have been no results")
|
assertEquals(0, result.size, "There should have been no results")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("list succeeds with a non-empty list")
|
||||||
|
fun listAll() =
|
||||||
|
SQLiteDB().use { db ->
|
||||||
|
JsonDocument.load(db.conn, SQLiteDB.tableName)
|
||||||
|
val result = db.conn.customList<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData)
|
||||||
|
assertEquals(5, result.size, "There should have been 5 results")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("single succeeds when document not found")
|
||||||
|
fun singleNone() =
|
||||||
|
SQLiteDB().use { db ->
|
||||||
|
assertNull(
|
||||||
|
db.conn.customSingle(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData),
|
||||||
|
"There should not have been a document returned"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("single succeeds when a document is found")
|
||||||
|
fun singleOne() {
|
||||||
|
SQLiteDB().use { db ->
|
||||||
|
JsonDocument.load(db.conn, SQLiteDB.tableName)
|
||||||
|
assertNotNull(
|
||||||
|
db.conn.customSingle<JsonDocument>(Find.all(SQLiteDB.tableName), mapFunc = Results::fromData),
|
||||||
|
"There should not have been a document returned"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TestDocument(val id: String)
|
|
||||||
|
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.Connection
|
||||||
import java.sql.ResultSet
|
import java.sql.ResultSet
|
||||||
|
|
||||||
|
// ~~~ CUSTOM QUERIES ~~~
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a query that returns a list of results
|
* Execute a query that returns a list of results
|
||||||
*
|
*
|
||||||
@ -12,7 +14,7 @@ import java.sql.ResultSet
|
|||||||
* @return A list of results for the given query
|
* @return A list of results for the given query
|
||||||
*/
|
*/
|
||||||
inline fun <reified TDoc> Connection.customList(
|
inline fun <reified TDoc> Connection.customList(
|
||||||
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> TDoc
|
query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
|
||||||
) = Custom.list(query, parameters, this, mapFunc)
|
) = Custom.list(query, parameters, this, mapFunc)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,7 +26,7 @@ inline fun <reified TDoc> Connection.customList(
|
|||||||
* @return The document if one matches the query, `null` otherwise
|
* @return The document if one matches the query, `null` otherwise
|
||||||
*/
|
*/
|
||||||
inline fun <reified TDoc> Connection.customSingle(
|
inline fun <reified TDoc> Connection.customSingle(
|
||||||
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> TDoc
|
query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
|
||||||
) = Custom.single(query, parameters, this, mapFunc)
|
) = Custom.single(query, parameters, this, mapFunc)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,7 +35,7 @@ inline fun <reified TDoc> Connection.customSingle(
|
|||||||
* @param query The query to retrieve the results
|
* @param query The query to retrieve the results
|
||||||
* @param parameters Parameters to use for the query
|
* @param parameters Parameters to use for the query
|
||||||
*/
|
*/
|
||||||
fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>>) =
|
fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) =
|
||||||
Custom.nonQuery(query, parameters, this)
|
Custom.nonQuery(query, parameters, this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,6 +48,69 @@ fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>
|
|||||||
*/
|
*/
|
||||||
inline fun <reified T> Connection.customScalar(
|
inline fun <reified T> Connection.customScalar(
|
||||||
query: String,
|
query: String,
|
||||||
parameters: Collection<Parameter<*>>,
|
parameters: Collection<Parameter<*>> = listOf(),
|
||||||
mapFunc: (ResultSet) -> T & Any
|
mapFunc: (ResultSet) -> T & Any
|
||||||
) = Custom.scalar(query, parameters, this, mapFunc)
|
) = Custom.scalar(query, parameters, this, mapFunc)
|
||||||
|
|
||||||
|
// ~~~ DEFINITION QUERIES ~~~
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a document table if necessary
|
||||||
|
*
|
||||||
|
* @param tableName The table whose existence should be ensured (may include schema)
|
||||||
|
*/
|
||||||
|
fun Connection.ensureTable(tableName: String) =
|
||||||
|
Definition.ensureTable(tableName, this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an index on field(s) within documents in the specified table if necessary
|
||||||
|
*
|
||||||
|
* @param tableName The table to be indexed (may include schema)
|
||||||
|
* @param indexName The name of the index to create
|
||||||
|
* @param fields One or more fields to be indexed<
|
||||||
|
*/
|
||||||
|
fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
|
||||||
|
Definition.ensureFieldIndex(tableName, indexName, fields, this)
|
||||||
|
|
||||||
|
// ~~~ DOCUMENT MANIPULATION QUERIES ~~~
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new document
|
||||||
|
*
|
||||||
|
* @param tableName The table into which the document should be inserted (may include schema)
|
||||||
|
* @param document The document to be inserted
|
||||||
|
*/
|
||||||
|
inline fun <reified TDoc> Connection.insert(tableName: String, document: TDoc) =
|
||||||
|
Document.insert(tableName, document, this)
|
||||||
|
|
||||||
|
// ~~~ DOCUMENT COUNT QUERIES ~~~
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count all documents in the table
|
||||||
|
*
|
||||||
|
* @param tableName The name of the table in which documents should be counted
|
||||||
|
* @return A count of the documents in the table
|
||||||
|
*/
|
||||||
|
fun Connection.countAll(tableName: String) =
|
||||||
|
Count.all(tableName, this)
|
||||||
|
|
||||||
|
|
||||||
|
// ~~~ DOCUMENT RETRIEVAL QUERIES ~~~
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all documents in the given table
|
||||||
|
*
|
||||||
|
* @param tableName The table from which documents should be retrieved
|
||||||
|
* @return A list of documents from the given table
|
||||||
|
*/
|
||||||
|
inline fun <reified TDoc> Connection.findAll(tableName: String) =
|
||||||
|
Find.all<TDoc>(tableName, this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all documents in the given table
|
||||||
|
*
|
||||||
|
* @param tableName The table from which documents should be retrieved
|
||||||
|
* @return A list of documents from the given table
|
||||||
|
*/
|
||||||
|
inline fun <reified TDoc> Connection.findAll(tableName: String, orderBy: Collection<Field<*>>) =
|
||||||
|
Find.all<TDoc>(tableName, orderBy, this)
|
||||||
|
29
src/main/kotlin/Count.kt
Normal file
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
|
* @return A list of results for the given query
|
||||||
*/
|
*/
|
||||||
inline fun <reified TDoc> list(
|
inline fun <reified TDoc> list(
|
||||||
query: String, parameters: Collection<Parameter<*>>, conn: Connection, mapFunc: (ResultSet) -> TDoc
|
query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc
|
||||||
) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, mapFunc) }
|
) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, mapFunc) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,7 +30,7 @@ object Custom {
|
|||||||
* @return A list of results for the given query
|
* @return A list of results for the given query
|
||||||
*/
|
*/
|
||||||
inline fun <reified TDoc> list(
|
inline fun <reified TDoc> list(
|
||||||
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> TDoc
|
query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
|
||||||
) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) }
|
) = Configuration.dbConn().use { list(query, parameters, it, mapFunc) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,7 +43,7 @@ object Custom {
|
|||||||
* @return The document if one matches the query, `null` otherwise
|
* @return The document if one matches the query, `null` otherwise
|
||||||
*/
|
*/
|
||||||
inline fun <reified TDoc> single(
|
inline fun <reified TDoc> single(
|
||||||
query: String, parameters: Collection<Parameter<*>>, conn: Connection, mapFunc: (ResultSet) -> TDoc
|
query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection, mapFunc: (ResultSet) -> TDoc
|
||||||
) = list("$query LIMIT 1", parameters, conn, mapFunc).singleOrNull()
|
) = list("$query LIMIT 1", parameters, conn, mapFunc).singleOrNull()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,7 +55,7 @@ object Custom {
|
|||||||
* @return The document if one matches the query, `null` otherwise
|
* @return The document if one matches the query, `null` otherwise
|
||||||
*/
|
*/
|
||||||
inline fun <reified TDoc> single(
|
inline fun <reified TDoc> single(
|
||||||
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> TDoc
|
query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> TDoc
|
||||||
) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) }
|
) = Configuration.dbConn().use { single(query, parameters, it, mapFunc) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,7 +65,7 @@ object Custom {
|
|||||||
* @param conn The connection over which the query should be executed
|
* @param conn The connection over which the query should be executed
|
||||||
* @param parameters Parameters to use for the query
|
* @param parameters Parameters to use for the query
|
||||||
*/
|
*/
|
||||||
fun nonQuery(query: String, parameters: Collection<Parameter<*>>, conn: Connection) {
|
fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection) {
|
||||||
Parameters.apply(conn, query, parameters).use { it.executeUpdate() }
|
Parameters.apply(conn, query, parameters).use { it.executeUpdate() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ object Custom {
|
|||||||
* @param query The query to retrieve the results
|
* @param query The query to retrieve the results
|
||||||
* @param parameters Parameters to use for the query
|
* @param parameters Parameters to use for the query
|
||||||
*/
|
*/
|
||||||
fun nonQuery(query: String, parameters: Collection<Parameter<*>>) =
|
fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) =
|
||||||
Configuration.dbConn().use { nonQuery(query, parameters, it) }
|
Configuration.dbConn().use { nonQuery(query, parameters, it) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,7 +88,7 @@ object Custom {
|
|||||||
* @return The scalar value from the query
|
* @return The scalar value from the query
|
||||||
*/
|
*/
|
||||||
inline fun <reified T> scalar(
|
inline fun <reified T> scalar(
|
||||||
query: String, parameters: Collection<Parameter<*>>, conn: Connection, mapFunc: (ResultSet) -> T & Any
|
query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection, mapFunc: (ResultSet) -> T & Any
|
||||||
) = Parameters.apply(conn, query, parameters).use { stmt ->
|
) = Parameters.apply(conn, query, parameters).use { stmt ->
|
||||||
stmt.executeQuery().use { rs ->
|
stmt.executeQuery().use { rs ->
|
||||||
rs.next()
|
rs.next()
|
||||||
@ -105,6 +105,6 @@ object Custom {
|
|||||||
* @return The scalar value from the query
|
* @return The scalar value from the query
|
||||||
*/
|
*/
|
||||||
inline fun <reified T> scalar(
|
inline fun <reified T> scalar(
|
||||||
query: String, parameters: Collection<Parameter<*>>, mapFunc: (ResultSet) -> T & Any
|
query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> T & Any
|
||||||
) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) }
|
) = Configuration.dbConn().use { scalar(query, parameters, it, mapFunc) }
|
||||||
}
|
}
|
||||||
|
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('@'))
|
if (!name.startsWith(':') && !name.startsWith('@'))
|
||||||
throw DocumentException("Name must start with : or @ ($name)")
|
throw DocumentException("Name must start with : or @ ($name)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString() =
|
||||||
|
"$type[$name] = $value"
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,16 @@ object Parameters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a parameter by encoding a JSON object
|
||||||
|
*
|
||||||
|
* @param name The parameter name
|
||||||
|
* @param value The object to be encoded as JSON
|
||||||
|
* @return A parameter with the value encoded
|
||||||
|
*/
|
||||||
|
inline fun <reified T> json(name: String, value: T) =
|
||||||
|
Parameter(name, ParameterType.JSON, Configuration.json.encodeToString(value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the parameter names in the query with question marks
|
* Replace the parameter names in the query with question marks
|
||||||
*
|
*
|
||||||
@ -93,7 +103,7 @@ object Parameters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ParameterType.JSON -> stmt.setString(idx, Configuration.json.encodeToString(param.value))
|
ParameterType.JSON -> stmt.setString(idx, param.value as String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,11 @@ object Definition {
|
|||||||
* SQL statement to create a document table in the current dialect
|
* SQL statement to create a document table in the current dialect
|
||||||
*
|
*
|
||||||
* @param tableName The name of the table to create (may include schema)
|
* @param tableName The name of the table to create (may include schema)
|
||||||
|
* @param dialect The dialect to generate (optional, used in place of current)
|
||||||
* @return A query to create a document table
|
* @return A query to create a document table
|
||||||
*/
|
*/
|
||||||
fun ensureTable(tableName: String) =
|
fun ensureTable(tableName: String, dialect: Dialect? = null) =
|
||||||
when (Configuration.dialect("create table creation query")) {
|
when (dialect ?: Configuration.dialect("create table creation query")) {
|
||||||
Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB")
|
Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB")
|
||||||
Dialect.SQLITE -> ensureTableFor(tableName, "TEXT")
|
Dialect.SQLITE -> ensureTableFor(tableName, "TEXT")
|
||||||
}
|
}
|
||||||
@ -47,15 +48,21 @@ object Definition {
|
|||||||
* @param tableName The table on which an index should be created (may include schema)
|
* @param tableName The table on which an index should be created (may include schema)
|
||||||
* @param indexName The name of the index to be created
|
* @param indexName The name of the index to be created
|
||||||
* @param fields One or more fields to include in the index
|
* @param fields One or more fields to include in the index
|
||||||
* @param dialect The SQL dialect to use when creating this index
|
* @param dialect The SQL dialect to use when creating this index (optional, used in place of current)
|
||||||
* @return A query to create the field index
|
* @return A query to create the field index
|
||||||
*/
|
*/
|
||||||
fun ensureIndexOn(tableName: String, indexName: String, fields: Collection<String>, dialect: Dialect): String {
|
fun ensureIndexOn(
|
||||||
|
tableName: String,
|
||||||
|
indexName: String,
|
||||||
|
fields: Collection<String>,
|
||||||
|
dialect: Dialect? = null
|
||||||
|
): String {
|
||||||
val (_, tbl) = splitSchemaAndTable(tableName)
|
val (_, tbl) = splitSchemaAndTable(tableName)
|
||||||
|
val mode = dialect ?: Configuration.dialect("create index $tbl.$indexName")
|
||||||
val jsonFields = fields.joinToString(", ") {
|
val jsonFields = fields.joinToString(", ") {
|
||||||
val parts = it.split(' ')
|
val parts = it.split(' ')
|
||||||
val direction = if (parts.size > 1) " ${parts[1]}" else ""
|
val direction = if (parts.size > 1) " ${parts[1]}" else ""
|
||||||
"(" + Field.nameToPath(parts[0], dialect, FieldFormat.SQL) + ")$direction"
|
"(" + Field.nameToPath(parts[0], mode, FieldFormat.SQL) + ")$direction"
|
||||||
}
|
}
|
||||||
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)"
|
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)"
|
||||||
}
|
}
|
||||||
@ -64,9 +71,9 @@ object Definition {
|
|||||||
* SQL statement to create a key index for a document table
|
* SQL statement to create a key index for a document table
|
||||||
*
|
*
|
||||||
* @param tableName The table on which a key index should be created (may include schema)
|
* @param tableName The table on which a key index should be created (may include schema)
|
||||||
* @param dialect The SQL dialect to use when creating this index
|
* @param dialect The SQL dialect to use when creating this index (optional, used in place of current)
|
||||||
* @return A query to create the key index
|
* @return A query to create the key index
|
||||||
*/
|
*/
|
||||||
fun ensureKey(tableName: String, dialect: Dialect) =
|
fun ensureKey(tableName: String, dialect: Dialect? = null) =
|
||||||
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
|
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package solutions.bitbadger.documents.query
|
package solutions.bitbadger.documents.query
|
||||||
|
|
||||||
|
import solutions.bitbadger.documents.Configuration
|
||||||
import solutions.bitbadger.documents.Dialect
|
import solutions.bitbadger.documents.Dialect
|
||||||
import solutions.bitbadger.documents.Field
|
import solutions.bitbadger.documents.Field
|
||||||
import solutions.bitbadger.documents.FieldMatch
|
import solutions.bitbadger.documents.FieldMatch
|
||||||
@ -44,7 +45,8 @@ fun byFields(statement: String, fields: Collection<Field<*>>, howMatched: FieldM
|
|||||||
* @param dialect The SQL dialect for the generated clause
|
* @param dialect The SQL dialect for the generated clause
|
||||||
* @return An `ORDER BY` clause for the given fields
|
* @return An `ORDER BY` clause for the given fields
|
||||||
*/
|
*/
|
||||||
fun orderBy(fields: Collection<Field<*>>, dialect: Dialect): String {
|
fun orderBy(fields: Collection<Field<*>>, dialect: Dialect? = null): String {
|
||||||
|
val mode = dialect ?: Configuration.dialect("generate ORDER BY clause")
|
||||||
if (fields.isEmpty()) return ""
|
if (fields.isEmpty()) return ""
|
||||||
val orderFields = fields.joinToString(", ") {
|
val orderFields = fields.joinToString(", ") {
|
||||||
val (field, direction) =
|
val (field, direction) =
|
||||||
@ -56,18 +58,18 @@ fun orderBy(fields: Collection<Field<*>>, dialect: Dialect): String {
|
|||||||
}
|
}
|
||||||
val path = when {
|
val path = when {
|
||||||
field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld ->
|
field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld ->
|
||||||
when (dialect) {
|
when (mode) {
|
||||||
Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric"
|
Dialect.POSTGRESQL -> "(${fld.path(mode)})::numeric"
|
||||||
Dialect.SQLITE -> fld.path(dialect)
|
Dialect.SQLITE -> fld.path(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(dialect).let { p ->
|
field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(mode).let { p ->
|
||||||
when (dialect) {
|
when (mode) {
|
||||||
Dialect.POSTGRESQL -> "LOWER($p)"
|
Dialect.POSTGRESQL -> "LOWER($p)"
|
||||||
Dialect.SQLITE -> "$p COLLATE NOCASE"
|
Dialect.SQLITE -> "$p COLLATE NOCASE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> field.path(dialect)
|
else -> field.path(mode)
|
||||||
}
|
}
|
||||||
"$path${direction ?: ""}"
|
"$path${direction ?: ""}"
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user