Initial Development #1

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

View File

@ -14,6 +14,24 @@ class Field<T>(
val parameterName: String? = null, val parameterName: String? = null,
val qualifier: String? = null) { val qualifier: String? = null) {
/**
* Specify the parameter name for the field
*
* @param paramName The parameter name to use for this field
* @return A new `Field` with the parameter name specified
*/
fun withParameterName(paramName: String): Field<T> =
Field(name, comparison, paramName, qualifier)
/**
* Specify a qualifier (alias) for the document table
*
* @param alias The table alias for this field
* @return A new `Field` with the table qualifier specified
*/
fun withQualifier(alias: String): Field<T> =
Field(name, comparison, parameterName, alias)
/** /**
* Get the path for this field * Get the path for this field
* *
@ -32,7 +50,7 @@ class Field<T>(
* @param value The value for the comparison * @param value The value for the comparison
* @return A `Field` with the given comparison * @return A `Field` with the given comparison
*/ */
fun <T> Equal(name: String, value: T): Field<T> = fun <T> equal(name: String, value: T): Field<T> =
Field<T>(name, Comparison(Op.EQUAL, value)) Field<T>(name, Comparison(Op.EQUAL, value))
/** /**
@ -42,7 +60,7 @@ class Field<T>(
* @param value The value for the comparison * @param value The value for the comparison
* @return A `Field` with the given comparison * @return A `Field` with the given comparison
*/ */
fun <T> Greater(name: String, value: T): Field<T> = fun <T> greater(name: String, value: T): Field<T> =
Field(name, Comparison(Op.GREATER, value)) Field(name, Comparison(Op.GREATER, value))
/** /**
@ -52,7 +70,7 @@ class Field<T>(
* @param value The value for the comparison * @param value The value for the comparison
* @return A `Field` with the given comparison * @return A `Field` with the given comparison
*/ */
fun <T> GreaterOrEqual(name: String, value: T): Field<T> = fun <T> greaterOrEqual(name: String, value: T): Field<T> =
Field(name, Comparison(Op.GREATER_OR_EQUAL, value)) Field(name, Comparison(Op.GREATER_OR_EQUAL, value))
/** /**
@ -62,7 +80,7 @@ class Field<T>(
* @param value The value for the comparison * @param value The value for the comparison
* @return A `Field` with the given comparison * @return A `Field` with the given comparison
*/ */
fun <T> Less(name: String, value: T): Field<T> = fun <T> less(name: String, value: T): Field<T> =
Field(name, Comparison(Op.LESS, value)) Field(name, Comparison(Op.LESS, value))
/** /**
@ -72,7 +90,7 @@ class Field<T>(
* @param value The value for the comparison * @param value The value for the comparison
* @return A `Field` with the given comparison * @return A `Field` with the given comparison
*/ */
fun <T> LessOrEqual(name: String, value: T): Field<T> = fun <T> lessOrEqual(name: String, value: T): Field<T> =
Field(name, Comparison(Op.LESS_OR_EQUAL, value)) Field(name, Comparison(Op.LESS_OR_EQUAL, value))
/** /**
@ -82,7 +100,7 @@ class Field<T>(
* @param value The value for the comparison * @param value The value for the comparison
* @return A `Field` with the given comparison * @return A `Field` with the given comparison
*/ */
fun <T> NotEqual(name: String, value: T): Field<T> = fun <T> notEqual(name: String, value: T): Field<T> =
Field(name, Comparison(Op.NOT_EQUAL, value)) Field(name, Comparison(Op.NOT_EQUAL, value))
/** /**
@ -93,22 +111,37 @@ class Field<T>(
* @param maxValue The upper value for the comparison * @param maxValue The upper value for the comparison
* @return A `Field` with the given comparison * @return A `Field` with the given comparison
*/ */
fun <T> Between(name: String, minValue: T, maxValue: T): Field<Pair<T, T>> = fun <T> between(name: String, minValue: T, maxValue: T): Field<Pair<T, T>> =
Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue))) Field(name, Comparison(Op.BETWEEN, Pair(minValue, maxValue)))
fun <T> In(name: String, values: List<T>): Field<List<T>> = /**
* Create a field where any values match (SQL `IN`)
*
* @param name The name of the field to be compared
* @param values The values for the comparison
* @return A `Field` with the given comparison
*/
fun <T> any(name: String, values: List<T>): Field<List<T>> =
Field(name, Comparison(Op.IN, values)) Field(name, Comparison(Op.IN, values))
fun <T> InArray(name: String, tableName: String, values: List<T>): Field<Pair<String, List<T>>> = /**
* Create a field where values should exist in a document's array
*
* @param name The name of the field to be compared
* @param tableName The name of the document table
* @param values The values for the comparison
* @return A `Field` with the given comparison
*/
fun <T> inArray(name: String, tableName: String, values: List<T>): Field<Pair<String, List<T>>> =
Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values))) Field(name, Comparison(Op.IN_ARRAY, Pair(tableName, values)))
fun Exists(name: String): Field<String> = fun exists(name: String): Field<String> =
Field(name, Comparison(Op.EXISTS, "")) Field(name, Comparison(Op.EXISTS, ""))
fun NotExists(name: String): Field<String> = fun notExists(name: String): Field<String> =
Field(name, Comparison(Op.NOT_EXISTS, "")) Field(name, Comparison(Op.NOT_EXISTS, ""))
fun Named(name: String): Field<String> = fun named(name: String): Field<String> =
Field(name, Comparison(Op.EQUAL, "")) Field(name, Comparison(Op.EQUAL, ""))
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String { fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
@ -118,7 +151,7 @@ class Field<T>(
if (dialect == Dialect.POSTGRESQL) { if (dialect == Dialect.POSTGRESQL) {
path.append("#>", extra, "'{", name.replace('.', ','), "}'") path.append("#>", extra, "'{", name.replace('.', ','), "}'")
} else { } else {
val names = mutableListOf(name.split('.')) val names = name.split('.').toMutableList()
val last = names.removeLast() val last = names.removeLast()
names.forEach { path.append("->'", it, "'") } names.forEach { path.append("->'", it, "'") }
path.append("->", extra, "'", last, "'") path.append("->", extra, "'", last, "'")

View File

@ -3,7 +3,7 @@ package solutions.bitbadger.documents.common
/** /**
* How fields should be matched in by-field queries * How fields should be matched in by-field queries
*/ */
enum class FieldMatch(sql: String) { enum class FieldMatch(val sql: String) {
/** Match any of the field criteria (`OR`) */ /** Match any of the field criteria (`OR`) */
ANY("OR"), ANY("OR"),
/** Match all the field criteria (`AND`) */ /** Match all the field criteria (`AND`) */

View File

@ -143,19 +143,19 @@ object Query {
val (field, direction) = val (field, direction) =
if (it.name.indexOf(' ') > -1) { if (it.name.indexOf(' ') > -1) {
val parts = it.name.split(' ') val parts = it.name.split(' ')
Pair(Field.Named(parts[0]), " " + parts.drop(1).joinToString(" ")) Pair(Field.named(parts[0]), " " + parts.drop(1).joinToString(" "))
} else { } else {
Pair<Field<*>, String?>(it, null) Pair<Field<*>, String?>(it, null)
} }
val path = val path =
if (field.name.startsWith("n:")) { if (field.name.startsWith("n:")) {
val fld = Field.Named(field.name.substring(2)) val fld = Field.named(field.name.substring(2))
when (dialect) { when (dialect) {
Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric" Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric"
Dialect.SQLITE -> fld.path(dialect) Dialect.SQLITE -> fld.path(dialect)
} }
} else if (field.name.startsWith("i:")) { } else if (field.name.startsWith("i:")) {
val p = Field.Named(field.name.substring(2)).path(dialect) val p = Field.named(field.name.substring(2)).path(dialect)
when (dialect) { when (dialect) {
Dialect.POSTGRESQL -> "LOWER($p)" Dialect.POSTGRESQL -> "LOWER($p)"
Dialect.SQLITE -> "$p COLLATE NOCASE" Dialect.SQLITE -> "$p COLLATE NOCASE"

View File

@ -0,0 +1,20 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class FieldMatchTest {
@Test
@DisplayName("ANY uses proper SQL")
fun any() {
assertEquals("OR", FieldMatch.ANY.sql, "ANY should use OR")
}
@Test
@DisplayName("ALL uses proper SQL")
fun all() {
assertEquals("AND", FieldMatch.ALL.sql, "ALL should use AND")
}
}

View File

@ -8,13 +8,270 @@ import kotlin.test.assertNull
class FieldTest { class FieldTest {
@Test @Test
@DisplayName("Equal constructs a field") @DisplayName("equal constructs a field")
fun equalCtor() { fun equalCtor() {
val field = Field.Equal("Test", 14) val field = Field.equal("Test", 14)
assertEquals("Test", field.name, "Field name not filled correctly") assertEquals("Test", field.name, "Field name not filled correctly")
assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly") assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(14, field.comparison.value, "Field comparison value not filled correctly") assertEquals(14, field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null") assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null") assertNull(field.qualifier, "The qualifier should have been null")
} }
@Test
@DisplayName("greater constructs a field")
fun greaterCtor() {
val field = Field.greater("Great", "night")
assertEquals("Great", field.name, "Field name not filled correctly")
assertEquals(Op.GREATER, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("night", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("greaterOrEqual constructs a field")
fun greaterOrEqualCtor() {
val field = Field.greaterOrEqual("Nice", 88L)
assertEquals("Nice", field.name, "Field name not filled correctly")
assertEquals(Op.GREATER_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(88L, field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("less constructs a field")
fun lessCtor() {
val field = Field.less("Lesser", "seven")
assertEquals("Lesser", field.name, "Field name not filled correctly")
assertEquals(Op.LESS, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("seven", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("lessOrEqual constructs a field")
fun lessOrEqualCtor() {
val field = Field.lessOrEqual("Nobody", "KNOWS")
assertEquals("Nobody", field.name, "Field name not filled correctly")
assertEquals(Op.LESS_OR_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("KNOWS", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("notEqual constructs a field")
fun notEqualCtor() {
val field = Field.notEqual("Park", "here")
assertEquals("Park", field.name, "Field name not filled correctly")
assertEquals(Op.NOT_EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("here", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("between constructs a field")
fun betweenCtor() {
val field = Field.between("Age", 18, 49)
assertEquals("Age", field.name, "Field name not filled correctly")
assertEquals(Op.BETWEEN, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(18, field.comparison.value.first, "Field comparison min value not filled correctly")
assertEquals(49, field.comparison.value.second, "Field comparison max value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("any constructs a field")
fun inCtor() {
val field = Field.any("Here", listOf(8, 16, 32))
assertEquals("Here", field.name, "Field name not filled correctly")
assertEquals(Op.IN, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals(listOf(8, 16, 32), field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("inArray constructs a field")
fun inArrayCtor() {
val field = Field.inArray("ArrayField", "table", listOf("z"))
assertEquals("ArrayField", field.name, "Field name not filled correctly")
assertEquals(Op.IN_ARRAY, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("table", field.comparison.value.first, "Field comparison table not filled correctly")
assertEquals(listOf("z"), field.comparison.value.second, "Field comparison values not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("exists constructs a field")
fun existsCtor() {
val field = Field.exists("Groovy")
assertEquals("Groovy", field.name, "Field name not filled correctly")
assertEquals(Op.EXISTS, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("notExists constructs a field")
fun notExistsCtor() {
val field = Field.notExists("Groovy")
assertEquals("Groovy", field.name, "Field name not filled correctly")
assertEquals(Op.NOT_EXISTS, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("named constructs a field")
fun namedCtor() {
val field = Field.named("Tacos")
assertEquals("Tacos", field.name, "Field name not filled correctly")
assertEquals(Op.EQUAL, field.comparison.op, "Field comparison operation not filled correctly")
assertEquals("", field.comparison.value, "Field comparison value not filled correctly")
assertNull(field.parameterName, "The parameter name should have been null")
assertNull(field.qualifier, "The qualifier should have been null")
}
@Test
@DisplayName("nameToPath creates a simple PostgreSQL SQL name")
fun nameToPathPostgresSimpleSQL() {
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly")
}
@Test
@DisplayName("nameToPath creates a simple SQLite SQL name")
fun nameToPathSQLiteSimpleSQL() {
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly")
}
@Test
@DisplayName("nameToPath creates a nested PostgreSQL SQL name")
fun nameToPathPostgresNestedSQL() {
assertEquals("data#>>'{A,Long,Path,to,the,Property}'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly")
}
@Test
@DisplayName("nameToPath creates a nested SQLite SQL name")
fun nameToPathSQLiteNestedSQL() {
assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->>'Property'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly")
}
@Test
@DisplayName("nameToPath creates a simple PostgreSQL JSON name")
fun nameToPathPostgresSimpleJSON() {
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly")
}
@Test
@DisplayName("nameToPath creates a simple SQLite JSON name")
fun nameToPathSQLiteSimpleJSON() {
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly")
}
@Test
@DisplayName("nameToPath creates a nested PostgreSQL JSON name")
fun nameToPathPostgresNestedJSON() {
assertEquals("data#>'{A,Long,Path,to,the,Property}'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly")
}
@Test
@DisplayName("nameToPath creates a nested SQLite JSON name")
fun nameToPathSQLiteNestedJSON() {
assertEquals("data->'A'->'Long'->'Path'->'to'->'the'->'Property'",
Field.nameToPath("A.Long.Path.to.the.Property", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly")
}
@Test
@DisplayName("withParameterName adjusts the parameter name")
fun withParameterName() {
assertEquals(":name", Field.equal("Bob", "Tom").withParameterName(":name").parameterName,
"Parameter name not filled correctly")
}
@Test
@DisplayName("withQualifier adjust the table qualifier")
fun withQualifier() {
assertEquals("joe", Field.equal("Bill", "Matt").withQualifier("joe").qualifier,
"Qualifier not filled correctly")
}
@Test
@DisplayName("path generates for simple unqualified PostgreSQL field")
fun pathPostgresSimpleUnqualified() {
assertEquals("data->>'SomethingCool'",
Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct")
}
@Test
@DisplayName("path generates for simple qualified PostgreSQL field")
fun pathPostgresSimpleQualified() {
assertEquals("this.data->>'SomethingElse'",
Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not correct")
}
@Test
@DisplayName("path generates for nested unqualified PostgreSQL field")
fun pathPostgresNestedUnqualified() {
assertEquals("data#>>'{My,Nested,Field}'",
Field.equal("My.Nested.Field", "howdy").path(Dialect.POSTGRESQL, FieldFormat.SQL), "Path not correct")
}
@Test
@DisplayName("path generates for nested qualified PostgreSQL field")
fun pathPostgresNestedQualified() {
assertEquals("bird.data#>>'{Nest,Away}'",
Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not correct")
}
@Test
@DisplayName("path generates for simple unqualified SQLite field")
fun pathSQLiteSimpleUnqualified() {
assertEquals("data->>'SomethingCool'",
Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct")
}
@Test
@DisplayName("path generates for simple qualified SQLite field")
fun pathSQLiteSimpleQualified() {
assertEquals("this.data->>'SomethingElse'",
Field.less("SomethingElse", 9).withQualifier("this").path(Dialect.SQLITE, FieldFormat.SQL),
"Path not correct")
}
@Test
@DisplayName("path generates for nested unqualified SQLite field")
fun pathSQLiteNestedUnqualified() {
assertEquals("data->'My'->'Nested'->>'Field'",
Field.equal("My.Nested.Field", "howdy").path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct")
}
@Test
@DisplayName("path generates for nested qualified SQLite field")
fun pathSQLiteNestedQualified() {
assertEquals("bird.data->'Nest'->>'Away'",
Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL),
"Path not correct")
}
} }

View File

@ -0,0 +1,26 @@
package solutions.bitbadger.documents.common
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class ParameterNameTest {
@Test
@DisplayName("derive works when given existing names")
fun withExisting() {
val names = ParameterName()
assertEquals(":taco", names.derive(":taco"), "Name should have been :taco")
assertEquals(":field0", names.derive(null), "Counter should not have advanced for named field")
}
@Test
@DisplayName("derive works when given all anonymous fields")
fun allAnonymous() {
val names = ParameterName()
assertEquals(":field0", names.derive(null), "Anonymous field name should have been returned")
assertEquals(":field1", names.derive(null), "Counter should have advanced from previous call")
assertEquals(":field2", names.derive(null), "Counter should have advanced from previous call")
assertEquals(":field3", names.derive(null), "Counter should have advanced from previous call")
}
}