Initial Development #1

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

View File

@ -21,7 +21,7 @@ object Configuration {
var idStringLength = 16
/** The derived dialect value from the connection string */
internal var dialectValue: Dialect? = null
private var dialectValue: Dialect? = null
/** The connection string for the JDBC connection */
@JvmStatic
@ -35,12 +35,13 @@ object Configuration {
* Retrieve a new connection to the configured database
*
* @return A new connection to the configured database
* @throws IllegalArgumentException If the connection string is not set before calling this
* @throws DocumentException If the connection string is not set before calling this
*/
@Throws(DocumentException::class)
@JvmStatic
fun dbConn(): Connection {
if (connectionString == null) {
throw IllegalArgumentException("Please provide a connection string before attempting data access")
throw DocumentException("Please provide a connection string before attempting data access")
}
return DriverManager.getConnection(connectionString)
}

View File

@ -1,11 +1,14 @@
package solutions.bitbadger.documents
import kotlin.jvm.Throws
/**
* The SQL dialect to use when building queries
*/
enum class Dialect {
/** PostgreSQL */
POSTGRESQL,
/** SQLite */
SQLITE;
@ -18,6 +21,8 @@ enum class Dialect {
* @return The dialect for the connection string
* @throws DocumentException If the dialect cannot be determined
*/
@Throws(DocumentException::class)
@JvmStatic
fun deriveFromConnectionString(connectionString: String): Dialect =
when {
connectionString.contains(":sqlite:") -> SQLITE

View File

@ -6,4 +6,4 @@ package solutions.bitbadger.documents
* @param message The message for the exception
* @param cause The underlying exception (optional)
*/
class DocumentException(message: String, cause: Throwable? = null) : Exception(message, cause)
class DocumentException @JvmOverloads constructor(message: String, cause: Throwable? = null) : Exception(message, cause)

View File

@ -118,7 +118,6 @@ class Field<T> private constructor(
is ComparisonInArray<*> -> {
val mkString = Configuration.dialect("append parameters for InArray") == Dialect.POSTGRESQL
// TODO: I think this is actually Pair<String, Collection<*>>
comparison.value.second.forEachIndexed { index, item ->
if (mkString) {
existing.add(Parameter("${parameterName}_$index", ParameterType.STRING, "$item"))

View File

@ -6,6 +6,7 @@ package solutions.bitbadger.documents
enum class FieldFormat {
/** Retrieve the field as a SQL value (string in PostgreSQL, best guess in SQLite */
SQL,
/** Retrieve the field as a JSON value */
JSON
}

View File

@ -6,6 +6,7 @@ package solutions.bitbadger.documents
enum class FieldMatch(val sql: String) {
/** Match any of the field criteria (`OR`) */
ANY("OR"),
/** Match all the field criteria (`AND`) */
ALL("AND"),
}

View File

@ -6,24 +6,34 @@ package solutions.bitbadger.documents
enum class Op(val sql: String) {
/** Compare using equality */
EQUAL("="),
/** Compare using greater-than */
GREATER(">"),
/** Compare using greater-than-or-equal-to */
GREATER_OR_EQUAL(">="),
/** Compare using less-than */
LESS("<"),
/** Compare using less-than-or-equal-to */
LESS_OR_EQUAL("<="),
/** Compare using inequality */
NOT_EQUAL("<>"),
/** Compare between two values */
BETWEEN("BETWEEN"),
/** Compare existence in a list of values */
IN("IN"),
/** Compare overlap between an array and a list of values */
IN_ARRAY("??|"),
/** Compare existence */
EXISTS("IS NOT NULL"),
/** Compare nonexistence */
NOT_EXISTS("IS NULL")
}

View File

@ -2,6 +2,7 @@ package solutions.bitbadger.documents
import java.sql.PreparedStatement
import java.sql.Types
import kotlin.jvm.Throws
/**
* A parameter to use for a query
@ -22,7 +23,9 @@ class Parameter<T>(val name: String, val type: ParameterType, val value: T) {
*
* @param stmt The prepared statement to which this parameter should be bound
* @param index The index (1-based) to which the parameter should be bound
* @throws DocumentException If a number parameter is given a non-numeric value
*/
@Throws(DocumentException::class)
fun bind(stmt: PreparedStatement, index: Int) {
when (type) {
ParameterType.NUMBER -> {
@ -33,7 +36,7 @@ class Parameter<T>(val name: String, val type: ParameterType, val value: T) {
is Int -> stmt.setInt(index, value)
is Long -> stmt.setLong(index, value)
else -> throw DocumentException(
"Number parameter must be Byte, Short, Int, or Long (${value!!::class.simpleName})"
"Number parameter must be Byte, Short, Int, or Long (${value::class.simpleName})"
)
}
}

View File

@ -6,6 +6,7 @@ import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.jvm.*
import java.sql.Connection
import java.sql.ResultSet
import kotlin.jvm.Throws
// ~~~ CUSTOM QUERIES ~~~
@ -17,7 +18,9 @@ import java.sql.ResultSet
* @param clazz The class of the document to be returned
* @param mapFunc The mapping function between the document and the domain item
* @return A list of results for the given query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
fun <TDoc> Connection.customList(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
@ -34,7 +37,9 @@ fun <TDoc> Connection.customList(
* @param clazz The class of the document to be returned
* @param mapFunc The mapping function between the document and the domain item
* @return The document if one matches the query, `null` otherwise
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
fun <TDoc> Connection.customSingle(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
@ -48,7 +53,9 @@ fun <TDoc> Connection.customSingle(
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) =
Custom.nonQuery(query, parameters, this)
@ -60,7 +67,9 @@ fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>
* @param clazz The class of the document to be returned
* @param mapFunc The mapping function between the document and the domain item
* @return The scalar value from the query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
fun <T : Any> Connection.customScalar(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
@ -109,7 +118,7 @@ fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex)
* @param document The document to be inserted
*/
fun <TDoc> Connection.insert(tableName: String, document: TDoc) =
Document.insert<TDoc>(tableName, document, this)
Document.insert(tableName, document, this)
/**
* Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
@ -137,7 +146,9 @@ fun <TKey, TDoc> Connection.update(tableName: String, docId: TKey, document: TDo
*
* @param tableName The name of the table in which documents should be counted
* @return A count of the documents in the table
* @throws DocumentException If any dependent process does
*/
@Throws(DocumentException::class)
fun Connection.countAll(tableName: String) =
Count.all(tableName, this)
@ -148,7 +159,9 @@ fun Connection.countAll(tableName: String) =
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @return A count of the matching documents in the table
* @throws DocumentException If the dialect has not been configured
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.countByFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
Count.byFields(tableName, fields, howMatched, this)

View File

@ -4,6 +4,7 @@ import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.CountQuery
import solutions.bitbadger.documents.extensions.customScalar
import java.sql.Connection
import kotlin.jvm.Throws
/**
* Functions to count documents
@ -16,7 +17,9 @@ object Count {
* @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
* @throws DocumentException If any dependent process does
*/
@Throws(DocumentException::class)
@JvmStatic
fun all(tableName: String, conn: Connection) =
conn.customScalar(CountQuery.all(tableName), listOf(), Long::class.java, Results::toCount)
@ -26,7 +29,9 @@ object Count {
*
* @param tableName The name of the table in which documents should be counted
* @return A count of the documents in the table
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun all(tableName: String) =
Configuration.dbConn().use { all(tableName, it) }
@ -39,7 +44,9 @@ object Count {
* @param howMatched How the fields should be matched
* @param conn The connection on which the deletion should be executed
* @return A count of the matching documents in the table
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(
@ -64,7 +71,9 @@ object Count {
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @return A count of the matching documents in the table
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
@ -79,6 +88,7 @@ object Count {
* @return A count of the matching documents in the table
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> byContains(tableName: String, criteria: TContains, conn: Connection) =
conn.customScalar(
@ -94,8 +104,9 @@ object Count {
* @param tableName The name of the table in which documents should be counted
* @param criteria The object for which JSON containment should be checked
* @return A count of the matching documents in the table
* @throws DocumentException If called on a SQLite connection
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> byContains(tableName: String, criteria: TContains) =
Configuration.dbConn().use { byContains(tableName, criteria, it) }
@ -109,6 +120,7 @@ object Count {
* @return A count of the matching documents in the table
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String, path: String, conn: Connection) =
conn.customScalar(
@ -124,8 +136,9 @@ object Count {
* @param tableName The name of the table in which documents should be counted
* @param path The JSON path comparison to match
* @return A count of the matching documents in the table
* @throws DocumentException If called on a SQLite connection
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String, path: String) =
Configuration.dbConn().use { byJsonPath(tableName, path, it) }

View File

@ -1,9 +1,11 @@
package solutions.bitbadger.documents.jvm
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.Parameter
import java.sql.Connection
import java.sql.ResultSet
import kotlin.jvm.Throws
object Custom {
@ -16,7 +18,9 @@ object Custom {
* @param conn The connection over which the query should be executed
* @param mapFunc The mapping function between the document and the domain item
* @return A list of results for the given query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> list(
query: String,
@ -34,7 +38,9 @@ object Custom {
* @param clazz The class of the document to be returned
* @param mapFunc The mapping function between the document and the domain item
* @return A list of results for the given query
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> list(
query: String,
@ -52,7 +58,9 @@ object Custom {
* @param conn The connection over which the query should be executed
* @param mapFunc The mapping function between the document and the domain item
* @return The document if one matches the query, `null` otherwise
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> single(
query: String,
@ -70,7 +78,9 @@ object Custom {
* @param clazz The class of the document to be returned
* @param mapFunc The mapping function between the document and the domain item
* @return The document if one matches the query, `null` otherwise
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> single(
query: String,
@ -85,7 +95,9 @@ object Custom {
* @param query The query to retrieve the results
* @param conn The connection over which the query should be executed
* @param parameters Parameters to use for the query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection) {
Parameters.apply(conn, query, parameters).use { it.executeUpdate() }
@ -96,7 +108,9 @@ object Custom {
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) =
@ -110,7 +124,9 @@ object Custom {
* @param conn The connection over which the query should be executed
* @param mapFunc The mapping function between the document and the domain item
* @return The scalar value from the query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <T : Any> scalar(
query: String,
@ -132,7 +148,10 @@ object Custom {
* @param parameters Parameters to use for the query
* @param mapFunc The mapping function between the document and the domain item
* @return The scalar value from the query
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <T : Any> scalar(
query: String,
parameters: Collection<Parameter<*>> = listOf(),

View File

@ -6,6 +6,7 @@ import solutions.bitbadger.documents.DocumentIndex
import solutions.bitbadger.documents.extensions.customNonQuery
import solutions.bitbadger.documents.query.DefinitionQuery
import java.sql.Connection
import kotlin.jvm.Throws
/**
* Functions to define tables and indexes
@ -17,7 +18,9 @@ object Definition {
*
* @param tableName The table whose existence should be ensured (may include schema)
* @param conn The connection on which the query should be executed
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureTable(tableName: String, conn: Connection) =
Configuration.dialect("ensure $tableName exists").let {
@ -29,7 +32,9 @@ object Definition {
* Create a document table if necessary
*
* @param tableName The table whose existence should be ensured (may include schema)
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureTable(tableName: String) =
Configuration.dbConn().use { ensureTable(tableName, it) }
@ -41,7 +46,9 @@ object Definition {
* @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
* @throws DocumentException If any dependent process does
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>, conn: Connection) =
conn.customNonQuery(DefinitionQuery.ensureIndexOn(tableName, indexName, fields))
@ -52,7 +59,9 @@ object Definition {
* @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
* @throws DocumentException If no connection string has been set, or if any dependent process does
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
Configuration.dbConn().use { ensureFieldIndex(tableName, indexName, fields, it) }
@ -75,7 +84,7 @@ object Definition {
*
* @param tableName The table to be indexed (may include schema)
* @param indexType The type of index to ensure
* @throws DocumentException If called on a SQLite connection
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic

View File

@ -17,18 +17,18 @@ import static solutions.bitbadger.documents.support.TypesKt.TEST_TABLE;
*/
final public class CountFunctions {
public static void all(ThrowawayDatabase db) {
public static void all(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should have been 5 documents in the table");
}
public static void byFieldsNumeric(ThrowawayDatabase db) {
public static void byFieldsNumeric(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(3L, countByFields(db.getConn(), TEST_TABLE, List.of(Field.between("numValue", 10, 20))),
"There should have been 3 matching documents");
}
public static void byFieldsAlpha(ThrowawayDatabase db) {
public static void byFieldsAlpha(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(1L, countByFields(db.getConn(), TEST_TABLE, List.of(Field.between("value", "aardvark", "apple"))),
"There should have been 1 matching document");

View File

@ -14,7 +14,7 @@ public class CountIT {
@Test
@DisplayName("all counts all documents")
public void all() {
public void all() throws DocumentException {
try (PgDB db = new PgDB()) {
CountFunctions.all(db);
}
@ -22,7 +22,7 @@ public class CountIT {
@Test
@DisplayName("byFields counts documents by a numeric value")
public void byFieldsNumeric() {
public void byFieldsNumeric() throws DocumentException {
try (PgDB db = new PgDB()) {
CountFunctions.byFieldsNumeric(db);
}
@ -30,7 +30,7 @@ public class CountIT {
@Test
@DisplayName("byFields counts documents by a alphanumeric value")
public void byFieldsAlpha() {
public void byFieldsAlpha() throws DocumentException {
try (PgDB db = new PgDB()) {
CountFunctions.byFieldsAlpha(db);
}

View File

@ -16,7 +16,7 @@ public class CountIT {
@Test
@DisplayName("all counts all documents")
public void all() {
public void all() throws DocumentException {
try (SQLiteDB db = new SQLiteDB()) {
CountFunctions.all(db);
}
@ -24,7 +24,7 @@ public class CountIT {
@Test
@DisplayName("byFields counts documents by a numeric value")
public void byFieldsNumeric() {
public void byFieldsNumeric() throws DocumentException {
try (SQLiteDB db = new SQLiteDB()) {
CountFunctions.byFieldsNumeric(db);
}
@ -32,7 +32,7 @@ public class CountIT {
@Test
@DisplayName("byFields counts documents by a alphanumeric value")
public void byFieldsAlpha() {
public void byFieldsAlpha() throws DocumentException {
try (SQLiteDB db = new SQLiteDB()) {
CountFunctions.byFieldsAlpha(db);
}

View File

@ -0,0 +1,172 @@
package solutions.bitbadger.documents.java.query;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.Field;
import solutions.bitbadger.documents.FieldMatch;
import solutions.bitbadger.documents.query.Where;
import solutions.bitbadger.documents.support.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Unit tests for the `Where` object
*/
@DisplayName("JVM | Java | Query | Where")
final public class WhereTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("byFields is blank when given no fields")
public void byFieldsBlankIfEmpty() throws DocumentException {
assertEquals("", Where.byFields(List.of()));
}
@Test
@DisplayName("byFields generates one numeric field | PostgreSQL")
public void byFieldsOneFieldPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("(data->>'it')::numeric = :that", Where.byFields(List.of(Field.equal("it", 9, ":that"))));
}
@Test
@DisplayName("byFields generates one alphanumeric field | PostgreSQL")
public void byFieldsOneAlphaFieldPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data->>'it' = :that", Where.byFields(List.of(Field.equal("it", "", ":that"))));
}
@Test
@DisplayName("byFields generates one field | SQLite")
public void byFieldsOneFieldSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("data->>'it' = :that", Where.byFields(List.of(Field.equal("it", "", ":that"))));
}
@Test
@DisplayName("byFields generates multiple fields w/ default match | PostgreSQL")
public void byFieldsMultipleDefaultPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three",
Where.byFields(List.of(
Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))));
}
@Test
@DisplayName("byFields generates multiple fields w/ default match | SQLite")
public void byFieldsMultipleDefaultSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three",
Where.byFields(List.of(
Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three"))));
}
@Test
@DisplayName("byFields generates multiple fields w/ ANY match | PostgreSQL")
public void byFieldsMultipleAnyPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three",
Where.byFields(List.of(
Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
FieldMatch.ANY));
}
@Test
@DisplayName("byFields generates multiple fields w/ ANY match | SQLite")
public void byFieldsMultipleAnySQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three",
Where.byFields(List.of(
Field.equal("1", "", ":one"), Field.equal("2", 0L, ":two"), Field.equal("3", "", ":three")),
FieldMatch.ANY));
}
@Test
@DisplayName("byId generates defaults for alphanumeric key | PostgreSQL")
public void byIdDefaultAlphaPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data->>'id' = :id", Where.byId(":id", ""));
}
@Test
@DisplayName("byId generates defaults for numeric key | PostgreSQL")
public void byIdDefaultNumericPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("(data->>'id')::numeric = :id", Where.byId(":id", 5));
}
@Test
@DisplayName("byId generates defaults | SQLite")
public void byIdDefaultSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("data->>'id' = :id", Where.byId(":id", ""));
}
@Test
@DisplayName("byId generates named ID | PostgreSQL")
public void byIdDefaultNamedPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data->>'id' = :key", Where.byId(":key"));
}
@Test
@DisplayName("byId generates named ID | SQLite")
public void byIdDefaultNamedSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("data->>'id' = :key", Where.byId(":key"));
}
@Test
@DisplayName("jsonContains generates defaults | PostgreSQL")
public void jsonContainsDefaultPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data @> :criteria", Where.jsonContains());
}
@Test
@DisplayName("jsonContains generates named parameter | PostgreSQL")
public void jsonContainsNamedPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data @> :it", Where.jsonContains(":it"));
}
@Test
@DisplayName("jsonContains fails | SQLite")
public void jsonContainsFailsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, Where::jsonContains);
}
@Test
@DisplayName("jsonPathMatches generates defaults | PostgreSQL")
public void jsonPathMatchDefaultPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("jsonb_path_exists(data, :path::jsonpath)", Where.jsonPathMatches());
}
@Test
@DisplayName("jsonPathMatches generates named parameter | PostgreSQL")
public void jsonPathMatchNamedPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Where.jsonPathMatches(":jp"));
}
@Test
@DisplayName("jsonPathMatches fails | SQLite")
public void jsonPathFailsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, Where::jsonPathMatches);
}
}

View File

@ -4,6 +4,7 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import solutions.bitbadger.documents.support.ForceDialect
import kotlin.test.assertEquals
import kotlin.test.assertNotSame
import kotlin.test.assertNull
@ -19,7 +20,7 @@ class FieldTest {
*/
@AfterEach
fun cleanUp() {
Configuration.dialectValue = null
ForceDialect.none()
}
// ~~~ INSTANCE METHODS ~~~
@ -120,178 +121,178 @@ class FieldTest {
"Path not correct")
@Test
@DisplayName("toWhere generates for exists w/o qualifier (PostgreSQL)")
@DisplayName("toWhere generates for exists w/o qualifier | PostgreSQL")
fun toWhereExistsNoQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for exists w/o qualifier (SQLite)")
@DisplayName("toWhere generates for exists w/o qualifier | SQLite")
fun toWhereExistsNoQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("data->>'that_field' IS NOT NULL", Field.exists("that_field").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for not-exists w/o qualifier (PostgreSQL)")
@DisplayName("toWhere generates for not-exists w/o qualifier | PostgreSQL")
fun toWhereNotExistsNoQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for not-exists w/o qualifier (SQLite)")
@DisplayName("toWhere generates for not-exists w/o qualifier | SQLite")
fun toWhereNotExistsNoQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("data->>'a_field' IS NULL", Field.notExists("a_field").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range (PostgreSQL)")
@DisplayName("toWhere generates for BETWEEN w/o qualifier, numeric range | PostgreSQL")
fun toWhereBetweenNoQualNumericPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("(data->>'age')::numeric BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").toWhere(), "Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range (PostgreSQL)")
@DisplayName("toWhere generates for BETWEEN w/o qualifier, alphanumeric range | PostgreSQL")
fun toWhereBetweenNoQualAlphaPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->>'city' BETWEEN :citymin AND :citymax",
Field.between("city", "Atlanta", "Chicago", ":city").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/o qualifier (SQLite)")
@DisplayName("toWhere generates for BETWEEN w/o qualifier | SQLite")
fun toWhereBetweenNoQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("data->>'age' BETWEEN @agemin AND @agemax", Field.between("age", 13, 17, "@age").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range (PostgreSQL)")
@DisplayName("toWhere generates for BETWEEN w/ qualifier, numeric range | PostgreSQL")
fun toWhereBetweenQualNumericPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("(test.data->>'age')::numeric BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range (PostgreSQL)")
@DisplayName("toWhere generates for BETWEEN w/ qualifier, alphanumeric range | PostgreSQL")
fun toWhereBetweenQualAlphaPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("unit.data->>'city' BETWEEN :citymin AND :citymax",
Field.between("city", "Atlanta", "Chicago", ":city").withQualifier("unit").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for BETWEEN w/ qualifier (SQLite)")
@DisplayName("toWhere generates for BETWEEN w/ qualifier | SQLite")
fun toWhereBetweenQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("my.data->>'age' BETWEEN @agemin AND @agemax",
Field.between("age", 13, 17, "@age").withQualifier("my").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for IN/any, numeric values (PostgreSQL)")
@DisplayName("toWhere generates for IN/any, numeric values | PostgreSQL")
fun toWhereAnyNumericPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)",
Field.any("even", listOf(2, 4, 6), ":nbr").toWhere(), "Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for IN/any, alphanumeric values (PostgreSQL)")
@DisplayName("toWhere generates for IN/any, alphanumeric values | PostgreSQL")
fun toWhereAnyAlphaPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->>'test' IN (:city_0, :city_1)",
Field.any("test", listOf("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for IN/any (SQLite)")
@DisplayName("toWhere generates for IN/any | SQLite")
fun toWhereAnySQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("data->>'test' IN (:city_0, :city_1)",
Field.any("test", listOf("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for inArray (PostgreSQL)")
@DisplayName("toWhere generates for inArray | PostgreSQL")
fun toWhereInArrayPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]",
Field.inArray("even", "tbl", listOf(2, 4, 6, 8), ":it").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for inArray (SQLite)")
@DisplayName("toWhere generates for inArray | SQLite")
fun toWhereInArraySQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))",
Field.inArray("test", "tbl", listOf("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for others w/o qualifier (PostgreSQL)")
@DisplayName("toWhere generates for others w/o qualifier | PostgreSQL")
fun toWhereOtherNoQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates for others w/o qualifier (SQLite)")
@DisplayName("toWhere generates for others w/o qualifier | SQLite")
fun toWhereOtherNoQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("data->>'some_field' = :value", Field.equal("some_field", "", ":value").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates no-parameter w/ qualifier (PostgreSQL)")
@DisplayName("toWhere generates no-parameter w/ qualifier | PostgreSQL")
fun toWhereNoParamWithQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates no-parameter w/ qualifier (SQLite)")
@DisplayName("toWhere generates no-parameter w/ qualifier | SQLite")
fun toWhereNoParamWithQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("test.data->>'no_field' IS NOT NULL", Field.exists("no_field").withQualifier("test").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates parameter w/ qualifier (PostgreSQL)")
@DisplayName("toWhere generates parameter w/ qualifier | PostgreSQL")
fun toWhereParamWithQualPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("(q.data->>'le_field')::numeric <= :it",
Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(),
"Field WHERE clause not generated correctly")
}
@Test
@DisplayName("toWhere generates parameter w/ qualifier (SQLite)")
@DisplayName("toWhere generates parameter w/ qualifier | SQLite")
fun toWhereParamWithQualSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("q.data->>'le_field' <= :it",
Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(),
"Field WHERE clause not generated correctly")

View File

@ -5,6 +5,7 @@ import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.support.ForceDialect
import kotlin.test.assertEquals
/**
@ -18,7 +19,7 @@ class WhereTest {
*/
@AfterEach
fun cleanUp() {
Configuration.dialectValue = null
ForceDialect.none()
}
@Test
@ -27,30 +28,30 @@ class WhereTest {
assertEquals("", Where.byFields(listOf()))
@Test
@DisplayName("byFields generates one numeric field (PostgreSQL)")
@DisplayName("byFields generates one numeric field | PostgreSQL")
fun byFieldsOneFieldPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("(data->>'it')::numeric = :that", Where.byFields(listOf(Field.equal("it", 9, ":that"))))
}
@Test
@DisplayName("byFields generates one alphanumeric field (PostgreSQL)")
@DisplayName("byFields generates one alphanumeric field | PostgreSQL")
fun byFieldsOneAlphaFieldPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->>'it' = :that", Where.byFields(listOf(Field.equal("it", "", ":that"))))
}
@Test
@DisplayName("byFields generates one field (SQLite)")
@DisplayName("byFields generates one field | SQLite")
fun byFieldsOneFieldSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("data->>'it' = :that", Where.byFields(listOf(Field.equal("it", "", ":that"))))
}
@Test
@DisplayName("byFields generates multiple fields w/ default match (PostgreSQL)")
@DisplayName("byFields generates multiple fields w/ default match | PostgreSQL")
fun byFieldsMultipleDefaultPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals(
"data->>'1' = :one AND (data->>'2')::numeric = :two AND data->>'3' = :three",
Where.byFields(
@ -60,9 +61,9 @@ class WhereTest {
}
@Test
@DisplayName("byFields generates multiple fields w/ default match (SQLite)")
@DisplayName("byFields generates multiple fields w/ default match | SQLite")
fun byFieldsMultipleDefaultSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals(
"data->>'1' = :one AND data->>'2' = :two AND data->>'3' = :three",
Where.byFields(
@ -72,9 +73,9 @@ class WhereTest {
}
@Test
@DisplayName("byFields generates multiple fields w/ ANY match (PostgreSQL)")
@DisplayName("byFields generates multiple fields w/ ANY match | PostgreSQL")
fun byFieldsMultipleAnyPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals(
"data->>'1' = :one OR (data->>'2')::numeric = :two OR data->>'3' = :three",
Where.byFields(
@ -85,9 +86,9 @@ class WhereTest {
}
@Test
@DisplayName("byFields generates multiple fields w/ ANY match (SQLite)")
@DisplayName("byFields generates multiple fields w/ ANY match | SQLite")
fun byFieldsMultipleAnySQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals(
"data->>'1' = :one OR data->>'2' = :two OR data->>'3' = :three",
Where.byFields(
@ -98,79 +99,79 @@ class WhereTest {
}
@Test
@DisplayName("byId generates defaults for alphanumeric key (PostgreSQL)")
@DisplayName("byId generates defaults for alphanumeric key | PostgreSQL")
fun byIdDefaultAlphaPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->>'id' = :id", Where.byId(docId = ""))
}
@Test
@DisplayName("byId generates defaults for numeric key (PostgreSQL)")
@DisplayName("byId generates defaults for numeric key | PostgreSQL")
fun byIdDefaultNumericPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("(data->>'id')::numeric = :id", Where.byId(docId = 5))
}
@Test
@DisplayName("byId generates defaults (SQLite)")
@DisplayName("byId generates defaults | SQLite")
fun byIdDefaultSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("data->>'id' = :id", Where.byId(docId = ""))
}
@Test
@DisplayName("byId generates named ID (PostgreSQL)")
@DisplayName("byId generates named ID | PostgreSQL")
fun byIdDefaultNamedPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data->>'id' = :key", Where.byId<String>(":key"))
}
@Test
@DisplayName("byId generates named ID (SQLite)")
@DisplayName("byId generates named ID | SQLite")
fun byIdDefaultNamedSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertEquals("data->>'id' = :key", Where.byId<String>(":key"))
}
@Test
@DisplayName("jsonContains generates defaults (PostgreSQL)")
@DisplayName("jsonContains generates defaults | PostgreSQL")
fun jsonContainsDefaultPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data @> :criteria", Where.jsonContains())
}
@Test
@DisplayName("jsonContains generates named parameter (PostgreSQL)")
@DisplayName("jsonContains generates named parameter | PostgreSQL")
fun jsonContainsNamedPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("data @> :it", Where.jsonContains(":it"))
}
@Test
@DisplayName("jsonContains fails (SQLite)")
@DisplayName("jsonContains fails | SQLite")
fun jsonContainsFailsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertThrows<DocumentException> { Where.jsonContains() }
}
@Test
@DisplayName("jsonPathMatches generates defaults (PostgreSQL)")
@DisplayName("jsonPathMatches generates defaults | PostgreSQL")
fun jsonPathMatchDefaultPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("jsonb_path_exists(data, :path::jsonpath)", Where.jsonPathMatches())
}
@Test
@DisplayName("jsonPathMatches generates named parameter (PostgreSQL)")
@DisplayName("jsonPathMatches generates named parameter | PostgreSQL")
fun jsonPathMatchNamedPostgres() {
Configuration.dialectValue = Dialect.POSTGRESQL
ForceDialect.postgres()
assertEquals("jsonb_path_exists(data, :jp::jsonpath)", Where.jsonPathMatches(":jp"))
}
@Test
@DisplayName("jsonPathMatches fails (SQLite)")
@DisplayName("jsonPathMatches fails | SQLite")
fun jsonPathFailsSQLite() {
Configuration.dialectValue = Dialect.SQLITE
ForceDialect.sqlite()
assertThrows<DocumentException> { Where.jsonPathMatches() }
}
}