Initial Development #1

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

View File

@ -0,0 +1,87 @@
package solutions.bitbadger.documents.common
import solutions.bitbadger.documents.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Integration tests for the `Find` object
*/
object Patch {
fun byIdMatch(db: ThrowawayDatabase) {
JsonDocument.load(db)
db.conn.patchById(TEST_TABLE, "one", mapOf("numValue" to 44))
val doc = db.conn.findById<String, JsonDocument>(TEST_TABLE, "one")
assertNotNull(doc, "There should have been a document returned")
assertEquals("one", doc.id, "An incorrect document was returned")
assertEquals(44, doc.numValue, "The document was not patched")
}
fun byIdNoMatch(db: ThrowawayDatabase) {
JsonDocument.load(db)
assertFalse("Document with ID \"forty-seven\" should not exist") {
db.conn.existsById(TEST_TABLE, "forty-seven")
}
db.conn.patchById(TEST_TABLE, "forty-seven", mapOf("foo" to "green")) // no exception = pass
}
fun byFieldsMatch(db: ThrowawayDatabase) {
JsonDocument.load(db)
db.conn.patchByFields(TEST_TABLE, listOf(Field.equal("value", "purple")), mapOf("numValue" to 77))
assertEquals(
2,
db.conn.countByFields(TEST_TABLE, listOf(Field.equal("numValue", 77))),
"There should have been 2 documents with numeric value 77"
)
}
fun byFieldsNoMatch(db: ThrowawayDatabase) {
JsonDocument.load(db)
val fields = listOf(Field.equal("value", "burgundy"))
assertFalse("There should be no documents with value of \"burgundy\"") {
db.conn.existsByFields(TEST_TABLE, fields)
}
db.conn.patchByFields(TEST_TABLE, fields, mapOf("foo" to "green")) // no exception = pass
}
fun byContainsMatch(db: ThrowawayDatabase) {
JsonDocument.load(db)
val contains = mapOf("value" to "another")
db.conn.patchByContains(TEST_TABLE, contains, mapOf("numValue" to 12))
val doc = db.conn.findFirstByContains<JsonDocument, Map<String, String>>(TEST_TABLE, contains)
assertNotNull(doc, "There should have been a document returned")
assertEquals("two", doc.id, "The incorrect document was returned")
assertEquals(12, doc.numValue, "The document was not updated")
}
fun byContainsNoMatch(db: ThrowawayDatabase) {
JsonDocument.load(db)
val contains = mapOf("value" to "updated")
assertFalse("There should be no matching documents") { db.conn.existsByContains(TEST_TABLE, contains) }
db.conn.patchByContains(TEST_TABLE, contains, mapOf("sub.foo" to "green")) // no exception = pass
}
fun byJsonPathMatch(db: ThrowawayDatabase) {
JsonDocument.load(db)
val path = "$.numValue ? (@ > 10)"
db.conn.patchByJsonPath(TEST_TABLE, path, mapOf("value" to "blue"))
val docs = db.conn.findByJsonPath<JsonDocument>(TEST_TABLE, path)
assertEquals(2, docs.size, "There should have been two documents returned")
docs.forEach {
assertTrue(listOf("four", "five").contains(it.id), "An incorrect document was returned (${it.id})")
assertEquals("blue", it.value, "The value for ID ${it.id} was incorrect")
}
}
fun byJsonPathNoMatch(db: ThrowawayDatabase) {
JsonDocument.load(db)
val path = "$.numValue ? (@ > 100)"
assertFalse("There should be no documents with numeric values over 100") {
db.conn.existsByJsonPath(TEST_TABLE, path)
}
db.conn.patchByJsonPath(TEST_TABLE, path, mapOf("value" to "blue")) // no exception = pass
}
}

View File

@ -0,0 +1,52 @@
package solutions.bitbadger.documents.postgresql
import org.junit.jupiter.api.DisplayName
import solutions.bitbadger.documents.common.Patch
import kotlin.test.Test
/**
* PostgreSQL integration tests for the `Patch` object / `patchBy*` connection extension functions
*/
@DisplayName("PostgreSQL - Patch")
class PatchIT {
@Test
@DisplayName("byId patches an existing document")
fun byIdMatch() =
PgDB().use(Patch::byIdMatch)
@Test
@DisplayName("byId succeeds for a non-existent document")
fun byIdNoMatch() =
PgDB().use(Patch::byIdNoMatch)
@Test
@DisplayName("byFields patches matching document")
fun byFieldsMatch() =
PgDB().use(Patch::byFieldsMatch)
@Test
@DisplayName("byFields succeeds when no documents match")
fun byFieldsNoMatch() =
PgDB().use(Patch::byFieldsNoMatch)
@Test
@DisplayName("byContains patches matching document")
fun byContainsMatch() =
PgDB().use(Patch::byContainsMatch)
@Test
@DisplayName("byContains succeeds when no documents match")
fun byContainsNoMatch() =
PgDB().use(Patch::byContainsNoMatch)
@Test
@DisplayName("byJsonPath patches matching document")
fun byJsonPathMatch() =
PgDB().use(Patch::byJsonPathMatch)
@Test
@DisplayName("byJsonPath succeeds when no documents match")
fun byJsonPathNoMatch() =
PgDB().use(Patch::byJsonPathNoMatch)
}

View File

@ -0,0 +1,46 @@
package solutions.bitbadger.documents.sqlite
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.assertThrows
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.common.Patch
import kotlin.test.Test
/**
* SQLite integration tests for the `Patch` object / `patchBy*` connection extension functions
*/
@DisplayName("SQLite - Patch")
class PatchIT {
@Test
@DisplayName("byId patches an existing document")
fun byIdMatch() =
SQLiteDB().use(Patch::byIdMatch)
@Test
@DisplayName("byId succeeds for a non-existent document")
fun byIdNoMatch() =
SQLiteDB().use(Patch::byIdNoMatch)
@Test
@DisplayName("byFields patches matching document")
fun byFieldsMatch() =
SQLiteDB().use(Patch::byFieldsMatch)
@Test
@DisplayName("byFields succeeds when no documents match")
fun byFieldsNoMatch() =
SQLiteDB().use(Patch::byFieldsNoMatch)
@Test
@DisplayName("byContains fails")
fun byContainsFails() {
assertThrows<DocumentException> { SQLiteDB().use(Patch::byContainsMatch) }
}
@Test
@DisplayName("byJsonPath fails")
fun byJsonPathFails() {
assertThrows<DocumentException> { SQLiteDB().use(Patch::byJsonPathMatch) }
}
}

View File

@ -302,6 +302,60 @@ inline fun <reified TDoc> Connection.findFirstByJsonPath(
) = ) =
Find.firstByJsonPath<TDoc>(tableName, path, orderBy, this) Find.firstByJsonPath<TDoc>(tableName, path, orderBy, this)
// ~~~ DOCUMENT PATCH (PARTIAL UPDATE) QUERIES ~~~
/**
* Patch a document by its ID
*
* @param tableName The name of the table in which a document should be patched
* @param docId The ID of the document to be patched
* @param patch The object whose properties should be replaced in the document
*/
inline fun <TKey, reified TPatch> Connection.patchById(tableName: String, docId: TKey, patch: TPatch) =
Patch.byId(tableName, docId, patch, this)
/**
* Patch documents using a field comparison
*
* @param tableName The name of the table in which documents should be patched
* @param fields The fields which should be compared
* @param patch The object whose properties should be replaced in the document
* @param howMatched How the fields should be matched
*/
inline fun <reified TPatch> Connection.patchByFields(
tableName: String,
fields: Collection<Field<*>>,
patch: TPatch,
howMatched: FieldMatch? = null
) =
Patch.byFields(tableName, fields, patch, howMatched, this)
/**
* Patch documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which documents should be patched
* @param criteria The object against which JSON containment should be checked
* @param patch The object whose properties should be replaced in the document
* @throws DocumentException If called on a SQLite connection
*/
inline fun <reified TContains, reified TPatch> Connection.patchByContains(
tableName: String,
criteria: TContains,
patch: TPatch
) =
Patch.byContains(tableName, criteria, patch, this)
/**
* Patch documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which documents should be patched
* @param path The JSON path comparison to match
* @param patch The object whose properties should be replaced in the document
* @throws DocumentException If called on a SQLite connection
*/
inline fun <reified TPatch> Connection.patchByJsonPath(tableName: String, path: String, patch: TPatch) =
Patch.byJsonPath(tableName, path, patch, this)
// ~~~ DOCUMENT DELETION QUERIES ~~~ // ~~~ DOCUMENT DELETION QUERIES ~~~
/** /**

135
src/main/kotlin/Patch.kt Normal file
View File

@ -0,0 +1,135 @@
package solutions.bitbadger.documents
import solutions.bitbadger.documents.query.Patch
import java.sql.Connection
/**
* Functions to patch (partially update) documents
*/
object Patch {
/**
* Patch a document by its ID
*
* @param tableName The name of the table in which a document should be patched
* @param docId The ID of the document to be patched
* @param patch The object whose properties should be replaced in the document
* @param conn The connection on which the update should be executed
*/
inline fun <TKey, reified TPatch> byId(tableName: String, docId: TKey, patch: TPatch, conn: Connection) =
conn.customNonQuery(
Patch.byId(tableName, docId),
Parameters.addFields(
listOf(Field.equal(Configuration.idField, docId, ":id")),
mutableListOf(Parameters.json(":data", patch))
)
)
/**
* Patch a document by its ID
*
* @param tableName The name of the table in which a document should be patched
* @param docId The ID of the document to be patched
* @param patch The object whose properties should be replaced in the document
*/
inline fun <TKey, reified TPatch> byId(tableName: String, docId: TKey, patch: TPatch) =
Configuration.dbConn().use { byId(tableName, docId, patch, it) }
/**
* Patch documents using a field comparison
*
* @param tableName The name of the table in which documents should be patched
* @param fields The fields which should be compared
* @param patch The object whose properties should be replaced in the document
* @param howMatched How the fields should be matched
* @param conn The connection on which the update should be executed
*/
inline fun <reified TPatch> byFields(
tableName: String,
fields: Collection<Field<*>>,
patch: TPatch,
howMatched: FieldMatch? = null,
conn: Connection
) {
val named = Parameters.nameFields(fields)
conn.customNonQuery(
Patch.byFields(tableName, named, howMatched), Parameters.addFields(
named,
mutableListOf(Parameters.json(":data", patch))
)
)
}
/**
* Patch documents using a field comparison
*
* @param tableName The name of the table in which documents should be patched
* @param fields The fields which should be compared
* @param patch The object whose properties should be replaced in the document
* @param howMatched How the fields should be matched
*/
inline fun <reified TPatch> byFields(
tableName: String,
fields: Collection<Field<*>>,
patch: TPatch,
howMatched: FieldMatch? = null
) =
Configuration.dbConn().use { byFields(tableName, fields, patch, howMatched, it) }
/**
* Patch documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which documents should be patched
* @param criteria The object against which JSON containment should be checked
* @param patch The object whose properties should be replaced in the document
* @param conn The connection on which the update should be executed
* @throws DocumentException If called on a SQLite connection
*/
inline fun <reified TContains, reified TPatch> byContains(
tableName: String,
criteria: TContains,
patch: TPatch,
conn: Connection
) =
conn.customNonQuery(
Patch.byContains(tableName),
listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch))
)
/**
* Patch documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which documents should be patched
* @param criteria The object against which JSON containment should be checked
* @param patch The object whose properties should be replaced in the document
* @throws DocumentException If called on a SQLite connection
*/
inline fun <reified TContains, reified TPatch> byContains(tableName: String, criteria: TContains, patch: TPatch) =
Configuration.dbConn().use { byContains(tableName, criteria, patch, it) }
/**
* Patch documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which documents should be patched
* @param path The JSON path comparison to match
* @param patch The object whose properties should be replaced in the document
* @param conn The connection on which the update should be executed
* @throws DocumentException If called on a SQLite connection
*/
inline fun <reified TPatch> byJsonPath(tableName: String, path: String, patch: TPatch, conn: Connection) =
conn.customNonQuery(
Patch.byJsonPath(tableName),
listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch))
)
/**
* Patch documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which documents should be patched
* @param path The JSON path comparison to match
* @param patch The object whose properties should be replaced in the document
* @throws DocumentException If called on a SQLite connection
*/
inline fun <reified TPatch> byJsonPath(tableName: String, path: String, patch: TPatch) =
Configuration.dbConn().use { byJsonPath(tableName, path, patch, it) }
}