Initial Development #1

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

View File

@ -96,7 +96,7 @@ object Query {
* @param dataType The type of data for the column (`JSON`, `JSONB`, etc.) * @param dataType The type of data for the column (`JSON`, `JSONB`, etc.)
* @return A query to create a document table * @return A query to create a document table
*/ */
fun ensureTableFor(tableName: String, dataType: String): String = fun ensureTableFor(tableName: String, dataType: String) =
"CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)" "CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)"
/** /**
@ -146,7 +146,7 @@ object Query {
* @param dialect The SQL dialect to use when creating this index * @param dialect The SQL dialect to use when creating this index
* @return A query to create the key index * @return A query to create the key index
*/ */
fun ensureKey(tableName: String, dialect: Dialect): String = fun ensureKey(tableName: String, dialect: Dialect) =
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX") ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
} }
@ -184,19 +184,59 @@ object Query {
* @param tableName The table into which to save (may include schema) * @param tableName The table into which to save (may include schema)
* @return A query to save a document * @return A query to save a document
*/ */
fun save(tableName: String): String = fun save(tableName: String) =
insert(tableName, AutoId.DISABLED) + insert(tableName, AutoId.DISABLED) +
" ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data" " ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data"
/** /**
* Query to count documents in a table (this query has no `WHERE` clause) * Functions to count documents
*/
object Count {
/**
* Query to count all documents in a table
* *
* @param tableName The table in which to count documents (may include schema) * @param tableName The table in which to count documents (may include schema)
* @return A query to count documents * @return A query to count documents
*/ */
fun count(tableName: String): String = fun all(tableName: String) =
"SELECT COUNT(*) AS it FROM $tableName" "SELECT COUNT(*) AS it FROM $tableName"
/**
* Query to count documents matching the given fields
*
* @param tableName The table in which to count documents (may include schema)
* @param fields The field comparisons for the count
* @param howMatched How fields should be compared (optional, defaults to ALL)
* @return A query to count documents matching the given fields
*/
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
statementWhere(all(tableName), Where.byFields(fields, howMatched))
/**
* Query to count documents via JSON containment (PostgreSQL only)
*
* @param tableName The table in which to count documents (may include schema)
* @return A query to count documents via JSON containment
*/
fun byContains(tableName: String) =
statementWhere(all(tableName), Where.jsonContains())
/**
* Query to count documents via a JSON path match (PostgreSQL only)
*
* @param tableName The table in which to count documents (may include schema)
* @return A query to count documents via a JSON path match
*/
fun byJsonPath(tableName: String) =
statementWhere(all(tableName), Where.jsonPathMatches())
}
/**
* Functions to check for document existence
*/
object Exists {
/** /**
* Query to check for document existence in a table * Query to check for document existence in a table
* *
@ -204,25 +244,110 @@ object Query {
* @param where The `WHERE` clause with the existence criteria * @param where The `WHERE` clause with the existence criteria
* @return A query to check document existence * @return A query to check document existence
*/ */
fun exists(tableName: String, where: String): String = private fun exists(tableName: String, where: String) =
"SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it" "SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it"
/** /**
* Query to select documents from a table (this query has no `WHERE` clause) * Query to check for document existence by ID
* *
* @param tableName The table from which documents should be found (may include schema) * @param tableName The table in which existence should be checked (may include schema)
* @param docId The ID of the document (optional, used for type checking)
* @return A query to determine document existence by ID
*/
fun <TKey> byId(tableName: String, docId: TKey?) =
exists(tableName, Where.byId(docId = docId))
/**
* Query to check for document existence matching the given fields
*
* @param tableName The table in which existence should be checked (may include schema)
* @param fields The field comparisons for the existence check
* @param howMatched How fields should be compared (optional, defaults to ALL)
* @return A query to determine document existence for the given fields
*/
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch?) =
exists(tableName, Where.byFields(fields, howMatched))
/**
* Query to check for document existence via JSON containment (PostgreSQL only)
*
* @param tableName The table in which existence should be checked (may include schema)
* @return A query to determine document existence via JSON containment
*/
fun byContains(tableName: String) =
exists(tableName, Where.jsonContains())
/**
* Query to check for document existence via a JSON path match (PostgreSQL only)
*
* @param tableName The table in which existence should be checked (may include schema)
* @return A query to determine document existence via a JSON path match
*/
fun byJsonPath(tableName: String) =
exists(tableName, Where.jsonPathMatches())
}
/**
* Functions to retrieve documents
*/
object Find {
/**
* Query to retrieve all documents from a table
*
* @param tableName The table from which documents should be retrieved (may include schema)
* @return A query to retrieve documents * @return A query to retrieve documents
*/ */
fun find(tableName: String): String = fun all(tableName: String) =
"SELECT data FROM $tableName" "SELECT data FROM $tableName"
/**
* Query to retrieve a document by its ID
*
* @param tableName The table from which documents should be retrieved (may include schema)
* @param docId The ID of the document (optional, used for type checking)
* @return A query to retrieve a document by its ID
*/
fun <TKey> byId(tableName: String, docId: TKey?) =
statementWhere(all(tableName), Where.byId(docId = docId))
/**
* Query to retrieve documents matching the given fields
*
* @param tableName The table from which documents should be retrieved (may include schema)
* @param fields The field comparisons for matching documents to retrieve
* @param howMatched How fields should be compared (optional, defaults to ALL)
* @return A query to retrieve documents matching the given fields
*/
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch?) =
statementWhere(all(tableName), Where.byFields(fields, howMatched))
/**
* Query to retrieve documents via JSON containment (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved (may include schema)
* @return A query to retrieve documents via JSON containment
*/
fun byContains(tableName: String) =
statementWhere(all(tableName), Where.jsonContains())
/**
* Query to retrieve documents via a JSON path match (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved (may include schema)
* @return A query to retrieve documents via a JSON path match
*/
fun byJsonPath(tableName: String) =
statementWhere(all(tableName), Where.jsonPathMatches())
}
/** /**
* Query to update (replace) a document (this query has no `WHERE` clause) * Query to update (replace) a document (this query has no `WHERE` clause)
* *
* @param tableName The table in which documents should be replaced (may include schema) * @param tableName The table in which documents should be replaced (may include schema)
* @return A query to update documents * @return A query to update documents
*/ */
fun update(tableName: String): String = fun update(tableName: String) =
"UPDATE $tableName SET data = :data" "UPDATE $tableName SET data = :data"
/** /**
@ -286,13 +411,59 @@ object Query {
} }
/** /**
* Query to delete documents from a table (this query has no `WHERE` clause) * Functions to delete documents
*/
object Delete {
/**
* Query to delete documents from a table
* *
* @param tableName The table in which documents should be deleted (may include schema) * @param tableName The table in which documents should be deleted (may include schema)
* @param where The WHERE clause for the delete statement
* @return A query to delete documents * @return A query to delete documents
*/ */
fun delete(tableName: String): String = private fun delete(tableName: String, where: String) =
"DELETE FROM $tableName" statementWhere("DELETE FROM $tableName", where)
/**
* Query to delete a document by its ID
*
* @param tableName The table from which documents should be deleted (may include schema)
* @param docId The ID of the document (optional, used for type checking)
* @return A query to delete a document by its ID
*/
fun <TKey> byId(tableName: String, docId: TKey?) =
delete(tableName, Where.byId(docId = docId))
/**
* Query to delete documents matching the given fields
*
* @param tableName The table from which documents should be deleted (may include schema)
* @param fields The field comparisons for documents to be deleted
* @param howMatched How fields should be compared (optional, defaults to ALL)
* @return A query to delete documents matching for the given fields
*/
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch?) =
delete(tableName, Where.byFields(fields, howMatched))
/**
* Query to delete documents via JSON containment (PostgreSQL only)
*
* @param tableName The table from which documents should be deleted (may include schema)
* @return A query to delete documents via JSON containment
*/
fun byContains(tableName: String) =
delete(tableName, Where.jsonContains())
/**
* Query to delete documents via a JSON path match (PostgreSQL only)
*
* @param tableName The table from which documents should be deleted (may include schema)
* @return A query to delete documents via a JSON path match
*/
fun byJsonPath(tableName: String) =
delete(tableName, Where.jsonPathMatches())
}
/** /**
* Create an `ORDER BY` clause for the given fields * Create an `ORDER BY` clause for the given fields

View File

@ -399,30 +399,78 @@ class QueryTest {
} }
@Test @Test
@DisplayName("count generates correctly") @DisplayName("Count.all generates correctly")
fun count() = fun countAll() =
assertEquals("SELECT COUNT(*) AS it FROM $tbl", Query.count(tbl), "Count query not constructed correctly") assertEquals("SELECT COUNT(*) AS it FROM $tbl", Query.Count.all(tbl), "Count query not constructed correctly")
@Test @Test
@DisplayName("exists generates correctly") @DisplayName("Count.byFields generates correctly (PostgreSQL)")
fun exists() = fun countByFieldsPostgres() {
assertEquals("SELECT EXISTS (SELECT 1 FROM $tbl WHERE turkey) AS it", Query.exists(tbl, "turkey"), Configuration.connectionString = pg
"Exists query not constructed correctly") assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
Query.Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))),
"Count query not constructed correctly")
}
@Test @Test
@DisplayName("find generates correctly") @DisplayName("Count.byFields generates correctly (PostgreSQL)")
fun find() = fun countByFieldsSQLite() {
assertEquals("SELECT data FROM $tbl", Query.find(tbl), "Find query not constructed correctly") Configuration.connectionString = lite
assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data->>'test' = :field0",
Query.Count.byFields(tbl, listOf(Field.equal("test", "", ":field0"))),
"Count query not constructed correctly")
}
@Test
@DisplayName("Count.byContains generates correctly (PostgreSQL)")
fun countByContainsPostgres() {
Configuration.connectionString = pg
assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE data @> :criteria", Query.Count.byContains(tbl),
"Count query not constructed correctly")
}
@Test
@DisplayName("Count.byContains fails (SQLite)")
fun countByContainsSQLite() {
Configuration.connectionString = lite
assertThrows<DocumentException> { Query.Count.byContains(tbl) }
}
@Test
@DisplayName("Count.byJsonPath generates correctly (PostgreSQL)")
fun countByJsonPathPostgres() {
Configuration.connectionString = pg
assertEquals("SELECT COUNT(*) AS it FROM $tbl WHERE jsonb_path_exists(data, :path::jsonpath)",
Query.Count.byJsonPath(tbl), "Count query not constructed correctly")
}
@Test
@DisplayName("Count.byJsonPath fails (SQLite)")
fun countByJsonPathSQLite() {
Configuration.connectionString = lite
assertThrows<DocumentException> { Query.Count.byJsonPath(tbl) }
}
// @Test
// @DisplayName("exists generates correctly")
// fun exists() =
// assertEquals("SELECT EXISTS (SELECT 1 FROM $tbl WHERE turkey) AS it", Query.exists(tbl, "turkey"),
// "Exists query not constructed correctly")
@Test
@DisplayName("Find.all generates correctly")
fun findAll() =
assertEquals("SELECT data FROM $tbl", Query.Find.all(tbl), "Find query not constructed correctly")
@Test @Test
@DisplayName("update generates successfully") @DisplayName("update generates successfully")
fun update() = fun update() =
assertEquals("UPDATE $tbl SET data = :data", Query.update(tbl), "Update query not constructed correctly") assertEquals("UPDATE $tbl SET data = :data", Query.update(tbl), "Update query not constructed correctly")
@Test // @Test
@DisplayName("delete generates successfully") // @DisplayName("delete generates successfully")
fun delete() = // fun delete() =
assertEquals("DELETE FROM $tbl", Query.delete(tbl), "Delete query not constructed correctly") // assertEquals("DELETE FROM $tbl", Query.delete(tbl), "Delete query not constructed correctly")
@Test @Test
@DisplayName("orderBy generates for no fields") @DisplayName("orderBy generates for no fields")