Initial Development #1

Merged
danieljsummers merged 88 commits from v1-rc into main 2025-04-16 01:29:20 +00:00
385 changed files with 41134 additions and 1 deletions

6
.gitignore vendored
View File

@ -26,3 +26,9 @@ replay_pid*
# Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects # Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects
.kotlin/ .kotlin/
# Temporary output directories
**/target
# Maven Central Repo settings
settings.xml

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<ScalaCodeStyleSettings>
<option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
</ScalaCodeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

25
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="kotlin" />
<module name="core" />
<module name="groovy" />
<module name="scala" />
<module name="kotlinx" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
<module name="common" target="1.8" />
<module name="core8" target="1.8" />
<module name="documents (2)" target="1.5" />
<module name="java" target="17" />
<module name="jvm" target="11" />
<module name="sqlite" target="1.8" />
</bytecodeTargetLevel>
</component>
</project>

33
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/core/src/main/kotlin" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/core/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/core/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/core/src/main/kotlin" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/core/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/groovy/src/main/groovy" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/groovy/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/groovy/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/java/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/java/src/main/kotlin" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/java/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/jvm/src/main/kotlin" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/jvm/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/kotlin/src/main/kotlin" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/kotlin/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/kotlinx/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/kotlinx/src/main/kotlin" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/kotlinx/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/kotlin" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/scala/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/scala/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/scala/src/main/scala" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/sqlite/src/main/kotlin" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/sqlite/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/src/main/resources" charset="UTF-8" />
</component>
</project>

25
.idea/jarRepositories.xml generated Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="mavenCentral" />
<option name="name" value="mavenCentral" />
<option name="url" value="https://repo1.maven.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

16
.idea/kotlinc.xml generated Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JsCompilerArguments">
<option name="moduleKind" value="plain" />
</component>
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="17" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.1" />
<option name="languageVersion" value="2.1" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.20" />
</component>
</project>

23
.idea/libraries/KotlinJavaRuntime.xml generated Normal file
View File

@ -0,0 +1,23 @@
<component name="libraryTable">
<library name="KotlinJavaRuntime" type="repository">
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.0.21/kotlin-stdlib-jdk8-2.0.21.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.0.21/kotlin-stdlib-2.0.21.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.0.21/kotlin-stdlib-jdk7-2.0.21.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.0.21/kotlin-stdlib-jdk8-2.0.21-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.0.21/kotlin-stdlib-2.0.21-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.0.21/kotlin-stdlib-jdk7-2.0.21-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.0.21/kotlin-stdlib-jdk8-2.0.21-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.0.21/kotlin-stdlib-2.0.21-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.0.21/kotlin-stdlib-jdk7-2.0.21-sources.jar!/" />
</SOURCES>
</library>
</component>

View File

@ -0,0 +1,26 @@
<component name="libraryTable">
<library name="Maven: scala-sdk-3.0.0" type="Scala">
<properties>
<language-level>Scala_3_0</language-level>
<compiler-classpath>
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-compiler_3/3.0.0/scala3-compiler_3-3.0.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-interfaces/3.0.0/scala3-interfaces-3.0.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-library_3/3.0.0/scala3-library_3-3.0.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala-library/2.13.5/scala-library-2.13.5.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/tasty-core_3/3.0.0/tasty-core_3-3.0.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/modules/scala-asm/9.1.0-scala-1/scala-asm-9.1.0-scala-1.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-sbt/compiler-interface/1.3.5/compiler-interface-1.3.5.jar" />
<root url="file://$MAVEN_REPOSITORY$/com/google/protobuf/protobuf-java/3.7.0/protobuf-java-3.7.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-sbt/util-interface/1.3.0/util-interface-1.3.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-reader/3.19.0/jline-reader-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.19.0/jline-terminal-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jna/3.19.0/jline-terminal-jna-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.3.1/jna-5.3.1.jar" />
</compiler-classpath>
<compiler-bridge-binary-jar>file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-sbt-bridge/3.0.0/scala3-sbt-bridge-3.0.0.jar</compiler-bridge-binary-jar>
</properties>
<CLASSES />
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -0,0 +1,26 @@
<component name="libraryTable">
<library name="Maven: scala-sdk-3.1.3" type="Scala">
<properties>
<language-level>Scala_3_1</language-level>
<compiler-classpath>
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-compiler_3/3.1.3/scala3-compiler_3-3.1.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-interfaces/3.1.3/scala3-interfaces-3.1.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-library_3/3.1.3/scala3-library_3-3.1.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala-library/2.13.8/scala-library-2.13.8.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/tasty-core_3/3.1.3/tasty-core_3-3.1.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/modules/scala-asm/9.2.0-scala-1/scala-asm-9.2.0-scala-1.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-sbt/compiler-interface/1.3.5/compiler-interface-1.3.5.jar" />
<root url="file://$MAVEN_REPOSITORY$/com/google/protobuf/protobuf-java/3.7.0/protobuf-java-3.7.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-sbt/util-interface/1.3.0/util-interface-1.3.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-reader/3.19.0/jline-reader-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.19.0/jline-terminal-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jna/3.19.0/jline-terminal-jna-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.3.1/jna-5.3.1.jar" />
</compiler-classpath>
<compiler-bridge-binary-jar>file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-sbt-bridge/3.1.3/scala3-sbt-bridge-3.1.3.jar</compiler-bridge-binary-jar>
</properties>
<CLASSES />
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -0,0 +1,25 @@
<component name="libraryTable">
<library name="Maven: scala-sdk-3.3.3" type="Scala">
<properties>
<language-level>Scala_3_3</language-level>
<compiler-classpath>
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-compiler_3/3.3.3/scala3-compiler_3-3.3.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-interfaces/3.3.3/scala3-interfaces-3.3.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-library_3/3.3.3/scala3-library_3-3.3.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala-library/2.13.12/scala-library-2.13.12.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/tasty-core_3/3.3.3/tasty-core_3-3.3.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/modules/scala-asm/9.5.0-scala-1/scala-asm-9.5.0-scala-1.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-sbt/compiler-interface/1.9.3/compiler-interface-1.9.3.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-sbt/util-interface/1.9.2/util-interface-1.9.2.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-reader/3.19.0/jline-reader-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.19.0/jline-terminal-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jna/3.19.0/jline-terminal-jna-3.19.0.jar" />
<root url="file://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.3.1/jna-5.3.1.jar" />
</compiler-classpath>
<compiler-bridge-binary-jar>file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-sbt-bridge/3.3.3/scala3-sbt-bridge-3.3.3.jar</compiler-bridge-binary-jar>
</properties>
<CLASSES />
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@ -0,0 +1,26 @@
<component name="libraryTable">
<library name="Maven: scala-sdk-3.5.2" type="Scala">
<properties>
<language-level>Scala_3_5</language-level>
<compiler-classpath>
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-compiler_3/3.5.2/scala3-compiler_3-3.5.2.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-interfaces/3.5.2/scala3-interfaces-3.5.2.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-library_3/3.5.2/scala3-library_3-3.5.2.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/scala-library/2.13.14/scala-library-2.13.14.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/tasty-core_3/3.5.2/tasty-core_3-3.5.2.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-lang/modules/scala-asm/9.7.0-scala-2/scala-asm-9.7.0-scala-2.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-sbt/compiler-interface/1.9.6/compiler-interface-1.9.6.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/scala-sbt/util-interface/1.9.8/util-interface-1.9.8.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-reader/3.25.1/jline-reader-3.25.1.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.25.1/jline-terminal-3.25.1.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-native/3.25.1/jline-native-3.25.1.jar" />
<root url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jna/3.25.1/jline-terminal-jna-3.25.1.jar" />
<root url="file://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar" />
</compiler-classpath>
<compiler-bridge-binary-jar>file://$MAVEN_REPOSITORY$/org/scala-lang/scala3-sbt-bridge/3.5.2/scala3-sbt-bridge-3.5.2.jar</compiler-bridge-binary-jar>
</properties>
<CLASSES />
<JAVADOC />
<SOURCES />
</library>
</component>

23
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/src/common/pom.xml" />
<option value="$PROJECT_DIR$/src/sqlite/pom.xml" />
<option value="$PROJECT_DIR$/pom.xml" />
<option value="$PROJECT_DIR$/old-pom.xml" />
</list>
</option>
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/src/common/pom.xml" />
<option value="$PROJECT_DIR$/src/sqlite/pom.xml" />
</set>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="corretto-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

7
.idea/scala_compiler.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ScalaCompilerConfiguration">
<option name="incrementalityType" value="IDEA" />
<profile name="Maven 1" modules="core,jvm,scala" />
</component>
</project>

6
.idea/scala_settings.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ScalaProjectSettings">
<option name="scala3DisclaimerShown" value="true" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,3 +1,53 @@
# solutions.bitbadger.documents # solutions.bitbadger.documents
Treat PostgreSQL and SQLite as document stores from Java and Kotlin Treat PostgreSQL and SQLite as document stores from Java, Kotlin, Scala, and Groovy
## Examples
```java
// Retrieve (find) all orders (Java)
public List<Order> findOrders(Connection conn) {
Find.all(/*table name*/ "order", /*type*/ Order.class, conn);
}
```
```kotlin
// Mark an order as fulfilled (Kotlin)
fun markFulfilled(orderId: Long, conn: Connection) =
conn.patchById(
/*table name*/ "order",
/*document ID*/ orderId,
/*patch object*/ mapOf("fulfilled" to true)
)
```
```scala
// Delete orders marked as obsolete (Scala)
def deleteObsolete(Connection conn):
conn.deleteByFields(/*table name*/ "order",
/*field criteria*/ Field.equal("obsolete", true) :: Nil)
```
```groovy
// Remove the pending status from multiple orders (Groovy)
void clearPending(List<Long> orderIds, Connection conn) {
conn.removeFieldsByFields(/*table name*/ "order",
/*field criteria*/ List.of(Field.any("id", orderIds)),
/*fields to remove*/ List.of("pending"))
}
```
## Packages / Modules
* The `core` module provides the base implementation and can be used from any JVM language.
* The `solutions.bitbadger.documents` package contains support types like `Configuration` and `Field`.
* The `solutions.bitbadger.documents.java` package contains document access functions and serialization config.
* The `solutions.bitbadger.documents.java.extensions` package contains extensions on the JDBC `Connection` object, callable as extension methods from Kotlin or as static functions from other languages.
* The `groovy` module packages the extension methods so that Groovy can access them. No other packages will need to be imported; they will show up on any `Connection` instance.
* The `kotlinx` module utilizes the kotlinx-serialization project for its JSON serialization, which requires a different serializer and different function/method signatures (`inline fun <reified T> ...` vs. `fun <T> ...`).
* `solutions.bitbadger.documents.kotlinx` and `solutions.bitbadger.documents.kotlinx.extensions` packages expose a similar API to their `java` counterparts, but one designed to be consumed from Kotlin. Generally, document retrieval functions will require a generic parameter instead of a `Class<T>` parameter.
* The `scala` module extends `core` by utilizing Scala's implicit `ClassTag`s to remove the `Class[T]` parameter.
* `solutions.bitbadger.documents.scala` and `solutions.bitbadger.documents.scala.extensions` packages expose the same API as their `java` counterparts, utilizing Scala collections and `Option`s instead of Java collections and `Optional`s.

117
pom.xml Normal file
View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>solutions.bitbadger</groupId>
<artifactId>documents</artifactId>
<version>1.0.0-RC1</version>
<packaging>pom</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<description>Expose a document store interface for PostgreSQL and SQLite</description>
<url>https://relationaldocs.bitbadger.solutions/jvm/</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://www.opensource.org/licenses/mit-license.php</url>
</license>
</licenses>
<developers>
<developer>
<name>Daniel J. Summers</name>
<email>daniel@bitbadger.solutions</email>
<organization>Bit Badger Solutions</organization>
<organizationUrl>https://bitbadger.solutions</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git</connection>
<developerConnection>scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git</developerConnection>
<url>https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents</url>
</scm>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.code.style>official</kotlin.code.style>
<java.version>17</java.version>
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<kotlin.version>2.1.20</kotlin.version>
<serialization.version>1.8.0</serialization.version>
<scala.version>3.5.2</scala.version>
<groovy.version>4.0.26</groovy.version>
<surefire.version>3.5.2</surefire.version>
<failsafe.version>3.5.2</failsafe.version>
<jackson.version>2.18.2</jackson.version>
<sqlite.version>3.46.1.2</sqlite.version>
<postgresql.version>42.7.5</postgresql.version>
<buildHelperPlugin.version>3.6.0</buildHelperPlugin.version>
<sourcePlugin.version>3.3.1</sourcePlugin.version>
<javaDocPlugin.version>3.11.2</javaDocPlugin.version>
</properties>
<modules>
<module>./src/core</module>
<module>./src/groovy</module>
<module>./src/kotlinx</module>
<module>./src/scala</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>${sqlite.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.16</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="GENERAL_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
src/core/core.iml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
</content>
</component>
</module>

182
src/core/pom.xml Normal file
View File

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>solutions.bitbadger</groupId>
<artifactId>documents</artifactId>
<version>1.0.0-RC1</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<groupId>solutions.bitbadger.documents</groupId>
<artifactId>core</artifactId>
<name>${project.groupId}:${project.artifactId}</name>
<description>Expose a document store interface for PostgreSQL and SQLite (Core Library)</description>
<url>https://relationaldocs.bitbadger.solutions/jvm/</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://www.opensource.org/licenses/mit-license.php</url>
</license>
</licenses>
<developers>
<developer>
<name>Daniel J. Summers</name>
<email>daniel@bitbadger.solutions</email>
<organization>Bit Badger Solutions</organization>
<organizationUrl>https://bitbadger.solutions</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git</connection>
<developerConnection>scm:git:https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents.git</developerConnection>
<url>https://git.bitbadger.solutions/bit-badger/solutions.bitbadger.documents</url>
</scm>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>process-test-sources</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/java</sourceDir>
<sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire.version}</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${failsafe.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>${buildHelperPlugin.version}</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/kotlin</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${sourcePlugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.dokka</groupId>
<artifactId>dokka-maven-plugin</artifactId>
<version>2.0.0</version>
<configuration>
<reportUndocumented>true</reportUndocumented>
<includes>${project.basedir}/src/main/module-info.md</includes>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>javadocJar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.7.0</version>
<extensions>true</extensions>
<configuration>
<deploymentName>Deployment-core-${project.version}</deploymentName>
<publishingServerId>central</publishingServerId>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,9 @@
module solutions.bitbadger.documents.core {
requires kotlin.stdlib;
requires kotlin.reflect;
requires java.sql;
exports solutions.bitbadger.documents;
exports solutions.bitbadger.documents.java;
exports solutions.bitbadger.documents.java.extensions;
exports solutions.bitbadger.documents.query;
}

View File

@ -0,0 +1,85 @@
package solutions.bitbadger.documents
import kotlin.jvm.Throws
import kotlin.reflect.full.*
import kotlin.reflect.jvm.isAccessible
/**
* Strategies for automatic document IDs
*/
enum class AutoId {
/** No automatic IDs will be generated */
DISABLED,
/** Generate a `MAX`-plus-1 numeric ID */
NUMBER,
/** Generate a `UUID` string ID */
UUID,
/** Generate a random hex character string ID */
RANDOM_STRING;
companion object {
/**
* Generate a `UUID` string
*
* @return A `UUID` string
*/
@JvmStatic
fun generateUUID(): String =
java.util.UUID.randomUUID().toString().replace("-", "")
/**
* Generate a string of random hex characters
*
* @param length The length of the string (optional; defaults to configured length)
* @return A string of random hex characters of the requested length
*/
@JvmStatic
fun generateRandomString(length: Int? = null): String =
(length ?: Configuration.idStringLength).let { len ->
kotlin.random.Random.nextBytes((len + 2) / 2)
.joinToString("") { String.format("%02x", it) }
.substring(0, len)
}
/**
* Determine if a document needs an automatic ID applied
*
* @param strategy The auto ID strategy for which the document is evaluated
* @param document The document whose need of an automatic ID should be determined
* @param idProp The name of the document property containing the ID
* @return `true` if the document needs an automatic ID, `false` if not
* @throws DocumentException If bad input prevents the determination
*/
@Throws(DocumentException::class)
@JvmStatic
fun <T> needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean {
if (document == null) throw DocumentException("document cannot be null")
if (strategy == DISABLED) return false
val id = document!!::class.memberProperties.find { it.name == idProp }?.apply { isAccessible = true }
if (id == null) throw DocumentException("$idProp not found in document")
if (strategy == NUMBER) {
return when (id.returnType) {
Byte::class.createType() -> id.call(document) == 0.toByte()
Short::class.createType() -> id.call(document) == 0.toShort()
Int::class.createType() -> id.call(document) == 0
Long::class.createType() -> id.call(document) == 0.toLong()
else -> throw DocumentException("$idProp was not a number; cannot auto-generate number ID")
}
}
val typ = id.returnType.toString()
if (typ.endsWith("String") || typ.endsWith("String!")) {
return id.call(document) == ""
}
throw DocumentException("$idProp was not a string ($typ); cannot auto-generate UUID or random string")
}
}
}

View File

@ -0,0 +1,68 @@
package solutions.bitbadger.documents
/**
* Information required to generate a JSON field comparison
*/
interface Comparison<T> {
/** The operation for the field comparison */
val op: Op
/** The value against which the comparison will be made */
val value: T
/** Whether the value should be considered numeric */
val isNumeric: Boolean
}
/**
* Function to determine if a value is numeric
*
* @param it The value in question
* @return True if it is a numeric type, false if not
*/
private fun <T> isNumeric(it: T) =
it is Byte || it is Short || it is Int || it is Long
/**
* A single-value comparison against a field in a JSON document
*/
class ComparisonSingle<T>(override val op: Op, override val value: T) : Comparison<T> {
init {
when (op) {
Op.BETWEEN, Op.IN, Op.IN_ARRAY ->
throw DocumentException("Cannot use single comparison for multiple values")
else -> { }
}
}
override val isNumeric = isNumeric(value)
override fun toString() =
"$op $value"
}
/**
* A range comparison against a field in a JSON document
*/
class ComparisonBetween<T>(override val value: Pair<T, T>) : Comparison<Pair<T, T>> {
override val op = Op.BETWEEN
override val isNumeric = isNumeric(value.first)
}
/**
* A check within a collection of values
*/
class ComparisonIn<T>(override val value: Collection<T>) : Comparison<Collection<T>> {
override val op = Op.IN
override val isNumeric = !value.isEmpty() && isNumeric(value.elementAt(0))
}
/**
* A check within a collection of values against an array in a document
*/
class ComparisonInArray<T>(override val value: Pair<String, Collection<T>>) : Comparison<Pair<String, Collection<T>>> {
override val op = Op.IN_ARRAY
override val isNumeric = false
}

View File

@ -0,0 +1,67 @@
package solutions.bitbadger.documents
import java.sql.Connection
import java.sql.DriverManager
/**
* Configuration for the document library
*/
object Configuration {
/** The field in which a document's ID is stored */
@JvmField
var idField = "id"
/** The automatic ID strategy to use */
@JvmField
var autoIdStrategy = AutoId.DISABLED
/** The length of automatic random hex character string */
@JvmField
var idStringLength = 16
/** The derived dialect value from the connection string */
private var dialectValue: Dialect? = null
/** The connection string for the JDBC connection */
@JvmStatic
var connectionString: String? = null
/**
* Set a value for the connection string
* @param value The connection string to set
*/
set(value) {
field = value
dialectValue = if (value.isNullOrBlank()) null else Dialect.deriveFromConnectionString(value)
}
/**
* Retrieve a new connection to the configured database
*
* @return A new connection to the configured database
* @throws DocumentException If the connection string is not set before calling this
*/
@Throws(DocumentException::class)
@JvmStatic
fun dbConn(): Connection {
if (connectionString == null) {
throw DocumentException("Please provide a connection string before attempting data access")
}
return DriverManager.getConnection(connectionString)
}
/**
* The dialect in use
*
* @param process The process being attempted
* @return The dialect for the current connection
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun dialect(process: String? = null): Dialect =
dialectValue ?: throw DocumentException(
"Database mode not set" + if (process == null) "" else "; cannot $process"
)
}

View File

@ -0,0 +1,33 @@
package solutions.bitbadger.documents
import kotlin.jvm.Throws
/**
* The SQL dialect to use when building queries
*/
enum class Dialect {
/** PostgreSQL */
POSTGRESQL,
/** SQLite */
SQLITE;
companion object {
/**
* Derive the dialect from the given connection string
*
* @param connectionString The connection string from which the dialect will be derived
* @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
connectionString.contains(":postgresql:") -> POSTGRESQL
else -> throw DocumentException("Cannot determine dialect from [$connectionString]")
}
}
}

View File

@ -0,0 +1,9 @@
package solutions.bitbadger.documents
/**
* An exception caused by invalid operations in the document library
*
* @param message The message for the exception
* @param cause The underlying exception (optional)
*/
class DocumentException @JvmOverloads constructor(message: String, cause: Throwable? = null) : Exception(message, cause)

View File

@ -0,0 +1,13 @@
package solutions.bitbadger.documents
/**
* The type of index to generate for the document
*/
enum class DocumentIndex(val sql: String) {
/** A GIN index with standard operations (all operators supported) */
FULL(""),
/** A GIN index with JSONPath operations (optimized for @>, @?, @@ operators) */
OPTIMIZED(" jsonb_path_ops")
}

View File

@ -0,0 +1,24 @@
package solutions.bitbadger.documents
/**
* The interface for a document serializer/deserializer
*/
interface DocumentSerializer {
/**
* Serialize a document to its JSON representation
*
* @param document The document to be serialized
* @return The JSON representation of the document
*/
fun <TDoc> serialize(document: TDoc): String
/**
* Deserialize a document from its JSON representation
*
* @param json The JSON representation of the document
* @param clazz The class of the document to be deserialized
* @return The document instance represented by the given JSON string
*/
fun <TDoc> deserialize(json: String, clazz: Class<TDoc>): TDoc
}

View File

@ -0,0 +1,320 @@
package solutions.bitbadger.documents
import kotlin.jvm.Throws
/**
* A field and its comparison
*
* @property name The name of the field in the JSON document
* @property comparison The comparison to apply against the field
* @property parameterName The name of the parameter to use in the query (optional, generated if missing)
* @property qualifier A table qualifier to use to address the `data` field (useful for multi-table queries)
*/
class Field<T> private constructor(
val name: String,
val comparison: Comparison<T>,
val parameterName: String? = null,
val qualifier: String? = null) {
init {
if (parameterName != null && !parameterName.startsWith(':') && !parameterName.startsWith('@'))
throw DocumentException("Parameter Name must start with : or @ ($name)")
}
/**
* 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(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(name, comparison, parameterName, alias)
/**
* Get the path for this field
*
* @param dialect The SQL dialect to use for the path to the JSON field
* @param format Whether the value should be retrieved as JSON or SQL (optional, default SQL)
* @return The path for the field
*/
@JvmOverloads
fun path(dialect: Dialect, format: FieldFormat = FieldFormat.SQL): String =
(if (qualifier == null) "" else "${qualifier}.") + nameToPath(name, dialect, format)
/** Parameters to bind each value of `IN` and `IN_ARRAY` operations */
private val inParameterNames: String
get() {
val values = if (comparison.op == Op.IN) {
comparison.value as Collection<*>
} else {
val parts = comparison.value as Pair<*, *>
parts.second as Collection<*>
}
return List(values.size) { idx -> "${parameterName}_$idx" }.joinToString(", ")
}
/**
* Create a `WHERE` clause fragment for this field
*
* @return The `WHERE` clause for this field
* @throws DocumentException If the field has no parameter name or the database dialect has not been set
*/
@Throws(DocumentException::class)
fun toWhere(): String {
if (parameterName == null && !listOf(Op.EXISTS, Op.NOT_EXISTS).contains(comparison.op))
throw DocumentException("Parameter for $name must be specified")
val dialect = Configuration.dialect("make field WHERE clause")
val fieldName = path(dialect, if (comparison.op == Op.IN_ARRAY) FieldFormat.JSON else FieldFormat.SQL)
val fieldPath = when (dialect) {
Dialect.POSTGRESQL -> if (comparison.isNumeric) "($fieldName)::numeric" else fieldName
Dialect.SQLITE -> fieldName
}
val criteria = when (comparison.op) {
in listOf(Op.EXISTS, Op.NOT_EXISTS) -> ""
Op.BETWEEN -> " ${parameterName}min AND ${parameterName}max"
Op.IN -> " ($inParameterNames)"
Op.IN_ARRAY -> if (dialect == Dialect.POSTGRESQL) " ARRAY[$inParameterNames]" else ""
else -> " $parameterName"
}
@Suppress("UNCHECKED_CAST")
return if (dialect == Dialect.SQLITE && comparison.op == Op.IN_ARRAY) {
val (table, _) = comparison.value as? Pair<String, *> ?: throw DocumentException("InArray field invalid")
"EXISTS (SELECT 1 FROM json_each($table.data, '$.$name') WHERE value IN ($inParameterNames))"
} else {
"$fieldPath ${comparison.op.sql}$criteria"
}
}
/**
* Append the parameters required for this field
*
* @param existing The existing parameters
* @return The collection with the necessary parameters appended
*/
fun appendParameter(existing: MutableCollection<Parameter<*>>): MutableCollection<Parameter<*>> {
val typ = if (comparison.isNumeric) ParameterType.NUMBER else ParameterType.STRING
when (comparison) {
is ComparisonBetween<*> -> {
existing.add(Parameter("${parameterName}min", typ, comparison.value.first))
existing.add(Parameter("${parameterName}max", typ, comparison.value.second))
}
is ComparisonIn<*> -> {
comparison.value.forEachIndexed { index, item ->
existing.add(Parameter("${parameterName}_$index", typ, item))
}
}
is ComparisonInArray<*> -> {
val mkString = Configuration.dialect("append parameters for InArray") == Dialect.POSTGRESQL
comparison.value.second.forEachIndexed { index, item ->
if (mkString) {
existing.add(Parameter("${parameterName}_$index", ParameterType.STRING, "$item"))
} else {
existing.add(Parameter("${parameterName}_$index", typ, item))
}
}
}
else -> {
if (comparison.op != Op.EXISTS && comparison.op != Op.NOT_EXISTS) {
existing.add(Parameter(parameterName!!, typ, comparison.value))
}
}
}
return existing
}
override fun toString() =
"Field ${parameterName ?: "<unnamed>"} $comparison${qualifier?.let { " (qualifier $it)" } ?: ""}"
companion object {
/**
* Create a field equality comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> equal(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.EQUAL, value), paramName)
/**
* Create a field greater-than comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> greater(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.GREATER, value), paramName)
/**
* Create a field greater-than-or-equal-to comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> greaterOrEqual(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.GREATER_OR_EQUAL, value), paramName)
/**
* Create a field less-than comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> less(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.LESS, value), paramName)
/**
* Create a field less-than-or-equal-to comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> lessOrEqual(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.LESS_OR_EQUAL, value), paramName)
/**
* Create a field inequality comparison
*
* @param name The name of the field to be compared
* @param value The value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> notEqual(name: String, value: T, paramName: String? = null) =
Field(name, ComparisonSingle(Op.NOT_EQUAL, value), paramName)
/**
* Create a field range comparison
*
* @param name The name of the field to be compared
* @param minValue The lower value for the comparison
* @param maxValue The upper value for the comparison
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> between(name: String, minValue: T, maxValue: T, paramName: String? = null) =
Field(name, ComparisonBetween(Pair(minValue, maxValue)), paramName)
/**
* 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
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> any(name: String, values: Collection<T>, paramName: String? = null) =
Field(name, ComparisonIn(values), paramName)
/**
* 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
* @param paramName The parameter name for the field (optional, defaults to auto-generated)
* @return A `Field` with the given comparison
*/
@JvmStatic
@JvmOverloads
fun <T> inArray(name: String, tableName: String, values: Collection<T>, paramName: String? = null) =
Field(name, ComparisonInArray(Pair(tableName, values)), paramName)
/**
* Create a field where a document field should exist
*
* @param name The name of the field whose existence should be checked
* @return A `Field` with the given comparison
*/
@JvmStatic
fun exists(name: String) =
Field(name, ComparisonSingle(Op.EXISTS, ""))
/**
* Create a field where a document field should not exist
*
* @param name The name of the field whose existence should be checked
* @return A `Field` with the given comparison
*/
@JvmStatic
fun notExists(name: String) =
Field(name, ComparisonSingle(Op.NOT_EXISTS, ""))
/**
* Create a field with a given named comparison (useful for ordering fields)
*
* @param name The name of the field
* @return A `Field` with the given name (comparison equal to an empty string)
*/
@JvmStatic
fun named(name: String) =
Field(name, ComparisonSingle(Op.EQUAL, ""))
/**
* Convert a name to the SQL path for the given dialect
*
* @param name The field name to be translated
* @param dialect The database for which the path should be created
* @param format Whether the field should be retrieved as a JSON value or a SQL value
* @return The path to the JSON field
*/
@JvmStatic
fun nameToPath(name: String, dialect: Dialect, format: FieldFormat): String {
val path = StringBuilder("data")
val extra = if (format == FieldFormat.SQL) ">" else ""
if (name.indexOf('.') > -1) {
if (dialect == Dialect.POSTGRESQL) {
path.append("#>", extra, "'{", name.replace('.', ','), "}'")
} else {
val names = name.split('.').toMutableList()
val last = names.removeLast()
names.forEach { path.append("->'", it, "'") }
path.append("->", extra, "'", last, "'")
}
} else {
path.append("->", extra, "'", name, "'")
}
return path.toString()
}
}
}

View File

@ -0,0 +1,12 @@
package solutions.bitbadger.documents
/**
* The data format for a document field retrieval
*/
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

@ -0,0 +1,12 @@
package solutions.bitbadger.documents
/**
* How fields should be matched in by-field queries
*/
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

@ -0,0 +1,39 @@
package solutions.bitbadger.documents
/**
* A comparison operator used for fields
*/
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

@ -0,0 +1,58 @@
package solutions.bitbadger.documents
import java.sql.PreparedStatement
import java.sql.Types
import kotlin.jvm.Throws
/**
* A parameter to use for a query
*
* @property name The name of the parameter (prefixed with a colon)
* @property type The type of this parameter
* @property value The value of the parameter
*/
class Parameter<T>(val name: String, val type: ParameterType, val value: T) {
init {
if (!name.startsWith(':') && !name.startsWith('@'))
throw DocumentException("Name must start with : or @ ($name)")
}
/**
* Bind this parameter to a prepared statement at the given index
*
* @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 -> {
when (value) {
null -> stmt.setNull(index, Types.NULL)
is Byte -> stmt.setByte(index, value)
is Short -> stmt.setShort(index, value)
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})"
)
}
}
ParameterType.STRING -> {
when (value) {
null -> stmt.setNull(index, Types.NULL)
is String -> stmt.setString(index, value)
else -> stmt.setString(index, value.toString())
}
}
ParameterType.JSON -> stmt.setObject(index, value as String, Types.OTHER)
}
}
override fun toString() =
"$type[$name] = $value"
}

View File

@ -0,0 +1,18 @@
package solutions.bitbadger.documents
/**
* Derive parameter names; each instance wraps a counter to provide names for anonymous fields
*/
class ParameterName {
private var currentIdx = 0
/**
* Derive the parameter name from the current possibly-null string
*
* @param paramName The name of the parameter as specified by the field
* @return The name from the field, if present, or a derived name if missing
*/
fun derive(paramName: String?): String =
paramName ?: ":field${currentIdx++}"
}

View File

@ -0,0 +1,15 @@
package solutions.bitbadger.documents
/**
* The types of parameters supported by the document library
*/
enum class ParameterType {
/** The parameter value is some sort of number (`Byte`, `Short`, `Int`, or `Long`) */
NUMBER,
/** The parameter value is a string */
STRING,
/** The parameter should be JSON-encoded */
JSON,
}

View File

@ -0,0 +1,147 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.CountQuery
import java.sql.Connection
import kotlin.jvm.Throws
/**
* 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
* @throws DocumentException If any dependent process does
*/
@Throws(DocumentException::class)
@JvmStatic
fun all(tableName: String, conn: Connection) =
Custom.scalar(CountQuery.all(tableName), listOf(), Long::class.java, conn, 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
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun all(tableName: String) =
Configuration.dbConn().use { all(tableName, it) }
/**
* Count documents using a field comparison
*
* @param tableName The name of the table in which documents should be counted
* @param fields The fields which should be compared
* @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(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
conn: Connection
): Long {
val named = Parameters.nameFields(fields)
return Custom.scalar(
CountQuery.byFields(tableName, named, howMatched),
Parameters.addFields(named),
Long::class.java,
conn,
Results::toCount
)
}
/**
* Count documents using a field comparison
*
* @param tableName The name of the table in which documents should be counted
* @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) =
Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) }
/**
* Count documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which documents should be counted
* @param criteria The object for which JSON containment should be checked
* @param conn The connection on which the count should be executed
* @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) =
Custom.scalar(
CountQuery.byContains(tableName),
listOf(Parameters.json(":criteria", criteria)),
Long::class.java,
conn,
Results::toCount
)
/**
* Count documents using a JSON containment query (PostgreSQL only)
*
* @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 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) }
/**
* Count documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which documents should be counted
* @param path The JSON path comparison to match
* @param conn The connection on which the count should be executed
* @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) =
Custom.scalar(
CountQuery.byJsonPath(tableName),
listOf(Parameter(":path", ParameterType.STRING, path)),
Long::class.java,
conn,
Results::toCount
)
/**
* Count documents using a JSON Path match query (PostgreSQL only)
*
* @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 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

@ -0,0 +1,281 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.Parameter
import java.io.PrintWriter
import java.sql.Connection
import java.sql.ResultSet
import java.sql.SQLException
import java.util.*
import kotlin.jvm.Throws
/**
* Functions to run custom queries
*/
object Custom {
/**
* Execute a query that returns a list of results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param clazz The class of the document to be returned
* @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,
parameters: Collection<Parameter<*>> = listOf(),
clazz: Class<TDoc>,
conn: Connection,
mapFunc: (ResultSet, Class<TDoc>) -> TDoc
) = Parameters.apply(conn, query, parameters).use { Results.toCustomList(it, clazz, mapFunc) }
/**
* Execute a query that returns a list of results (creates connection)
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @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,
parameters: Collection<Parameter<*>> = listOf(),
clazz: Class<TDoc>,
mapFunc: (ResultSet, Class<TDoc>) -> TDoc
) = Configuration.dbConn().use { list(query, parameters, clazz, it, mapFunc) }
/**
* Execute a query that returns a JSON array of results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param conn The connection over which the query should be executed
* @param mapFunc The mapping function to extract the JSON from the query
* @return A JSON array of results for the given query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun jsonArray(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
conn: Connection,
mapFunc: (ResultSet) -> String
) = Parameters.apply(conn, query, parameters).use { Results.toJsonArray(it, mapFunc) }
/**
* Execute a query that returns a JSON array of results (creates connection)
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param mapFunc The mapping function to extract the JSON from the query
* @return A JSON array of results for the given query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun jsonArray(query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> String) =
Configuration.dbConn().use { jsonArray(query, parameters, it, mapFunc) }
/**
* Execute a query, writing its JSON array of results to the given `PrintWriter`
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param writer The writer to which the results should be written
* @param conn The connection over which the query should be executed
* @param mapFunc The mapping function to extract the JSON from the query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeJsonArray(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
writer: PrintWriter,
conn: Connection,
mapFunc: (ResultSet) -> String
) = Parameters.apply(conn, query, parameters).use { Results.writeJsonArray(writer, it, mapFunc) }
/**
* Execute a query, writing its JSON array of results to the given `PrintWriter` (creates connection)
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param writer The writer to which the results should be written
* @param mapFunc The mapping function to extract the JSON from the query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeJsonArray(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
writer: PrintWriter,
mapFunc: (ResultSet) -> String
) = Configuration.dbConn().use { writeJsonArray(query, parameters, writer, it, mapFunc) }
/**
* Execute a query that returns one or no results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param clazz The class of the document to be returned
* @param conn The connection over which the query should be executed
* @param mapFunc The mapping function between the document and the domain item
* @return An `Optional` value, with the document if one matches the query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> single(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
clazz: Class<TDoc>,
conn: Connection,
mapFunc: (ResultSet, Class<TDoc>) -> TDoc
) = Optional.ofNullable(list("$query LIMIT 1", parameters, clazz, conn, mapFunc).singleOrNull())
/**
* Execute a query that returns one or no results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @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,
parameters: Collection<Parameter<*>> = listOf(),
clazz: Class<TDoc>,
mapFunc: (ResultSet, Class<TDoc>) -> TDoc
) = Configuration.dbConn().use { single(query, parameters, clazz, it, mapFunc) }
/**
* Execute a query that returns JSON for one or no documents
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @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 JSON for the document if found, an empty object (`{}`) if not
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun jsonSingle(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
conn: Connection,
mapFunc: (ResultSet) -> String
) = jsonArray("$query LIMIT 1", parameters, conn, mapFunc).let {
if (it == "[]") "{}" else it.substring(1, it.length - 1)
}
/**
* Execute a query that returns JSON for one or no documents (creates connection)
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param mapFunc The mapping function between the document and the domain item
* @return The JSON for the document if found, an empty object (`{}`) if not
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun jsonSingle(query: String, parameters: Collection<Parameter<*>> = listOf(), mapFunc: (ResultSet) -> String) =
Configuration.dbConn().use { jsonSingle(query, parameters, it, mapFunc) }
/**
* Execute a query that returns no results
*
* @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 or if the query fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf(), conn: Connection) {
try {
Parameters.apply(conn, query, parameters).use { it.executeUpdate() }
} catch (ex: SQLException) {
throw DocumentException("Unable to execute non-query: ${ex.message}", ex)
}
}
/**
* Execute a query that returns no results
*
* @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, if parameters are invalid, or if the query fails
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun nonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) =
Configuration.dbConn().use { nonQuery(query, parameters, it) }
/**
* Execute a query that returns a scalar result
*
* @param query The query to retrieve the result
* @param parameters Parameters to use for the query
* @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 or if the query fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <T : Any> scalar(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
clazz: Class<T>,
conn: Connection,
mapFunc: (ResultSet, Class<T>) -> T
) = Parameters.apply(conn, query, parameters).use { stmt ->
try {
stmt.executeQuery().use { rs ->
rs.next()
mapFunc(rs, clazz)
}
} catch (ex: SQLException) {
throw DocumentException("Unable to retrieve scalar value: ${ex.message}", ex)
}
}
/**
* Execute a query that returns a scalar result
*
* @param query The query to retrieve the result
* @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(),
clazz: Class<T>,
mapFunc: (ResultSet, Class<T>) -> T
) = Configuration.dbConn().use { scalar(query, parameters, clazz, it, mapFunc) }
}

View File

@ -0,0 +1,92 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.DocumentIndex
import solutions.bitbadger.documents.query.DefinitionQuery
import java.sql.Connection
import kotlin.jvm.Throws
/**
* 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
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureTable(tableName: String, conn: Connection) =
Configuration.dialect("ensure $tableName exists").let {
Custom.nonQuery(DefinitionQuery.ensureTable(tableName, it), conn = conn)
Custom.nonQuery(DefinitionQuery.ensureKey(tableName, it), conn = conn)
}
/**
* 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) }
/**
* 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
* @throws DocumentException If any dependent process does
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>, conn: Connection) =
Custom.nonQuery(DefinitionQuery.ensureIndexOn(tableName, indexName, fields), conn = conn)
/**
* 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
* @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) }
/**
* Create a document index on a table (PostgreSQL only)
*
* @param tableName The table to be indexed (may include schema)
* @param indexType The type of index to ensure
* @param conn The connection on which the query should be executed
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex, conn: Connection) =
Custom.nonQuery(DefinitionQuery.ensureDocumentIndexOn(tableName, indexType), conn = conn)
/**
* Create a document index on a table (PostgreSQL only)
*
* @param tableName The table to be indexed (may include schema)
* @param indexType The type of index to ensure
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureDocumentIndex(tableName: String, indexType: DocumentIndex) =
Configuration.dbConn().use { ensureDocumentIndex(tableName, indexType, it) }
}

View File

@ -0,0 +1,122 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.DeleteQuery
import java.sql.Connection
import kotlin.jvm.Throws
/**
* Functions to delete documents
*/
object Delete {
/**
* Delete a document by its ID
*
* @param tableName The name of the table from which documents should be deleted
* @param docId The ID of the document to be deleted
* @param conn The connection on which the deletion should be executed
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> byId(tableName: String, docId: TKey, conn: Connection) =
Custom.nonQuery(
DeleteQuery.byId(tableName, docId),
Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))),
conn
)
/**
* Delete a document by its ID
*
* @param tableName The name of the table from which documents should be deleted
* @param docId The ID of the document to be deleted
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> byId(tableName: String, docId: TKey) =
Configuration.dbConn().use { byId(tableName, docId, it) }
/**
* Delete documents using a field comparison
*
* @param tableName The name of the table from which documents should be deleted
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param conn The connection on which the deletion should be executed
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null, conn: Connection) {
val named = Parameters.nameFields(fields)
Custom.nonQuery(DeleteQuery.byFields(tableName, named, howMatched), Parameters.addFields(named), conn)
}
/**
* Delete documents using a field comparison
*
* @param tableName The name of the table from which documents should be deleted
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) }
/**
* Delete documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table from which documents should be deleted
* @param criteria The object for which JSON containment should be checked
* @param conn The connection on which the deletion should be executed
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> byContains(tableName: String, criteria: TContains, conn: Connection) =
Custom.nonQuery(DeleteQuery.byContains(tableName), listOf(Parameters.json(":criteria", criteria)), conn)
/**
* Delete documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table from which documents should be deleted
* @param criteria The object for which JSON containment should be checked
* @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) }
/**
* Delete documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table from which documents should be deleted
* @param path The JSON path comparison to match
* @param conn The connection on which the deletion should be executed
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String, path: String, conn: Connection) =
Custom.nonQuery(DeleteQuery.byJsonPath(tableName), listOf(Parameter(":path", ParameterType.STRING, path)), conn)
/**
* Delete documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table from which documents should be deleted
* @param path The JSON path comparison to match
* @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

@ -0,0 +1,109 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.AutoId
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.Field
import solutions.bitbadger.documents.query.DocumentQuery
import solutions.bitbadger.documents.query.Where
import solutions.bitbadger.documents.query.statementWhere
import java.sql.Connection
import kotlin.jvm.Throws
/**
* 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
* @throws DocumentException If IDs are misconfigured, or if the database command fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> insert(tableName: String, document: TDoc, conn: Connection) {
val strategy = Configuration.autoIdStrategy
val query = if (strategy == AutoId.DISABLED || !AutoId.needsAutoId(strategy, document, Configuration.idField)) {
DocumentQuery.insert(tableName)
} else {
DocumentQuery.insert(tableName, strategy)
}
Custom.nonQuery(query, listOf(Parameters.json(":data", document)), conn)
}
/**
* 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
* @throws DocumentException If no connection string has been set; if IDs are misconfigured; or if the database
* command fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> insert(tableName: String, document: TDoc) =
Configuration.dbConn().use { insert(tableName, document, it) }
/**
* Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
*
* @param tableName The table in which the document should be saved (may include schema)
* @param document The document to be saved
* @param conn The connection on which the query should be executed
* @throws DocumentException If the database command fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> save(tableName: String, document: TDoc, conn: Connection) =
Custom.nonQuery(DocumentQuery.save(tableName), listOf(Parameters.json(":data", document)), conn)
/**
* Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
*
* @param tableName The table in which the document should be saved (may include schema)
* @param document The document to be saved
* @throws DocumentException If no connection string has been set, or if the database command fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> save(tableName: String, document: TDoc) =
Configuration.dbConn().use { save(tableName, document, it) }
/**
* Update (replace) a document by its ID
*
* @param tableName The table in which the document should be replaced (may include schema)
* @param docId The ID of the document to be replaced
* @param document The document to be replaced
* @param conn The connection on which the query should be executed
* @throws DocumentException If no dialect has been configured, or if the database command fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey, TDoc> update(tableName: String, docId: TKey, document: TDoc, conn: Connection) =
Custom.nonQuery(
statementWhere(DocumentQuery.update(tableName), Where.byId(":id", docId)),
Parameters.addFields(
listOf(Field.equal(Configuration.idField, docId, ":id")),
mutableListOf(Parameters.json(":data", document))
),
conn
)
/**
* Update (replace) a document by its ID
*
* @param tableName The table in which the document should be replaced (may include schema)
* @param docId The ID of the document to be replaced
* @param document The document to be replaced
* @throws DocumentException If no connection string has been set, or if the database command fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey, TDoc> update(tableName: String, docId: TKey, document: TDoc) =
Configuration.dbConn().use { update(tableName, docId, document, it) }
}

View File

@ -0,0 +1,15 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.DocumentSerializer
/**
* Configuration for document serialization
*/
object DocumentConfig {
/**
* The serializer to use for documents
*/
@JvmStatic
var serializer: DocumentSerializer = NullDocumentSerializer()
}

View File

@ -0,0 +1,155 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.ExistsQuery
import java.sql.Connection
import kotlin.jvm.Throws
/**
* Functions to determine whether documents exist
*/
object Exists {
/**
* Determine a document's existence by its ID
*
* @param tableName The name of the table in which document existence should be checked
* @param docId The ID of the document to be checked
* @param conn The connection on which the existence check should be executed
* @return True if the document exists, false if not
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> byId(tableName: String, docId: TKey, conn: Connection) =
Custom.scalar(
ExistsQuery.byId(tableName, docId),
Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))),
Boolean::class.java,
conn,
Results::toExists
)
/**
* Determine a document's existence by its ID
*
* @param tableName The name of the table in which document existence should be checked
* @param docId The ID of the document to be checked
* @return True if the document exists, false if not
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> byId(tableName: String, docId: TKey) =
Configuration.dbConn().use { byId(tableName, docId, it) }
/**
* Determine document existence using a field comparison
*
* @param tableName The name of the table in which document existence should be checked
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param conn The connection on which the existence check should be executed
* @return True if any matching documents exist, false if not
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
conn: Connection
): Boolean {
val named = Parameters.nameFields(fields)
return Custom.scalar(
ExistsQuery.byFields(tableName, named, howMatched),
Parameters.addFields(named),
Boolean::class.java,
conn,
Results::toExists
)
}
/**
* Determine document existence using a field comparison
*
* @param tableName The name of the table in which document existence should be checked
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @return True if any matching documents exist, false if not
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
Configuration.dbConn().use { byFields(tableName, fields, howMatched, it) }
/**
* Determine document existence using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param criteria The object for which JSON containment should be checked
* @param conn The connection on which the existence check should be executed
* @return True if any matching documents exist, false if not
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> byContains(tableName: String, criteria: TContains, conn: Connection) =
Custom.scalar(
ExistsQuery.byContains(tableName),
listOf(Parameters.json(":criteria", criteria)),
Boolean::class.java,
conn,
Results::toExists
)
/**
* Determine document existence using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param criteria The object for which JSON containment should be checked
* @return True if any matching documents exist, false if not
* @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) }
/**
* Determine document existence using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param path The JSON path comparison to match
* @param conn The connection on which the existence check should be executed
* @return True if any matching documents exist, false if not
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String, path: String, conn: Connection) =
Custom.scalar(
ExistsQuery.byJsonPath(tableName),
listOf(Parameter(":path", ParameterType.STRING, path)),
Boolean::class.java,
conn,
Results::toExists
)
/**
* Determine document existence using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param path The JSON path comparison to match
* @return True if any matching documents exist, false if not
* @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

@ -0,0 +1,502 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.FindQuery
import solutions.bitbadger.documents.query.orderBy
import java.sql.Connection
import java.util.Optional
import kotlin.jvm.Throws
/**
* Functions to find and retrieve documents
*/
object Find {
/**
* Retrieve all documents in the given table, ordering results by the optional given fields
*
* @param tableName The table from which documents should be retrieved
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return A list of documents from the given table
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> all(tableName: String, clazz: Class<TDoc>, orderBy: Collection<Field<*>>? = null, conn: Connection) =
Custom.list(FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""), listOf(), clazz, conn, Results::fromData)
/**
* Retrieve all documents in the given table
*
* @param tableName The table from which documents should be retrieved
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A list of documents from the given table
* @throws DocumentException If no connection string has been set, or if query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TDoc> all(tableName: String, clazz: Class<TDoc>, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { all(tableName, clazz, orderBy, it) }
/**
* Retrieve all documents in the given table
*
* @param tableName The table from which documents should be retrieved
* @param clazz The class of the document to be returned
* @param conn The connection over which documents should be retrieved
* @return A list of documents from the given table
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> all(tableName: String, clazz: Class<TDoc>, conn: Connection) =
all(tableName, clazz, null, conn)
/**
* Retrieve a document by its ID
*
* @param tableName The table from which the document should be retrieved
* @param docId The ID of the document to retrieve
* @param clazz The class of the document to be returned
* @param conn The connection over which documents should be retrieved
* @return An `Optional` item with the document if it is found
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey, TDoc> byId(tableName: String, docId: TKey, clazz: Class<TDoc>, conn: Connection) =
Custom.single(
FindQuery.byId(tableName, docId),
Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))),
clazz,
conn,
Results::fromData
)
/**
* Retrieve a document by its ID
*
* @param tableName The table from which the document should be retrieved
* @param docId The ID of the document to retrieve
* @param clazz The class of the document to be returned
* @return An `Optional` item with the document if it is found
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey, TDoc> byId(tableName: String, docId: TKey, clazz: Class<TDoc>) =
Configuration.dbConn().use { byId(tableName, docId, clazz, it) }
/**
* Retrieve documents using a field comparison, ordering results by the given fields
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param clazz The class of the document to be returned
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return A list of documents matching the field comparison
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> byFields(
tableName: String,
fields: Collection<Field<*>>,
clazz: Class<TDoc>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null,
conn: Connection
): List<TDoc> {
val named = Parameters.nameFields(fields)
return Custom.list(
FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""),
Parameters.addFields(named),
clazz,
conn,
Results::fromData
)
}
/**
* Retrieve documents using a field comparison, ordering results by the given fields
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param clazz The class of the document to be returned
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A list of documents matching the field comparison
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TDoc> byFields(
tableName: String,
fields: Collection<Field<*>>,
clazz: Class<TDoc>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) =
Configuration.dbConn().use { byFields(tableName, fields, clazz, howMatched, orderBy, it) }
/**
* Retrieve documents using a field comparison
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param clazz The class of the document to be returned
* @param howMatched How the fields should be matched
* @param conn The connection over which documents should be retrieved
* @return A list of documents matching the field comparison
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> byFields(
tableName: String,
fields: Collection<Field<*>>,
clazz: Class<TDoc>,
howMatched: FieldMatch? = null,
conn: Connection
) =
byFields(tableName, fields, clazz, howMatched, null, conn)
/**
* Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return A list of documents matching the JSON containment query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc, TContains> byContains(
tableName: String,
criteria: TContains,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) =
Custom.list(
FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameters.json(":criteria", criteria)),
clazz,
conn,
Results::fromData
)
/**
* Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A list of documents matching the JSON containment query
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TDoc, TContains> byContains(
tableName: String,
criteria: TContains,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null
) =
Configuration.dbConn().use { byContains(tableName, criteria, clazz, orderBy, it) }
/**
* Retrieve documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param clazz The class of the document to be returned
* @param conn The connection over which documents should be retrieved
* @return A list of documents matching the JSON containment query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc, TContains> byContains(tableName: String, criteria: TContains, clazz: Class<TDoc>, conn: Connection) =
byContains(tableName, criteria, clazz, null, conn)
/**
* Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return A list of documents matching the JSON Path match query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> byJsonPath(
tableName: String,
path: String,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) =
Custom.list(
FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameter(":path", ParameterType.STRING, path)),
clazz,
conn,
Results::fromData
)
/**
* Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A list of documents matching the JSON Path match query
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TDoc> byJsonPath(tableName: String, path: String, clazz: Class<TDoc>, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { byJsonPath(tableName, path, clazz, orderBy, it) }
/**
* Retrieve documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param clazz The class of the document to be returned
* @param conn The connection over which documents should be retrieved
* @return A list of documents matching the JSON Path match query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> byJsonPath(tableName: String, path: String, clazz: Class<TDoc>, conn: Connection) =
byJsonPath(tableName, path, clazz, null, conn)
/**
* Retrieve the first document using a field comparison and optional ordering fields
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param clazz The class of the document to be returned
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return An `Optional` item, with the first document matching the field comparison if found
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> firstByFields(
tableName: String,
fields: Collection<Field<*>>,
clazz: Class<TDoc>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null,
conn: Connection
): Optional<TDoc & Any> {
val named = Parameters.nameFields(fields)
return Custom.single(
FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""),
Parameters.addFields(named),
clazz,
conn,
Results::fromData
)
}
/**
* Retrieve the first document using a field comparison and optional ordering fields
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param clazz The class of the document to be returned
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return An `Optional` item, with the first document matching the field comparison if found
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TDoc> firstByFields(
tableName: String,
fields: Collection<Field<*>>,
clazz: Class<TDoc>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) =
Configuration.dbConn().use { firstByFields(tableName, fields, clazz, howMatched, orderBy, it) }
/**
* Retrieve the first document using a field comparison
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param clazz The class of the document to be returned
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param conn The connection over which documents should be retrieved
* @return An `Optional` item, with the first document matching the field comparison if found
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> firstByFields(
tableName: String,
fields: Collection<Field<*>>,
clazz: Class<TDoc>,
howMatched: FieldMatch? = null,
conn: Connection
) =
firstByFields(tableName, fields, clazz, howMatched, null, conn)
/**
* Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return An `Optional` item, with the first document matching the JSON containment query if found
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc, TContains> firstByContains(
tableName: String,
criteria: TContains,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) =
Custom.single(
FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameters.json(":criteria", criteria)),
clazz,
conn,
Results::fromData
)
/**
* Retrieve the first document using a JSON containment query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param clazz The class of the document to be returned
* @param conn The connection over which documents should be retrieved
* @return An `Optional` item, with the first document matching the JSON containment query if found
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc, TContains> firstByContains(
tableName: String,
criteria: TContains,
clazz: Class<TDoc>,
conn: Connection
) =
firstByContains(tableName, criteria, clazz, null, conn)
/**
* Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return An `Optional` item, with the first document matching the JSON containment query if found
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TDoc, TContains> firstByContains(
tableName: String,
criteria: TContains,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null
) =
Configuration.dbConn().use { firstByContains(tableName, criteria, clazz, orderBy, it) }
/**
* Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return An `Optional` item, with the first document matching the JSON Path match query if found
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> firstByJsonPath(
tableName: String,
path: String,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) =
Custom.single(
FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameter(":path", ParameterType.STRING, path)),
clazz,
conn,
Results::fromData
)
/**
* Retrieve the first document using a JSON Path match query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param clazz The class of the document to be returned
* @param conn The connection over which documents should be retrieved
* @return An `Optional` item, with the first document matching the JSON Path match query if found
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TDoc> firstByJsonPath(tableName: String, path: String, clazz: Class<TDoc>, conn: Connection) =
firstByJsonPath(tableName, path, clazz, null, conn)
/**
* Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return An `Optional` item, with the first document matching the JSON Path match query if found
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TDoc> firstByJsonPath(
tableName: String,
path: String,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null
) =
Configuration.dbConn().use { firstByJsonPath(tableName, path, clazz, orderBy, it) }
}

View File

@ -0,0 +1,877 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.FindQuery
import solutions.bitbadger.documents.query.orderBy
import java.io.PrintWriter
import java.sql.Connection
import kotlin.jvm.Throws
/**
* Functions to find and retrieve documents, returning them as JSON strings
*/
object Json {
/**
* Retrieve all documents in the given table, ordering results by the optional given fields
*
* @param tableName The table from which documents should be retrieved
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return A JSON array of documents from the given table
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun all(tableName: String, orderBy: Collection<Field<*>>? = null, conn: Connection) =
Custom.jsonArray(
FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(),
conn,
Results::jsonFromData
)
/**
* Retrieve all documents in the given table (creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A JSON array of documents from the given table
* @throws DocumentException If no connection string has been set, or if query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun all(tableName: String, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { all(tableName, orderBy, it) }
/**
* 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 JSON array of documents from the given table
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun all(tableName: String, conn: Connection) =
all(tableName, null, conn)
/**
* Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeAll(tableName: String, writer: PrintWriter, orderBy: Collection<Field<*>>? = null, conn: Connection) =
Custom.writeJsonArray(
FindQuery.all(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(),
writer,
conn,
Results::jsonFromData
)
/**
* Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields
* (creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun writeAll(tableName: String, writer: PrintWriter, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { writeAll(tableName, writer, orderBy, it) }
/**
* Write all documents in the given table to the given `PrintWriter`
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeAll(tableName: String, writer: PrintWriter, conn: Connection) =
writeAll(tableName, writer, null, conn)
/**
* Retrieve a document by its ID
*
* @param tableName The table from which the document should be retrieved
* @param docId The ID of the document to retrieve
* @param conn The connection over which documents should be retrieved
* @return A JSON document if found, an empty JSON object if not found
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> byId(tableName: String, docId: TKey, conn: Connection) =
Custom.jsonSingle(
FindQuery.byId(tableName, docId),
Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id"))),
conn,
Results::jsonFromData
)
/**
* Retrieve a document by its ID (creates connection)
*
* @param tableName The table from which the document should be retrieved
* @param docId The ID of the document to retrieve
* @return A JSON document if found, an empty JSON object if not found
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> byId(tableName: String, docId: TKey) =
Configuration.dbConn().use { byId(tableName, docId, it) }
/**
* Write a document to the given `PrintWriter` by its ID (writes empty object if not found)
*
* @param tableName The table from which the document should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param docId The ID of the document to retrieve
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> writeById(tableName: String, writer: PrintWriter, docId: TKey, conn: Connection) =
writer.write(byId(tableName, docId, conn))
/**
* Write a document to the given `PrintWriter` by its ID (writes empty object if not found; creates connection)
*
* @param tableName The table from which the document should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param docId The ID of the document to retrieve
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> writeById(tableName: String, writer: PrintWriter, docId: TKey) =
Configuration.dbConn().use { writeById(tableName, writer, docId, it) }
/**
* Retrieve documents using a field comparison, ordering results by the given fields
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return A JSON array of documents matching the field comparison
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun byFields(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null,
conn: Connection
): String {
val named = Parameters.nameFields(fields)
return Custom.jsonArray(
FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""),
Parameters.addFields(named),
conn,
Results::jsonFromData
)
}
/**
* Retrieve documents using a field comparison, ordering results by the given fields (creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A JSON array of documents matching the field comparison
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Configuration.dbConn().use { byFields(tableName, fields, howMatched, orderBy, it) }
/**
* Retrieve documents using a field comparison
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param conn The connection over which documents should be retrieved
* @return A JSON array of documents matching the field comparison
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null, conn: Connection) =
byFields(tableName, fields, howMatched, null, conn)
/**
* Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeByFields(
tableName: String,
writer: PrintWriter,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) {
val named = Parameters.nameFields(fields)
Custom.writeJsonArray(
FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""),
Parameters.addFields(named),
writer,
conn,
Results::jsonFromData
)
}
/**
* Write documents to the given `PrintWriter` using a field comparison, ordering results by the given fields
* (creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun writeByFields(
tableName: String,
writer: PrintWriter,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Configuration.dbConn().use { writeByFields(tableName, writer, fields, howMatched, orderBy, it) }
/**
* Write documents to the given `PrintWriter` using a field comparison
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeByFields(
tableName: String,
writer: PrintWriter,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
conn: Connection
) = writeByFields(tableName, writer, fields, howMatched, null, conn)
/**
* Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return A JSON array of documents matching the JSON containment query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> byContains(
tableName: String,
criteria: TContains,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) = Custom.jsonArray(
FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameters.json(":criteria", criteria)),
conn,
Results::jsonFromData
)
/**
* Retrieve documents using a JSON containment query, ordering results by the given fields (PostgreSQL only; creates
* connection)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A JSON array of documents matching the JSON containment query
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TContains> byContains(tableName: String, criteria: TContains, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { byContains(tableName, criteria, orderBy, it) }
/**
* Retrieve documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param conn The connection over which documents should be retrieved
* @return A JSON array of documents matching the JSON containment query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> byContains(tableName: String, criteria: TContains, conn: Connection) =
byContains(tableName, criteria, null, conn)
/**
* Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields
* (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> writeByContains(
tableName: String,
writer: PrintWriter,
criteria: TContains,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) = Custom.writeJsonArray(
FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameters.json(":criteria", criteria)),
writer,
conn,
Results::jsonFromData
)
/**
* Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the given fields
* (PostgreSQL only; creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TContains> writeByContains(
tableName: String,
writer: PrintWriter,
criteria: TContains,
orderBy: Collection<Field<*>>? = null
) = Configuration.dbConn().use { writeByContains(tableName, writer, criteria, orderBy, it) }
/**
* Write documents to the given `PrintWriter` using a JSON containment query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param criteria The object for which JSON containment should be checked
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> writeByContains(tableName: String, writer: PrintWriter, criteria: TContains, conn: Connection) =
writeByContains(tableName, writer, criteria, null, conn)
/**
* Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return A JSON array of documents matching the JSON Path match query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String, path: String, orderBy: Collection<Field<*>>? = null, conn: Connection) =
Custom.jsonArray(
FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameter(":path", ParameterType.STRING, path)),
conn,
Results::jsonFromData
)
/**
* Retrieve documents using a JSON Path match query, ordering results by the given fields (PostgreSQL only; creates
* connection)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A JSON array of documents matching the JSON Path match query
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byJsonPath(tableName: String, path: String, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { byJsonPath(tableName, path, orderBy, it) }
/**
* Retrieve documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param conn The connection over which documents should be retrieved
* @return A JSON array of documents matching the JSON Path match query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String, path: String, conn: Connection) =
byJsonPath(tableName, path, null, conn)
/**
* Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields
* (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeByJsonPath(
tableName: String,
writer: PrintWriter,
path: String,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) = Custom.writeJsonArray(
FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameter(":path", ParameterType.STRING, path)),
writer,
conn,
Results::jsonFromData
)
/**
* Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the given fields
* (PostgreSQL only; creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun writeByJsonPath(tableName: String, writer: PrintWriter, path: String, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { writeByJsonPath(tableName, writer, path, orderBy, it) }
/**
* Write documents to the given `PrintWriter` using a JSON Path match query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param path The JSON path comparison to match
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeByJsonPath(tableName: String, writer: PrintWriter, path: String, conn: Connection) =
writeByJsonPath(tableName, writer, path, null, conn)
/**
* Retrieve the first document using a field comparison and optional ordering fields
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return The first JSON document matching the field comparison if found, an empty JSON object otherwise
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun firstByFields(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null,
conn: Connection
): String {
val named = Parameters.nameFields(fields)
return Custom.jsonSingle(
FindQuery.byFields(tableName, named, howMatched) + (orderBy?.let(::orderBy) ?: ""),
Parameters.addFields(named),
conn,
Results::jsonFromData
)
}
/**
* Retrieve the first document using a field comparison and optional ordering fields (creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return The first JSON document matching the field comparison if found, an empty JSON object otherwise
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun firstByFields(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Configuration.dbConn().use { firstByFields(tableName, fields, howMatched, orderBy, it) }
/**
* Retrieve the first document using a field comparison
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param conn The connection over which documents should be retrieved
* @return The first JSON document matching the field comparison if found, an empty JSON object otherwise
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun firstByFields(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
conn: Connection
) = firstByFields(tableName, fields, howMatched, null, conn)
/**
* Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeFirstByFields(
tableName: String,
writer: PrintWriter,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) = writer.write(firstByFields(tableName, fields, howMatched, orderBy, conn))
/**
* Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields
* (creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun writeFirstByFields(
tableName: String,
writer: PrintWriter,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Configuration.dbConn().use { writeFirstByFields(tableName, writer, fields, howMatched, orderBy, it) }
/**
* Write the first document to the given `PrintWriter` using a field comparison
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeFirstByFields(
tableName: String,
writer: PrintWriter,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
conn: Connection
) = writeFirstByFields(tableName, writer, fields, howMatched, null, conn)
/**
* Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> firstByContains(
tableName: String,
criteria: TContains,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) = Custom.jsonSingle(
FindQuery.byContains(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameters.json(":criteria", criteria)),
conn,
Results::jsonFromData
)
/**
* Retrieve the first document using a JSON containment query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param conn The connection over which documents should be retrieved
* @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> firstByContains(tableName: String, criteria: TContains, conn: Connection) =
firstByContains(tableName, criteria, null, conn)
/**
* Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only; creates
* connection)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TContains> firstByContains(tableName: String, criteria: TContains, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { firstByContains(tableName, criteria, orderBy, it) }
/**
* Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields
* (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> writeFirstByContains(
tableName: String,
writer: PrintWriter,
criteria: TContains,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) = writer.write(firstByContains(tableName, criteria, orderBy, conn))
/**
* Write the first document to the given `PrintWriter` using a JSON containment query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param criteria The object for which JSON containment should be checked
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> writeFirstByContains(
tableName: String,
writer: PrintWriter,
criteria: TContains,
conn: Connection
) = writeFirstByContains(tableName, writer, criteria, null, conn)
/**
* Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields
* (PostgreSQL only; creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TContains> writeFirstByContains(
tableName: String,
writer: PrintWriter,
criteria: TContains,
orderBy: Collection<Field<*>>? = null
) = Configuration.dbConn().use { writeFirstByContains(tableName, writer, criteria, orderBy, it) }
/**
* Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun firstByJsonPath(tableName: String, path: String, orderBy: Collection<Field<*>>? = null, conn: Connection) =
Custom.jsonSingle(
FindQuery.byJsonPath(tableName) + (orderBy?.let(::orderBy) ?: ""),
listOf(Parameter(":path", ParameterType.STRING, path)),
conn,
Results::jsonFromData
)
/**
* Retrieve the first document using a JSON Path match query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param conn The connection over which documents should be retrieved
* @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun firstByJsonPath(tableName: String, path: String, conn: Connection) =
firstByJsonPath(tableName, path, null, conn)
/**
* Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only; creates
* connection)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun firstByJsonPath(tableName: String, path: String, orderBy: Collection<Field<*>>? = null) =
Configuration.dbConn().use { firstByJsonPath(tableName, path, orderBy, it) }
/**
* Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields
* (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeFirstByJsonPath(
tableName: String,
writer: PrintWriter,
path: String,
orderBy: Collection<Field<*>>? = null,
conn: Connection
) = writer.write(firstByJsonPath(tableName, path, orderBy, conn))
/**
* Write the first document to the given `PrintWriter` using a JSON Path match query (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param path The JSON path comparison to match
* @param conn The connection over which documents should be retrieved
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun writeFirstByJsonPath(tableName: String, writer: PrintWriter, path: String, conn: Connection) =
writeFirstByJsonPath(tableName, writer, path, null, conn)
/**
* Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields
* (PostgreSQL only; creates connection)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun writeFirstByJsonPath(
tableName: String,
writer: PrintWriter,
path: String,
orderBy: Collection<Field<*>>? = null
) = Configuration.dbConn().use { writeFirstByJsonPath(tableName, writer, path, orderBy, it) }
}

View File

@ -0,0 +1,21 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.DocumentSerializer
/**
* A serializer that tells the user to implement another one
*
* This is the default serializer, so the library itself does not have any firm dependency on any JSON serialization
* library. The tests for this library (will) have an example Jackson-based serializer.
*/
class NullDocumentSerializer : DocumentSerializer {
override fun <TDoc> serialize(document: TDoc): String {
TODO("Replace this serializer in DocumentConfig.serializer")
}
override fun <TDoc> deserialize(json: String, clazz: Class<TDoc>): TDoc {
TODO("Replace this serializer in DocumentConfig.serializer")
}
}

View File

@ -0,0 +1,131 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.ParameterName
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.SQLException
import kotlin.jvm.Throws
/**
* Functions to assist with the creation and implementation of parameters for SQL queries
*
* @author Daniel J. Summers <daniel@bitbadger.solutions>
*/
object Parameters {
/**
* Assign parameter names to any fields that do not have them assigned
*
* @param fields The collection of fields to be named
* @return The collection of fields with parameter names assigned
*/
@JvmStatic
fun nameFields(fields: Collection<Field<*>>): Collection<Field<*>> {
val name = ParameterName()
return fields.map {
if (it.parameterName.isNullOrEmpty() && !listOf(Op.EXISTS, Op.NOT_EXISTS).contains(it.comparison.op)) {
it.withParameterName(name.derive(null))
} else {
it
}
}
}
/**
* 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
*/
@JvmStatic
fun <T> json(name: String, value: T) =
Parameter(name, ParameterType.JSON, DocumentConfig.serializer.serialize(value))
/**
* Add field parameters to the given set of parameters
*
* @param fields The fields being compared in the query
* @param existing Any existing parameters for the query (optional, defaults to empty collection)
* @return A collection of parameters for the query
*/
@JvmStatic
fun addFields(fields: Collection<Field<*>>, existing: MutableCollection<Parameter<*>> = mutableListOf()) =
fields.fold(existing) { acc, field -> field.appendParameter(acc) }
/**
* Replace the parameter names in the query with question marks
*
* @param query The query with named placeholders
* @param parameters The parameters for the query
* @return The query, with name parameters changed to `?`s
*/
@JvmStatic
fun replaceNamesInQuery(query: String, parameters: Collection<Parameter<*>>) =
parameters.sortedByDescending { it.name.length }.fold(query) { acc, param -> acc.replace(param.name, "?") }
/**
* Apply the given parameters to the given query, returning a prepared statement
*
* @param conn The active JDBC connection
* @param query The query
* @param parameters The parameters for the query
* @return A `PreparedStatement` with the parameter names replaced with `?` and parameter values bound
* @throws DocumentException If parameter names are invalid or number value types are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun apply(conn: Connection, query: String, parameters: Collection<Parameter<*>>): PreparedStatement {
if (parameters.isEmpty()) return try {
conn.prepareStatement(query)
} catch (ex: SQLException) {
throw DocumentException("Error preparing no-parameter query: ${ex.message}", ex)
}
val replacements = mutableListOf<Pair<Int, Parameter<*>>>()
parameters.sortedByDescending { it.name.length }.forEach {
var startPos = query.indexOf(it.name)
while (startPos > -1) {
replacements.add(Pair(startPos, it))
startPos = query.indexOf(it.name, startPos + it.name.length + 1)
}
}
return try {
replaceNamesInQuery(query, parameters)
//.also(::println)
.let { conn.prepareStatement(it) }
.also { stmt ->
replacements.sortedBy { it.first }
.map { it.second }
.forEachIndexed { index, param -> param.bind(stmt, index + 1) }
}
} catch (ex: SQLException) {
throw DocumentException("Error creating query / binding parameters: ${ex.message}", ex)
}
}
/**
* Create parameters for field names to be removed from a document
*
* @param names The names of the fields to be removed
* @param parameterName The parameter name to use for the query
* @return A list of parameters to use for building the query
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun fieldNames(names: Collection<String>, parameterName: String = ":name"): MutableCollection<Parameter<*>> =
when (Configuration.dialect("generate field name parameters")) {
Dialect.POSTGRESQL -> mutableListOf(
Parameter(parameterName, ParameterType.STRING, names.joinToString(",").let { "{$it}" })
)
Dialect.SQLITE -> names.mapIndexed { index, name ->
Parameter("$parameterName$index", ParameterType.STRING, name)
}.toMutableList()
}
}

View File

@ -0,0 +1,155 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.PatchQuery
import java.sql.Connection
import kotlin.jvm.Throws
/**
* 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
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey, TPatch> byId(tableName: String, docId: TKey, patch: TPatch, conn: Connection) =
Custom.nonQuery(
PatchQuery.byId(tableName, docId),
Parameters.addFields(
listOf(Field.equal(Configuration.idField, docId, ":id")),
mutableListOf(Parameters.json(":data", patch))
),
conn
)
/**
* 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
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey, 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
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TPatch> byFields(
tableName: String,
fields: Collection<Field<*>>,
patch: TPatch,
howMatched: FieldMatch? = null,
conn: Connection
) {
val named = Parameters.nameFields(fields)
Custom.nonQuery(
PatchQuery.byFields(tableName, named, howMatched),
Parameters.addFields(named, mutableListOf(Parameters.json(":data", patch))),
conn
)
}
/**
* 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
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <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
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains, TPatch> byContains(tableName: String, criteria: TContains, patch: TPatch, conn: Connection) =
Custom.nonQuery(
PatchQuery.byContains(tableName),
listOf(Parameters.json(":criteria", criteria), Parameters.json(":data", patch)),
conn
)
/**
* 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 no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains, 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
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TPatch> byJsonPath(tableName: String, path: String, patch: TPatch, conn: Connection) =
Custom.nonQuery(
PatchQuery.byJsonPath(tableName),
listOf(Parameter(":path", ParameterType.STRING, path), Parameters.json(":data", patch)),
conn
)
/**
* 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 no connection string has been set, or if called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TPatch> byJsonPath(tableName: String, path: String, patch: TPatch) =
Configuration.dbConn().use { byJsonPath(tableName, path, patch, it) }
}

View File

@ -0,0 +1,178 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.query.RemoveFieldsQuery
import java.sql.Connection
import kotlin.jvm.Throws
/**
* Functions to remove fields from documents
*/
object RemoveFields {
/**
* Translate field paths to JSON paths for SQLite queries
*
* @param parameters The parameters for the specified fields
* @return The parameters for the specified fields, translated if used for SQLite
*/
private fun translatePath(parameters: MutableCollection<Parameter<*>>): MutableCollection<Parameter<*>> {
val dialect = Configuration.dialect("remove fields")
return when (dialect) {
Dialect.POSTGRESQL -> parameters
Dialect.SQLITE -> parameters.map { Parameter(it.name, it.type, "$.${it.value}") }.toMutableList()
}
}
/**
* Remove fields from a document by its ID
*
* @param tableName The name of the table in which the document's fields should be removed
* @param docId The ID of the document to have fields removed
* @param toRemove The names of the fields to be removed
* @param conn The connection on which the update should be executed
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> byId(tableName: String, docId: TKey, toRemove: Collection<String>, conn: Connection) {
val nameParams = Parameters.fieldNames(toRemove)
Custom.nonQuery(
RemoveFieldsQuery.byId(tableName, nameParams, docId),
Parameters.addFields(listOf(Field.equal(Configuration.idField, docId, ":id")), translatePath(nameParams)),
conn
)
}
/**
* Remove fields from a document by its ID
*
* @param tableName The name of the table in which the document's fields should be removed
* @param docId The ID of the document to have fields removed
* @param toRemove The names of the fields to be removed
* @throws DocumentException If no connection string has been set
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TKey> byId(tableName: String, docId: TKey, toRemove: Collection<String>) =
Configuration.dbConn().use { byId(tableName, docId, toRemove, it) }
/**
* Remove fields from documents using a field comparison
*
* @param tableName The name of the table in which document fields should be removed
* @param fields The fields which should be compared
* @param toRemove The names of the fields to be removed
* @param howMatched How the fields should be matched
* @param conn The connection on which the update should be executed
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
fun byFields(
tableName: String,
fields: Collection<Field<*>>,
toRemove: Collection<String>,
howMatched: FieldMatch? = null,
conn: Connection
) {
val named = Parameters.nameFields(fields)
val nameParams = Parameters.fieldNames(toRemove)
Custom.nonQuery(
RemoveFieldsQuery.byFields(tableName, nameParams, named, howMatched),
Parameters.addFields(named, translatePath(nameParams)),
conn
)
}
/**
* Remove fields from documents using a field comparison
*
* @param tableName The name of the table in which document fields should be removed
* @param fields The fields which should be compared
* @param toRemove The names of the fields to be removed
* @param howMatched How the fields should be matched
* @throws DocumentException If no connection string has been set, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(
tableName: String,
fields: Collection<Field<*>>,
toRemove: Collection<String>,
howMatched: FieldMatch? = null
) =
Configuration.dbConn().use { byFields(tableName, fields, toRemove, howMatched, it) }
/**
* Remove fields from documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which document fields should be removed
* @param criteria The object against which JSON containment should be checked
* @param toRemove The names of the fields to be removed
* @param conn The connection on which the update should be executed
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun <TContains> byContains(
tableName: String,
criteria: TContains,
toRemove: Collection<String>,
conn: Connection
) {
val nameParams = Parameters.fieldNames(toRemove)
Custom.nonQuery(
RemoveFieldsQuery.byContains(tableName, nameParams),
listOf(Parameters.json(":criteria", criteria), *nameParams.toTypedArray()),
conn
)
}
/**
* Remove fields from documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which document fields should be removed
* @param criteria The object against which JSON containment should be checked
* @param toRemove The names of the fields to be removed
* @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, toRemove: Collection<String>) =
Configuration.dbConn().use { byContains(tableName, criteria, toRemove, it) }
/**
* Remove fields from documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which document fields should be removed
* @param path The JSON path comparison to match
* @param toRemove The names of the fields to be removed
* @param conn The connection on which the update should be executed
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String, path: String, toRemove: Collection<String>, conn: Connection) {
val nameParams = Parameters.fieldNames(toRemove)
Custom.nonQuery(
RemoveFieldsQuery.byJsonPath(tableName, nameParams),
listOf(Parameter(":path", ParameterType.STRING, path), *nameParams.toTypedArray()),
conn
)
}
/**
* Remove fields from documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which document fields should be removed
* @param path The JSON path comparison to match
* @param toRemove The names of the fields to be removed
* @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, toRemove: Collection<String>) =
Configuration.dbConn().use { byJsonPath(tableName, path, toRemove, it) }
}

View File

@ -0,0 +1,167 @@
package solutions.bitbadger.documents.java
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.Dialect
import solutions.bitbadger.documents.DocumentException
import java.io.PrintWriter
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException
/**
* Functions to create results from queries
*/
object Results {
/**
* Create a domain item from a document, specifying the field in which the document is found
*
* @param field The field name containing the JSON document
* @param rs A `ResultSet` set to the row with the document to be constructed
* @param clazz The class of the document to be returned
* @return The constructed domain item
*/
@JvmStatic
fun <TDoc> fromDocument(field: String, rs: ResultSet, clazz: Class<TDoc>) =
DocumentConfig.serializer.deserialize(rs.getString(field), clazz)
/**
* Create a domain item from a document
*
* @param rs A `ResultSet` set to the row with the document to be constructed<
* @param clazz The class of the document to be returned
* @return The constructed domain item
*/
@JvmStatic
fun <TDoc> fromData(rs: ResultSet, clazz: Class<TDoc>) =
fromDocument("data", rs, clazz)
/**
* Create a list of items for the results of the given command, using the specified mapping function
*
* @param stmt The prepared statement to execute
* @param mapFunc The mapping function from data reader to domain class instance
* @param clazz The class of the document to be returned
* @return A list of items from the query's result
* @throws DocumentException If there is a problem executing the query (unchecked)
*/
@JvmStatic
fun <TDoc> toCustomList(
stmt: PreparedStatement, clazz: Class<TDoc>, mapFunc: (ResultSet, Class<TDoc>) -> TDoc
) =
try {
stmt.executeQuery().use {
val results = mutableListOf<TDoc>()
while (it.next()) {
results.add(mapFunc(it, clazz))
}
results.toList()
}
} catch (ex: SQLException) {
throw DocumentException("Error retrieving documents from query: ${ex.message}", ex)
}
/**
* Extract a count from the first column
*
* @param rs A `ResultSet` set to the row with the count to retrieve
* @param clazz The type parameter (ignored; this always returns `Long`)
* @return The count from the row
* @throws DocumentException If the dialect has not been set (unchecked)
*/
@JvmStatic
fun toCount(rs: ResultSet, clazz: Class<*>) =
when (Configuration.dialect()) {
Dialect.POSTGRESQL -> rs.getInt("it").toLong()
Dialect.SQLITE -> rs.getLong("it")
}
/**
* Extract a true/false value from the first column
*
* @param rs A `ResultSet` set to the row with the true/false value to retrieve
* @param clazz The type parameter (ignored; this always returns `Boolean`)
* @return The true/false value from the row
* @throws DocumentException If the dialect has not been set (unchecked)
*/
@JvmStatic
fun toExists(rs: ResultSet, clazz: Class<*>) =
when (Configuration.dialect()) {
Dialect.POSTGRESQL -> rs.getBoolean("it")
Dialect.SQLITE -> toCount(rs, Long::class.java) > 0L
}
/**
* Retrieve the JSON text of a document, specifying the field in which the document is found
*
* @param field The field name containing the JSON document
* @param rs A `ResultSet` set to the row with the document to be constructed
* @return The JSON text of the document
*/
@JvmStatic
fun jsonFromDocument(field: String, rs: ResultSet) =
rs.getString(field) ?: "{}"
/**
* Retrieve the JSON text of a document, specifying the field in which the document is found
*
* @param rs A `ResultSet` set to the row with the document to be constructed
* @return The JSON text of the document
*/
@JvmStatic
fun jsonFromData(rs: ResultSet) =
jsonFromDocument("data", rs)
/**
* Create a JSON array of items for the results of the given command, using the specified mapping function
*
* @param stmt The prepared statement to execute
* @param mapFunc The mapping function from data reader to JSON text
* @return A string with a JSON array of documents from the query's result
* @throws DocumentException If there is a problem executing the query (unchecked)
*/
@JvmStatic
fun toJsonArray(stmt: PreparedStatement, mapFunc: (ResultSet) -> String): String =
try {
val results = StringBuilder("[")
stmt.executeQuery().use {
while (it.next()) {
if (results.length > 2) results.append(",")
results.append(mapFunc(it))
}
}
results.append("]").toString()
} catch (ex: SQLException) {
throw DocumentException("Error retrieving documents from query: ${ex.message}", ex)
}
/**
* Write a JSON array of items for the results of the given command to the given `PrintWriter`, using the specified
* mapping function
*
* @param writer The writer for the results of the query
* @param stmt The prepared statement to execute
* @param mapFunc The mapping function from data reader to JSON text
* @return A string with a JSON array of documents from the query's result
* @throws DocumentException If there is a problem executing the query (unchecked)
*/
@JvmStatic
fun writeJsonArray(writer: PrintWriter, stmt: PreparedStatement, mapFunc: (ResultSet) -> String) =
try {
writer.write("[")
stmt.executeQuery().use {
var isFirst = true
while (it.next()) {
if (isFirst) {
isFirst = false
} else {
writer.write(",")
}
writer.write(mapFunc(it))
}
}
writer.write("]")
} catch (ex: SQLException) {
throw DocumentException("Error writing documents from query: ${ex.message}", ex)
}
}

View File

@ -0,0 +1,885 @@
@file:JvmName("ConnExt")
package solutions.bitbadger.documents.java.extensions
import solutions.bitbadger.documents.*
import solutions.bitbadger.documents.java.*
import java.io.PrintWriter
import java.sql.Connection
import java.sql.ResultSet
import kotlin.jvm.Throws
// ~~~ CUSTOM QUERIES ~~~
/**
* Execute a query that returns a list of results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @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(),
clazz: Class<TDoc>,
mapFunc: (ResultSet, Class<TDoc>) -> TDoc
) = Custom.list(query, parameters, clazz, this, mapFunc)
/**
* Execute a query that returns a JSON array of results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param mapFunc The mapping function to extract the JSON from the query
* @return A JSON array of results for the given query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
fun Connection.customJsonArray(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
mapFunc: (ResultSet) -> String
) = Custom.jsonArray(query, parameters, this, mapFunc)
/**
* Execute a query, writing its JSON array of results to the given `PrintWriter` (creates connection)
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param writer The writer to which the results should be written
* @param mapFunc The mapping function to extract the JSON from the query
* @return A JSON array of results for the given query
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
fun Connection.writeCustomJsonArray(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
writer: PrintWriter,
mapFunc: (ResultSet) -> String
) = Custom.writeJsonArray(query, parameters, writer, this, mapFunc)
/**
* Execute a query that returns one or no results
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @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(),
clazz: Class<TDoc>,
mapFunc: (ResultSet, Class<TDoc>) -> TDoc
) = Custom.single(query, parameters, clazz, this, mapFunc)
/**
* Execute a query that returns JSON for one or no documents
*
* @param query The query to retrieve the results
* @param parameters Parameters to use for the query
* @param mapFunc The mapping function between the document and the domain item
* @return The JSON for the document if found, an empty object (`{}`) if not
* @throws DocumentException If parameters are invalid
*/
@Throws(DocumentException::class)
fun Connection.customJsonSingle(
query: String,
parameters: Collection<Parameter<*>> = listOf(),
mapFunc: (ResultSet) -> String
) = Custom.jsonSingle(query, parameters, this, mapFunc)
/**
* Execute a query that returns no results
*
* @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)
@JvmOverloads
fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>> = listOf()) =
Custom.nonQuery(query, parameters, this)
/**
* Execute a query that returns a scalar result
*
* @param query The query to retrieve the result
* @param parameters Parameters to use for the query
* @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(),
clazz: Class<T>,
mapFunc: (ResultSet, Class<T>) -> T
) = Custom.scalar(query, parameters, clazz, this, mapFunc)
// ~~~ DEFINITION QUERIES ~~~
/**
* Create a document table if necessary
*
* @param tableName The table whose existence should be ensured (may include schema)
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
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<
* @throws DocumentException If any dependent process does
*/
@Throws(DocumentException::class)
fun Connection.ensureFieldIndex(tableName: String, indexName: String, fields: Collection<String>) =
Definition.ensureFieldIndex(tableName, indexName, fields, this)
/**
* Create a document index on a table (PostgreSQL only)
*
* @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::class)
fun Connection.ensureDocumentIndex(tableName: String, indexType: DocumentIndex) =
Definition.ensureDocumentIndex(tableName, indexType, 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
* @throws DocumentException If IDs are misconfigured, or if the database command fails
*/
@Throws(DocumentException::class)
fun <TDoc> Connection.insert(tableName: String, document: TDoc) =
Document.insert(tableName, document, this)
/**
* Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
*
* @param tableName The table in which the document should be saved (may include schema)
* @param document The document to be saved
* @throws DocumentException If the database command fails
*/
@Throws(DocumentException::class)
fun <TDoc> Connection.save(tableName: String, document: TDoc) =
Document.save(tableName, document, this)
/**
* Update (replace) a document by its ID
*
* @param tableName The table in which the document should be replaced (may include schema)
* @param docId The ID of the document to be replaced
* @param document The document to be replaced
* @throws DocumentException If no dialect has been configured, or if the database command fails
*/
@Throws(DocumentException::class)
fun <TKey, TDoc> Connection.update(tableName: String, docId: TKey, document: TDoc) =
Document.update(tableName, docId, 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
* @throws DocumentException If any dependent process does
*/
@Throws(DocumentException::class)
fun Connection.countAll(tableName: String) =
Count.all(tableName, this)
/**
* Count documents using a field comparison
*
* @param tableName The name of the table in which documents should be counted
* @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)
/**
* Count documents using a JSON containment query (PostgreSQL only)
*
* @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::class)
fun <TContains> Connection.countByContains(tableName: String, criteria: TContains) =
Count.byContains(tableName, criteria, this)
/**
* Count documents using a JSON Path match query (PostgreSQL only)
*
* @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::class)
fun Connection.countByJsonPath(tableName: String, path: String) =
Count.byJsonPath(tableName, path, this)
// ~~~ DOCUMENT EXISTENCE QUERIES ~~~
/**
* Determine a document's existence by its ID
*
* @param tableName The name of the table in which document existence should be checked
* @param docId The ID of the document to be checked
* @return True if the document exists, false if not
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
fun <TKey> Connection.existsById(tableName: String, docId: TKey) =
Exists.byId(tableName, docId, this)
/**
* Determine document existence using a field comparison
*
* @param tableName The name of the table in which document existence should be checked
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @return True if any matching documents exist, false if not
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.existsByFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
Exists.byFields(tableName, fields, howMatched, this)
/**
* Determine document existence using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param criteria The object for which JSON containment should be checked
* @return True if any matching documents exist, false if not
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
fun <TContains> Connection.existsByContains(tableName: String, criteria: TContains) =
Exists.byContains(tableName, criteria, this)
/**
* Determine document existence using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param path The JSON path comparison to match
* @return True if any matching documents exist, false if not
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
fun Connection.existsByJsonPath(tableName: String, path: String) =
Exists.byJsonPath(tableName, path, this)
// ~~~ DOCUMENT RETRIEVAL QUERIES (Domain Objects) ~~~
/**
* Retrieve all documents in the given table, ordering results by the optional given fields
*
* @param tableName The table from which documents should be retrieved
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A list of documents from the given table
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TDoc> Connection.findAll(tableName: String, clazz: Class<TDoc>, orderBy: Collection<Field<*>>? = null) =
Find.all(tableName, clazz, orderBy, this)
/**
* Retrieve a document by its ID
*
* @param tableName The table from which the document should be retrieved
* @param docId The ID of the document to retrieve
* @param clazz The class of the document to be returned
* @return An `Optional` item with the document if it is found
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
fun <TKey, TDoc> Connection.findById(tableName: String, docId: TKey, clazz: Class<TDoc>) =
Find.byId(tableName, docId, clazz, this)
/**
* Retrieve documents using a field comparison, ordering results by the optional given fields
*
* @param tableName The table from which the document should be retrieved
* @param fields The fields which should be compared
* @param clazz The class of the document to be returned
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A list of documents matching the field comparison
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TDoc> Connection.findByFields(
tableName: String,
fields: Collection<Field<*>>,
clazz: Class<TDoc>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Find.byFields(tableName, fields, clazz, howMatched, orderBy, this)
/**
* Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param criteria The object for which JSON containment should be checked
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A list of documents matching the JSON containment query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TDoc, TContains> Connection.findByContains(
tableName: String,
criteria: TContains,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null
) = Find.byContains(tableName, criteria, clazz, orderBy, this)
/**
* Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A list of documents matching the JSON Path match query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TDoc> Connection.findByJsonPath(
tableName: String,
path: String,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null
) = Find.byJsonPath(tableName, path, clazz, orderBy, this)
/**
* Retrieve the first document using a field comparison and optional ordering fields
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param clazz The class of the document to be returned
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return An `Optional` item, with the first document matching the field comparison if found
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TDoc> Connection.findFirstByFields(
tableName: String,
fields: Collection<Field<*>>,
clazz: Class<TDoc>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Find.firstByFields(tableName, fields, clazz, howMatched, orderBy, this)
/**
* Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return An `Optional` item, with the first document matching the JSON containment query if found
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TDoc, TContains> Connection.findFirstByContains(
tableName: String,
criteria: TContains,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null
) = Find.firstByContains(tableName, criteria, clazz, orderBy, this)
/**
* Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param clazz The class of the document to be returned
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return An `Optional` item, with the first document matching the JSON Path match query if found
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TDoc> Connection.findFirstByJsonPath(
tableName: String,
path: String,
clazz: Class<TDoc>,
orderBy: Collection<Field<*>>? = null
) = Find.firstByJsonPath(tableName, path, clazz, orderBy, this)
// ~~~ DOCUMENT RETRIEVAL QUERIES (Raw JSON) ~~~
/**
* Retrieve all documents in the given table, ordering results by the optional given fields
*
* @param tableName The table from which documents should be retrieved
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A JSON array of documents from the given table
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.jsonAll(tableName: String, orderBy: Collection<Field<*>>? = null) =
Json.all(tableName, orderBy, this)
/**
* Retrieve a document by its ID
*
* @param tableName The table from which the document should be retrieved
* @param docId The ID of the document to retrieve
* @return A JSON document if found, an empty JSON object if not found
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
fun <TKey> Connection.jsonById(tableName: String, docId: TKey) =
Json.byId(tableName, docId, this)
/**
* Retrieve documents using a field comparison, ordering results by the optional given fields
*
* @param tableName The table from which the document should be retrieved
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A JSON array of documents matching the field comparison
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.jsonByFields(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Json.byFields(tableName, fields, howMatched, orderBy, this)
/**
* Retrieve documents using a JSON containment query, ordering results by the optional given fields (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A JSON array of documents matching the JSON containment query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TContains> Connection.jsonByContains(
tableName: String,
criteria: TContains,
orderBy: Collection<Field<*>>? = null
) = Json.byContains(tableName, criteria, orderBy, this)
/**
* Retrieve documents using a JSON Path match query, ordering results by the optional given fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return A JSON array of documents matching the JSON Path match query
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.jsonByJsonPath(tableName: String, path: String, orderBy: Collection<Field<*>>? = null) =
Json.byJsonPath(tableName, path, orderBy, this)
/**
* Retrieve the first document using a field comparison and optional ordering fields
*
* @param tableName The table from which documents should be retrieved
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return The first JSON document matching the field comparison if found, an empty JSON object otherwise
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.jsonFirstByFields(
tableName: String,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Json.firstByFields(tableName, fields, howMatched, orderBy, this)
/**
* Retrieve the first document using a JSON containment query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return The first JSON document matching the JSON containment query if found, an empty JSON object otherwise
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TContains> Connection.jsonFirstByContains(
tableName: String,
criteria: TContains,
orderBy: Collection<Field<*>>? = null
) = Json.firstByContains(tableName, criteria, orderBy, this)
/**
* Retrieve the first document using a JSON Path match query and optional ordering fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @return The first JSON document matching the JSON Path match query if found, an empty JSON object otherwise
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.jsonFirstByJsonPath(tableName: String, path: String, orderBy: Collection<Field<*>>? = null) =
Json.firstByJsonPath(tableName, path, orderBy, this)
// ~~~ DOCUMENT RETRIEVAL QUERIES (Write raw JSON to output) ~~~
/**
* Write all documents in the given table to the given `PrintWriter`, ordering results by the optional given fields
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If query execution fails
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.writeJsonAll(tableName: String, writer: PrintWriter, orderBy: Collection<Field<*>>? = null) =
Json.writeAll(tableName, writer, orderBy, this)
/**
* Write a document to the given `PrintWriter` by its ID
*
* @param tableName The table from which the document should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param docId The ID of the document to retrieve
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
fun <TKey> Connection.writeJsonById(tableName: String, writer: PrintWriter, docId: TKey) =
Json.writeById(tableName, writer, docId, this)
/**
* Write documents to the given `PrintWriter` using a field comparison, ordering results by the optional given fields
*
* @param tableName The table from which the document should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.writeJsonByFields(
tableName: String,
writer: PrintWriter,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Json.writeByFields(tableName, writer, fields, howMatched, orderBy, this)
/**
* Write documents to the given `PrintWriter` using a JSON containment query, ordering results by the optional given
* fields (PostgreSQL only)
*
* @param tableName The name of the table in which document existence should be checked
* @param writer The `PrintWriter` to which the results should be written
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TContains> Connection.writeJsonByContains(
tableName: String,
writer: PrintWriter,
criteria: TContains,
orderBy: Collection<Field<*>>? = null
) = Json.writeByContains(tableName, writer, criteria, orderBy, this)
/**
* Write documents to the given `PrintWriter` using a JSON Path match query, ordering results by the optional given
* fields (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.writeJsonByJsonPath(
tableName: String,
writer: PrintWriter,
path: String,
orderBy: Collection<Field<*>>? = null
) = Json.writeByJsonPath(tableName, writer, path, orderBy, this)
/**
* Write the first document to the given `PrintWriter` using a field comparison and optional ordering fields
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched (optional, defaults to `FieldMatch.ALL`)
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.writeJsonFirstByFields(
tableName: String,
writer: PrintWriter,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null,
orderBy: Collection<Field<*>>? = null
) = Json.writeFirstByFields(tableName, writer, fields, howMatched, orderBy, this)
/**
* Write the first document to the given `PrintWriter` using a JSON containment query and optional ordering fields
* (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param criteria The object for which JSON containment should be checked
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <TContains> Connection.writeJsonFirstByContains(
tableName: String,
writer: PrintWriter,
criteria: TContains,
orderBy: Collection<Field<*>>? = null
) = Json.writeFirstByContains(tableName, writer, criteria, orderBy, this)
/**
* Write the first document to the given `PrintWriter` using a JSON Path match query and optional ordering fields
* (PostgreSQL only)
*
* @param tableName The table from which documents should be retrieved
* @param writer The `PrintWriter` to which the results should be written
* @param path The JSON path comparison to match
* @param orderBy Fields by which the query should be ordered (optional, defaults to no ordering)
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.writeJsonFirstByJsonPath(
tableName: String,
writer: PrintWriter,
path: String,
orderBy: Collection<Field<*>>? = null
) = Json.writeFirstByJsonPath(tableName, writer, 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
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
fun <TKey, 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
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun <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
*/
@Throws(DocumentException::class)
fun <TContains, 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
*/
@Throws(DocumentException::class)
fun <TPatch> Connection.patchByJsonPath(tableName: String, path: String, patch: TPatch) =
Patch.byJsonPath(tableName, path, patch, this)
// ~~~ DOCUMENT FIELD REMOVAL QUERIES ~~~
/**
* Remove fields from a document by its ID
*
* @param tableName The name of the table in which the document's fields should be removed
* @param docId The ID of the document to have fields removed
* @param toRemove The names of the fields to be removed
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
fun <TKey> Connection.removeFieldsById(tableName: String, docId: TKey, toRemove: Collection<String>) =
RemoveFields.byId(tableName, docId, toRemove, this)
/**
* Remove fields from documents using a field comparison
*
* @param tableName The name of the table in which document fields should be removed
* @param fields The fields which should be compared
* @param toRemove The names of the fields to be removed
* @param howMatched How the fields should be matched
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.removeFieldsByFields(
tableName: String,
fields: Collection<Field<*>>,
toRemove: Collection<String>,
howMatched: FieldMatch? = null
) = RemoveFields.byFields(tableName, fields, toRemove, howMatched, this)
/**
* Remove fields from documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table in which document fields should be removed
* @param criteria The object against which JSON containment should be checked
* @param toRemove The names of the fields to be removed
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
fun <TContains> Connection.removeFieldsByContains(
tableName: String,
criteria: TContains,
toRemove: Collection<String>
) = RemoveFields.byContains(tableName, criteria, toRemove, this)
/**
* Remove fields from documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table in which document fields should be removed
* @param path The JSON path comparison to match
* @param toRemove The names of the fields to be removed
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
fun Connection.removeFieldsByJsonPath(tableName: String, path: String, toRemove: Collection<String>) =
RemoveFields.byJsonPath(tableName, path, toRemove, this)
// ~~~ DOCUMENT DELETION QUERIES ~~~
/**
* Delete a document by its ID
*
* @param tableName The name of the table from which documents should be deleted
* @param docId The ID of the document to be deleted
* @throws DocumentException If no dialect has been configured
*/
@Throws(DocumentException::class)
fun <TKey> Connection.deleteById(tableName: String, docId: TKey) =
Delete.byId(tableName, docId, this)
/**
* Delete documents using a field comparison
*
* @param tableName The name of the table from which documents should be deleted
* @param fields The fields which should be compared
* @param howMatched How the fields should be matched
* @throws DocumentException If no dialect has been configured, or if parameters are invalid
*/
@Throws(DocumentException::class)
@JvmOverloads
fun Connection.deleteByFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
Delete.byFields(tableName, fields, howMatched, this)
/**
* Delete documents using a JSON containment query (PostgreSQL only)
*
* @param tableName The name of the table from which documents should be deleted
* @param criteria The object for which JSON containment should be checked
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
fun <TContains> Connection.deleteByContains(tableName: String, criteria: TContains) =
Delete.byContains(tableName, criteria, this)
/**
* Delete documents using a JSON Path match query (PostgreSQL only)
*
* @param tableName The name of the table from which documents should be deleted
* @param path The JSON path comparison to match
* @throws DocumentException If called on a SQLite connection
*/
@Throws(DocumentException::class)
fun Connection.deleteByJsonPath(tableName: String, path: String) =
Delete.byJsonPath(tableName, path, this)

View File

@ -0,0 +1,62 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.Field
import solutions.bitbadger.documents.FieldMatch
import kotlin.jvm.Throws
import solutions.bitbadger.documents.query.byFields as byFieldsBase
/**
* Functions to count documents
*/
object CountQuery {
/**
* Query to count all documents in a table
*
* @param tableName The table in which to count documents (may include schema)
* @return A query to count documents
*/
@JvmStatic
fun all(tableName: String) =
"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
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
byFieldsBase(all(tableName), 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
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
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
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String) =
statementWhere(all(tableName), Where.jsonPathMatches())
}

View File

@ -0,0 +1,108 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.*
import kotlin.jvm.Throws
/**
* Functions to create queries to define tables and indexes
*/
object DefinitionQuery {
/**
* SQL statement to create a document table
*
* @param tableName The name of the table to create (may include schema)
* @param dataType The type of data for the column (`JSON`, `JSONB`, etc.)
* @return A query to create a document table
*/
@JvmStatic
fun ensureTableFor(tableName: String, dataType: String) =
"CREATE TABLE IF NOT EXISTS $tableName (data $dataType NOT NULL)"
/**
* SQL statement to create a document table in the current dialect
*
* @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
* @throws DocumentException If the dialect is neither provided nor configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun ensureTable(tableName: String, dialect: Dialect? = null) =
when (dialect ?: Configuration.dialect("create table creation query")) {
Dialect.POSTGRESQL -> ensureTableFor(tableName, "JSONB")
Dialect.SQLITE -> ensureTableFor(tableName, "TEXT")
}
/**
* Split a schema and table name
*
* @param tableName The name of the table, possibly with a schema
* @return A pair with the first item as the schema and the second as the table name
*/
private fun splitSchemaAndTable(tableName: String) =
tableName.split('.').let { if (it.size == 1) Pair("", tableName) else Pair(it[0], it[1]) }
/**
* SQL statement to create an index on one or more fields in a JSON document
*
* @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 fields One or more fields to include in the 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
* @throws DocumentException If the dialect is neither provided nor configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun ensureIndexOn(
tableName: String,
indexName: String,
fields: Collection<String>,
dialect: Dialect? = null
): String {
val (_, tbl) = splitSchemaAndTable(tableName)
val mode = dialect ?: Configuration.dialect("create index $tbl.$indexName")
val jsonFields = fields.joinToString(", ") {
val parts = it.split(' ')
val direction = if (parts.size > 1) " ${parts[1]}" else ""
"(" + Field.nameToPath(parts[0], mode, FieldFormat.SQL) + ")$direction"
}
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)"
}
/**
* 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 dialect The SQL dialect to use when creating this index (optional, used in place of current)
* @return A query to create the key index
* @throws DocumentException If the dialect is neither provided nor configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun ensureKey(tableName: String, dialect: Dialect? = null) =
ensureIndexOn(tableName, "key", listOf(Configuration.idField), dialect).replace("INDEX", "UNIQUE INDEX")
/**
* Create a document-wide index on a table (PostgreSQL only)
*
* @param tableName The name of the table on which the document index should be created
* @param indexType The type of index to be created
* @return The SQL statement to create an index on JSON documents in the specified table
* @throws DocumentException If the database mode is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun ensureDocumentIndexOn(tableName: String, indexType: DocumentIndex): String {
if (Configuration.dialect("create document index query") != Dialect.POSTGRESQL) {
throw DocumentException("'Document indexes are only supported on PostgreSQL")
}
val (_, tbl) = splitSchemaAndTable(tableName)
return "CREATE INDEX IF NOT EXISTS idx_${tbl}_document ON $tableName USING GIN (data${indexType.sql})"
}
}

View File

@ -0,0 +1,76 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.Field
import solutions.bitbadger.documents.FieldMatch
import kotlin.jvm.Throws
import solutions.bitbadger.documents.query.byFields as byFieldsBase
import solutions.bitbadger.documents.query.byId as byIdBase
/**
* Functions to delete documents
*/
object DeleteQuery {
/**
* Query to delete documents from a table
*
* @param tableName The table in which documents should be deleted (may include schema)
* @return A query to delete documents
*/
private fun delete(tableName: String) =
"DELETE FROM $tableName"
/**
* 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
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TKey> byId(tableName: String, docId: TKey? = null) =
byIdBase(delete(tableName), 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
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
byFieldsBase(delete(tableName), 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
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byContains(tableName: String) =
statementWhere(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
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String) =
statementWhere(delete(tableName), Where.jsonPathMatches())
}

View File

@ -0,0 +1,68 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.AutoId
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.Dialect
import solutions.bitbadger.documents.DocumentException
import kotlin.jvm.Throws
/**
* Functions for document-level operations
*/
object DocumentQuery {
/**
* Query to insert a document
*
* @param tableName The table into which to insert (may include schema)
* @return A query to insert a document
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun insert(tableName: String, autoId: AutoId? = null): String {
val id = Configuration.idField
val values = when (Configuration.dialect("create INSERT statement")) {
Dialect.POSTGRESQL -> when (autoId ?: AutoId.DISABLED) {
AutoId.DISABLED -> ":data"
AutoId.NUMBER -> ":data::jsonb || ('{\"$id\":' || " +
"(SELECT COALESCE(MAX((data->>'$id')::numeric), 0) + 1 " +
"FROM $tableName) || '}')::jsonb"
AutoId.UUID -> ":data::jsonb || '{\"$id\":\"${AutoId.generateUUID()}\"}'"
AutoId.RANDOM_STRING -> ":data::jsonb || '{\"$id\":\"${AutoId.generateRandomString()}\"}'"
}
Dialect.SQLITE -> when (autoId ?: AutoId.DISABLED) {
AutoId.DISABLED -> ":data"
AutoId.NUMBER -> "json_set(:data, '$.$id', " +
"(SELECT coalesce(max(data->>'$id'), 0) + 1 FROM $tableName))"
AutoId.UUID -> "json_set(:data, '$.$id', '${AutoId.generateUUID()}')"
AutoId.RANDOM_STRING -> "json_set(:data, '$.$id', '${AutoId.generateRandomString()}')"
}
}
return "INSERT INTO $tableName VALUES ($values)"
}
/**
* Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
*
* @param tableName The table into which to save (may include schema)
* @return A query to save a document
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
@JvmStatic
fun save(tableName: String) =
insert(tableName, AutoId.DISABLED) +
" ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data"
/**
* 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)
* @return A query to update documents
*/
@JvmStatic
fun update(tableName: String) =
"UPDATE $tableName SET data = :data"
}

View File

@ -0,0 +1,75 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.Field
import solutions.bitbadger.documents.FieldMatch
import kotlin.jvm.Throws
/**
* Functions to check for document existence
*/
object ExistsQuery {
/**
* Query to check for document existence in a table
*
* @param tableName The table in which existence should be checked (may include schema)
* @param where The `WHERE` clause with the existence criteria
* @return A query to check document existence
*/
private fun exists(tableName: String, where: String) =
"SELECT EXISTS (SELECT 1 FROM $tableName WHERE $where) AS it"
/**
* Query to check for document existence by ID
*
* @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
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TKey> byId(tableName: String, docId: TKey? = null) =
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
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
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
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
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
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String) =
exists(tableName, Where.jsonPathMatches())
}

View File

@ -0,0 +1,77 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.Field
import solutions.bitbadger.documents.FieldMatch
import kotlin.jvm.Throws
import solutions.bitbadger.documents.query.byId as byIdBase
import solutions.bitbadger.documents.query.byFields as byFieldsBase
/**
* Functions to retrieve documents
*/
object FindQuery {
/**
* 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
*/
@JvmStatic
fun all(tableName: String) =
"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
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TKey> byId(tableName: String, docId: TKey? = null) =
byIdBase(all(tableName), 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
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
byFieldsBase(all(tableName), 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
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
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
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String) =
statementWhere(all(tableName), Where.jsonPathMatches())
}

View File

@ -0,0 +1,77 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.*
import kotlin.jvm.Throws
import solutions.bitbadger.documents.query.byFields as byFieldsBase
import solutions.bitbadger.documents.query.byId as byIdBase
/**
* Functions to create queries to patch (partially update) JSON documents
*/
object PatchQuery {
/**
* Create an `UPDATE` statement to patch documents
*
* @param tableName The table to be updated
* @return A query to patch documents
*/
private fun patch(tableName: String) =
when (Configuration.dialect("create patch query")) {
Dialect.POSTGRESQL -> "data || :data"
Dialect.SQLITE -> "json_patch(data, json(:data))"
}.let { "UPDATE $tableName SET data = $it" }
/**
* A query to patch (partially update) a JSON document by its ID
*
* @param tableName The name of the table where the document is stored
* @param docId The ID of the document to be updated (optional, used for type checking)
* @return A query to patch a JSON document by its ID
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TKey> byId(tableName: String, docId: TKey? = null) =
byIdBase(patch(tableName), docId)
/**
* A query to patch (partially update) a JSON document using field match criteria
*
* @param tableName The name of the table where the documents are stored
* @param fields The field criteria
* @param howMatched How the fields should be matched (optional, defaults to `ALL`)
* @return A query to patch JSON documents by field match criteria
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(tableName: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
byFieldsBase(patch(tableName), fields, howMatched)
/**
* A query to patch (partially update) a JSON document by JSON containment (PostgreSQL only)
*
* @param tableName The name of the table where the document is stored
* @return A query to patch JSON documents by JSON containment
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byContains(tableName: String) =
statementWhere(patch(tableName), Where.jsonContains())
/**
* A query to patch (partially update) a JSON document by JSON path match (PostgreSQL only)
*
* @param tableName The name of the table where the document is stored
* @return A query to patch JSON documents by JSON path match
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String) =
statementWhere(patch(tableName), Where.jsonPathMatches())
}

View File

@ -0,0 +1,82 @@
@file:JvmName("QueryUtils")
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.*
import kotlin.jvm.Throws
// ~~~ TOP-LEVEL FUNCTIONS FOR THE QUERY PACKAGE ~~~
/**
* Combine a query (`SELECT`, `UPDATE`, etc.) and a `WHERE` clause
*
* @param statement The first part of the statement
* @param where The `WHERE` clause for the statement
* @return The two parts of the query combined with `WHERE`
*/
fun statementWhere(statement: String, where: String) =
"$statement WHERE $where"
/**
* Create a query by a document's ID
*
* @param statement The SQL statement to be run against a document by its ID
* @param docId The ID of the document targeted
* @returns A query addressing a document by its ID
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
fun <TKey> byId(statement: String, docId: TKey) =
statementWhere(statement, Where.byId(docId = docId))
/**
* Create a query on JSON fields
*
* @param statement The SQL statement to be run against matching fields
* @param fields The field conditions to be matched
* @param howMatched Whether to match any or all of the field conditions (optional; default ALL)
* @return A query addressing documents by field matching conditions
*/
@Throws(DocumentException::class)
@JvmOverloads
fun byFields(statement: String, fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
statementWhere(statement, Where.byFields(fields, howMatched))
/**
* Create an `ORDER BY` clause for the given fields
*
* @param fields One or more fields by which to order
* @param dialect The SQL dialect for the generated clause
* @return An `ORDER BY` clause for the given fields
*/
@Throws(DocumentException::class)
@JvmOverloads
fun orderBy(fields: Collection<Field<*>>, dialect: Dialect? = null): String {
val mode = dialect ?: Configuration.dialect("generate ORDER BY clause")
if (fields.isEmpty()) return ""
val orderFields = fields.joinToString(", ") {
val (field, direction) =
if (it.name.indexOf(' ') > -1) {
val parts = it.name.split(' ')
Pair(Field.named(parts[0]), " " + parts.drop(1).joinToString(" "))
} else {
Pair<Field<*>, String?>(it, null)
}
val path = when {
field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld ->
when (mode) {
Dialect.POSTGRESQL -> "(${fld.path(mode)})::numeric"
Dialect.SQLITE -> fld.path(mode)
}
}
field.name.startsWith("i:") -> Field.named(field.name.substring(2)).path(mode).let { p ->
when (mode) {
Dialect.POSTGRESQL -> "LOWER($p)"
Dialect.SQLITE -> "$p COLLATE NOCASE"
}
}
else -> field.path(mode)
}
"$path${direction ?: ""}"
}
return " ORDER BY $orderFields"
}

View File

@ -0,0 +1,89 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.*
import kotlin.jvm.Throws
import solutions.bitbadger.documents.query.byFields as byFieldsBase
import solutions.bitbadger.documents.query.byId as byIdBase
/**
* Functions to create queries to remove fields from documents
*/
object RemoveFieldsQuery {
/**
* Create a query to remove fields based on the given parameters
*
* @param tableName The name of the table in which documents should have fields removed
* @param toRemove The parameters for the fields to be removed
* @return A query to remove fields from documents in the given table
*/
private fun removeFields(tableName: String, toRemove: Collection<Parameter<*>>) =
when (Configuration.dialect("generate field removal query")) {
Dialect.POSTGRESQL -> "UPDATE $tableName SET data = data - ${toRemove.elementAt(0).name}::text[]"
Dialect.SQLITE -> toRemove.joinToString(", ") { it.name }.let {
"UPDATE $tableName SET data = json_remove(data, $it)"
}
}
/**
* A query to patch (partially update) a JSON document by its ID
*
* @param tableName The name of the table where the document is stored
* @param toRemove The parameters for the fields to be removed
* @param docId The ID of the document to be updated (optional, used for type checking)
* @return A query to patch a JSON document by its ID
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TKey> byId(tableName: String, toRemove: Collection<Parameter<*>>, docId: TKey? = null) =
byIdBase(removeFields(tableName, toRemove), docId)
/**
* A query to patch (partially update) a JSON document using field match criteria
*
* @param tableName The name of the table where the documents are stored
* @param toRemove The parameters for the fields to be removed
* @param fields The field criteria
* @param howMatched How the fields should be matched (optional, defaults to `ALL`)
* @return A query to patch JSON documents by field match criteria
* @throws DocumentException If the dialect is not configured
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(
tableName: String,
toRemove: Collection<Parameter<*>>,
fields: Collection<Field<*>>,
howMatched: FieldMatch? = null
) =
byFieldsBase(removeFields(tableName, toRemove), fields, howMatched)
/**
* A query to patch (partially update) a JSON document by JSON containment (PostgreSQL only)
*
* @param tableName The name of the table where the document is stored
* @param toRemove The parameters for the fields to be removed
* @return A query to patch JSON documents by JSON containment
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byContains(tableName: String, toRemove: Collection<Parameter<*>>) =
statementWhere(removeFields(tableName, toRemove), Where.jsonContains())
/**
* A query to patch (partially update) a JSON document by JSON path match (PostgreSQL only)
*
* @param tableName The name of the table where the document is stored
* @param toRemove The parameters for the fields to be removed
* @return A query to patch JSON documents by JSON path match
* @throws DocumentException If the database dialect is not PostgreSQL
*/
@Throws(DocumentException::class)
@JvmStatic
fun byJsonPath(tableName: String, toRemove: Collection<Parameter<*>>) =
statementWhere(removeFields(tableName, toRemove), Where.jsonPathMatches())
}

View File

@ -0,0 +1,74 @@
package solutions.bitbadger.documents.query
import solutions.bitbadger.documents.Configuration
import solutions.bitbadger.documents.Dialect
import solutions.bitbadger.documents.DocumentException
import solutions.bitbadger.documents.Field
import solutions.bitbadger.documents.FieldMatch
import kotlin.jvm.Throws
/**
* Functions to create `WHERE` clause fragments
*/
object Where {
/**
* Create a `WHERE` clause fragment to query by one or more fields
*
* @param fields The fields to be queried
* @param howMatched How the fields should be matched (optional, defaults to `ALL`)
* @return A `WHERE` clause fragment to match the given fields
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun byFields(fields: Collection<Field<*>>, howMatched: FieldMatch? = null) =
fields.joinToString(" ${(howMatched ?: FieldMatch.ALL).sql} ") { it.toWhere() }
/**
* Create a `WHERE` clause fragment to retrieve a document by its ID
*
* @param parameterName The parameter name to use for the ID placeholder (optional, defaults to ":id")
* @param docId The ID value (optional; used for type determinations, string assumed if not provided)
* @return A `WHERE` clause fragment to match the document's ID
* @throws DocumentException If the dialect has not been set
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun <TKey> byId(parameterName: String = ":id", docId: TKey? = null) =
byFields(listOf(Field.equal(Configuration.idField, docId ?: "", parameterName)))
/**
* Create a `WHERE` clause fragment to implement a JSON containment query (PostgreSQL only)
*
* @param parameterName The parameter name to use for the JSON placeholder (optional, defaults to ":criteria")
* @return A `WHERE` clause fragment to implement a JSON containment criterion
* @throws DocumentException If called against a SQLite database
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun jsonContains(parameterName: String = ":criteria") =
when (Configuration.dialect("create containment WHERE clause")) {
Dialect.POSTGRESQL -> "data @> $parameterName"
Dialect.SQLITE -> throw DocumentException("JSON containment is not supported")
}
/**
* Create a `WHERE` clause fragment to implement a JSON path match query (PostgreSQL only)
*
* @param parameterName The parameter name to use for the placeholder (optional, defaults to ":path")
* @return A `WHERE` clause fragment to implement a JSON path match criterion
* @throws DocumentException If called against a SQLite database
*/
@Throws(DocumentException::class)
@JvmStatic
@JvmOverloads
fun jsonPathMatches(parameterName: String = ":path") =
when (Configuration.dialect("create JSON path match WHERE clause")) {
Dialect.POSTGRESQL -> "jsonb_path_exists(data, $parameterName::jsonpath)"
Dialect.SQLITE -> throw DocumentException("JSON path match is not supported")
}
}

View File

@ -0,0 +1,19 @@
# Module core
This module contains configuration and support files for the document store API, as well as an implementation suitable for any JVM language.
# Package solutions.bitbadger.documents
Configuration and other items to support the document store API
# Package solutions.bitbadger.documents.query
Functions to create document manipulation queries
# Package solutions.bitbadger.documents.java
A Java-focused implementation of the document store API
# Package solutions.bitbadger.documents.java.extensions
Extensions on the Java `Connection` object for document manipulation

View File

@ -0,0 +1,20 @@
module solutions.bitbadger.documents.core.tests {
requires solutions.bitbadger.documents.core;
requires com.fasterxml.jackson.databind;
requires java.sql;
requires kotlin.stdlib;
requires kotlin.test.junit5;
requires org.junit.jupiter.api;
requires org.slf4j;
requires annotations;
//requires org.checkerframework.checker.qual;
exports solutions.bitbadger.documents.core.tests;
exports solutions.bitbadger.documents.core.tests.integration;
exports solutions.bitbadger.documents.core.tests.java;
exports solutions.bitbadger.documents.core.tests.java.integration;
opens solutions.bitbadger.documents.core.tests;
opens solutions.bitbadger.documents.core.tests.java;
opens solutions.bitbadger.documents.core.tests.java.integration;
}

View File

@ -0,0 +1,216 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.AutoId;
import solutions.bitbadger.documents.DocumentException;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the `AutoId` enum
*/
@DisplayName("Core | Java | AutoId")
final public class AutoIdTest {
@Test
@DisplayName("Generates a UUID string")
public void generateUUID() {
assertEquals(32, AutoId.generateUUID().length(), "The UUID should have been a 32-character string");
}
@Test
@DisplayName("Generates a random hex character string of an even length")
public void generateRandomStringEven() {
final String result = AutoId.generateRandomString(8);
assertEquals(8, result.length(), "There should have been 8 characters in " + result);
}
@Test
@DisplayName("Generates a random hex character string of an odd length")
public void generateRandomStringOdd() {
final String result = AutoId.generateRandomString(11);
assertEquals(11, result.length(), "There should have been 11 characters in " + result);
}
@Test
@DisplayName("Generates different random hex character strings")
public void generateRandomStringIsRandom() {
final String result1 = AutoId.generateRandomString(16);
final String result2 = AutoId.generateRandomString(16);
assertNotEquals(result1, result2, "There should have been 2 different strings generated");
}
@Test
@DisplayName("needsAutoId fails for null document")
public void needsAutoIdFailsForNullDocument() {
assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.DISABLED, null, "id"));
}
@Test
@DisplayName("needsAutoId fails for missing ID property")
public void needsAutoIdFailsForMissingId() {
assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.UUID, new IntIdClass(0), "Id"));
}
@Test
@DisplayName("needsAutoId returns false if disabled")
public void needsAutoIdFalseIfDisabled() {
try {
assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns true for Number strategy and byte ID of 0")
public void needsAutoIdTrueForByteWithZero() {
try {
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 0), "id"),
"Number Auto ID with 0 should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0")
public void needsAutoIdFalseForByteWithNonZero() {
try {
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new ByteIdClass((byte) 77), "id"),
"Number Auto ID with 77 should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns true for Number strategy and short ID of 0")
public void needsAutoIdTrueForShortWithZero() {
try {
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 0), "id"),
"Number Auto ID with 0 should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Number strategy and short ID of non-0")
public void needsAutoIdFalseForShortWithNonZero() {
try {
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new ShortIdClass((short) 31), "id"),
"Number Auto ID with 31 should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns true for Number strategy and int ID of 0")
public void needsAutoIdTrueForIntWithZero() {
try {
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(0), "id"),
"Number Auto ID with 0 should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Number strategy and int ID of non-0")
public void needsAutoIdFalseForIntWithNonZero() {
try {
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new IntIdClass(6), "id"),
"Number Auto ID with 6 should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns true for Number strategy and long ID of 0")
public void needsAutoIdTrueForLongWithZero() {
try {
assertTrue(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(0L), "id"),
"Number Auto ID with 0 should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Number strategy and long ID of non-0")
public void needsAutoIdFalseForLongWithNonZero() {
try {
assertFalse(AutoId.needsAutoId(AutoId.NUMBER, new LongIdClass(2L), "id"),
"Number Auto ID with 2 should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId fails for Number strategy and non-number ID")
public void needsAutoIdFailsForNumberWithStringId() {
assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.NUMBER, new StringIdClass(""), "id"));
}
@Test
@DisplayName("needsAutoId returns true for UUID strategy and blank ID")
public void needsAutoIdTrueForUUIDWithBlank() {
try {
assertTrue(AutoId.needsAutoId(AutoId.UUID, new StringIdClass(""), "id"),
"UUID Auto ID with blank should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for UUID strategy and non-blank ID")
public void needsAutoIdFalseForUUIDNotBlank() {
try {
assertFalse(AutoId.needsAutoId(AutoId.UUID, new StringIdClass("howdy"), "id"),
"UUID Auto ID with non-blank should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId fails for UUID strategy and non-string ID")
public void needsAutoIdFailsForUUIDNonString() {
assertThrows(DocumentException.class, () -> AutoId.needsAutoId(AutoId.UUID, new IntIdClass(5), "id"));
}
@Test
@DisplayName("needsAutoId returns true for Random String strategy and blank ID")
public void needsAutoIdTrueForRandomWithBlank() {
try {
assertTrue(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass(""), "id"),
"Random String Auto ID with blank should return true");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId returns false for Random String strategy and non-blank ID")
public void needsAutoIdFalseForRandomNotBlank() {
try {
assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, new StringIdClass("full"), "id"),
"Random String Auto ID with non-blank should return false");
} catch (DocumentException ex) {
fail(ex);
}
}
@Test
@DisplayName("needsAutoId fails for Random String strategy and non-string ID")
public void needsAutoIdFailsForRandomNonString() {
assertThrows(DocumentException.class,
() -> AutoId.needsAutoId(AutoId.RANDOM_STRING, new ShortIdClass((short) 55), "id"));
}
}

View File

@ -0,0 +1,18 @@
package solutions.bitbadger.documents.core.tests.java;
public class ByteIdClass {
private byte id;
public byte getId() {
return id;
}
public void setId(byte id) {
this.id = id;
}
public ByteIdClass(byte id) {
this.id = id;
}
}

View File

@ -0,0 +1,48 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.AutoId;
import solutions.bitbadger.documents.Configuration;
import solutions.bitbadger.documents.Dialect;
import solutions.bitbadger.documents.DocumentException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Unit tests for the `Configuration` object
*/
@DisplayName("Core | Java | Configuration")
final public class ConfigurationTest {
@Test
@DisplayName("Default ID field is `id`")
public void defaultIdField() {
assertEquals("id", Configuration.idField, "Default ID field incorrect");
}
@Test
@DisplayName("Default Auto ID strategy is `DISABLED`")
public void defaultAutoId() {
assertEquals(AutoId.DISABLED, Configuration.autoIdStrategy, "Default Auto ID strategy should be `disabled`");
}
@Test
@DisplayName("Default ID string length should be 16")
public void defaultIdStringLength() {
assertEquals(16, Configuration.idStringLength, "Default ID string length should be 16");
}
@Test
@DisplayName("Dialect is derived from connection string")
public void dialectIsDerived() throws DocumentException {
try {
assertThrows(DocumentException.class, Configuration::dialect);
Configuration.setConnectionString("jdbc:postgresql:db");
assertEquals(Dialect.POSTGRESQL, Configuration.dialect());
} finally {
Configuration.setConnectionString(null);
}
}
}

View File

@ -0,0 +1,87 @@
package solutions.bitbadger.documents.core.tests.java;
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.query.CountQuery;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
/**
* Unit tests for the `Count` object
*/
@DisplayName("Core | Java | Query | CountQuery")
final public class CountQueryTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("all generates correctly")
public void all() {
assertEquals(String.format("SELECT COUNT(*) AS it FROM %s", TEST_TABLE), CountQuery.all(TEST_TABLE),
"Count query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | PostgreSQL")
public void byFieldsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("SELECT COUNT(*) AS it FROM %s WHERE data->>'test' = :field0", TEST_TABLE),
CountQuery.byFields(TEST_TABLE, List.of(Field.equal("test", "", ":field0"))),
"Count query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | SQLite")
public void byFieldsSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("SELECT COUNT(*) AS it FROM %s WHERE data->>'test' = :field0", TEST_TABLE),
CountQuery.byFields(TEST_TABLE, List.of(Field.equal("test", "", ":field0"))),
"Count query not constructed correctly");
}
@Test
@DisplayName("byContains generates correctly | PostgreSQL")
public void byContainsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("SELECT COUNT(*) AS it FROM %s WHERE data @> :criteria", TEST_TABLE),
CountQuery.byContains(TEST_TABLE), "Count query not constructed correctly");
}
@Test
@DisplayName("byContains fails | SQLite")
public void byContainsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> CountQuery.byContains(TEST_TABLE));
}
@Test
@DisplayName("byJsonPath generates correctly | PostgreSQL")
public void byJsonPathPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(
String.format("SELECT COUNT(*) AS it FROM %s WHERE jsonb_path_exists(data, :path::jsonpath)",
TEST_TABLE),
CountQuery.byJsonPath(TEST_TABLE), "Count query not constructed correctly");
}
@Test
@DisplayName("byJsonPath fails | SQLite")
public void byJsonPathSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> CountQuery.byJsonPath(TEST_TABLE));
}
}

View File

@ -0,0 +1,133 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.Dialect;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.DocumentIndex;
import solutions.bitbadger.documents.query.DefinitionQuery;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
/**
* Unit tests for the `Definition` object
*/
@DisplayName("Core | Java | Query | DefinitionQuery")
final public class DefinitionQueryTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("ensureTableFor generates correctly")
public void ensureTableFor() {
assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
DefinitionQuery.ensureTableFor("my.table", "JSONB"),
"CREATE TABLE statement not constructed correctly");
}
@Test
@DisplayName("ensureTable generates correctly | PostgreSQL")
public void ensureTablePostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("CREATE TABLE IF NOT EXISTS %s (data JSONB NOT NULL)", TEST_TABLE),
DefinitionQuery.ensureTable(TEST_TABLE));
}
@Test
@DisplayName("ensureTable generates correctly | SQLite")
public void ensureTableSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("CREATE TABLE IF NOT EXISTS %s (data TEXT NOT NULL)", TEST_TABLE),
DefinitionQuery.ensureTable(TEST_TABLE));
}
@Test
@DisplayName("ensureTable fails when no dialect is set")
public void ensureTableFailsUnknown() {
assertThrows(DocumentException.class, () -> DefinitionQuery.ensureTable(TEST_TABLE));
}
@Test
@DisplayName("ensureKey generates correctly with schema")
public void ensureKeyWithSchema() throws DocumentException {
assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))",
DefinitionQuery.ensureKey("test.table", Dialect.POSTGRESQL),
"CREATE INDEX for key statement with schema not constructed correctly");
}
@Test
@DisplayName("ensureKey generates correctly without schema")
public void ensureKeyWithoutSchema() throws DocumentException {
assertEquals(
String.format("CREATE UNIQUE INDEX IF NOT EXISTS idx_%1$s_key ON %1$s ((data->>'id'))", TEST_TABLE),
DefinitionQuery.ensureKey(TEST_TABLE, Dialect.SQLITE),
"CREATE INDEX for key statement without schema not constructed correctly");
}
@Test
@DisplayName("ensureIndexOn generates multiple fields and directions")
public void ensureIndexOnMultipleFields() throws DocumentException {
assertEquals(
"CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)",
DefinitionQuery.ensureIndexOn("test.table", "gibberish", List.of("taco", "guac DESC", "salsa ASC"),
Dialect.POSTGRESQL),
"CREATE INDEX for multiple field statement not constructed correctly");
}
@Test
@DisplayName("ensureIndexOn generates nested field | PostgreSQL")
public void ensureIndexOnNestedPostgres() throws DocumentException {
assertEquals(String.format("CREATE INDEX IF NOT EXISTS idx_%1$s_nest ON %1$s ((data#>>'{a,b,c}'))", TEST_TABLE),
DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List.of("a.b.c"), Dialect.POSTGRESQL),
"CREATE INDEX for nested PostgreSQL field incorrect");
}
@Test
@DisplayName("ensureIndexOn generates nested field | SQLite")
public void ensureIndexOnNestedSQLite() throws DocumentException {
assertEquals(
String.format("CREATE INDEX IF NOT EXISTS idx_%1$s_nest ON %1$s ((data->'a'->'b'->>'c'))", TEST_TABLE),
DefinitionQuery.ensureIndexOn(TEST_TABLE, "nest", List.of("a.b.c"), Dialect.SQLITE),
"CREATE INDEX for nested SQLite field incorrect");
}
@Test
@DisplayName("ensureDocumentIndexOn generates Full | PostgreSQL")
public void ensureDocumentIndexOnFullPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("CREATE INDEX IF NOT EXISTS idx_%1$s_document ON %1$s USING GIN (data)", TEST_TABLE),
DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL),
"CREATE INDEX for full document index incorrect");
}
@Test
@DisplayName("ensureDocumentIndexOn generates Optimized | PostgreSQL")
public void ensureDocumentIndexOnOptimizedPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(
String.format("CREATE INDEX IF NOT EXISTS idx_%1$s_document ON %1$s USING GIN (data jsonb_path_ops)",
TEST_TABLE),
DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.OPTIMIZED),
"CREATE INDEX for optimized document index incorrect");
}
@Test
@DisplayName("ensureDocumentIndexOn fails | SQLite")
public void ensureDocumentIndexOnFailsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class,
() -> DefinitionQuery.ensureDocumentIndexOn(TEST_TABLE, DocumentIndex.FULL));
}
}

View File

@ -0,0 +1,94 @@
package solutions.bitbadger.documents.core.tests.java;
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.query.DeleteQuery;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
/**
* Unit tests for the `Delete` object
*/
@DisplayName("Core | Java | Query | DeleteQuery")
final public class DeleteQueryTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("byId generates correctly | PostgreSQL")
public void byIdPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("DELETE FROM %s WHERE data->>'id' = :id", TEST_TABLE), DeleteQuery.byId(TEST_TABLE),
"Delete query not constructed correctly");
}
@Test
@DisplayName("byId generates correctly | SQLite")
public void byIdSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("DELETE FROM %s WHERE data->>'id' = :id", TEST_TABLE), DeleteQuery.byId(TEST_TABLE),
"Delete query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | PostgreSQL")
public void byFieldsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("DELETE FROM %s WHERE data->>'a' = :b", TEST_TABLE),
DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal("a", "", ":b"))),
"Delete query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | SQLite")
public void byFieldsSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("DELETE FROM %s WHERE data->>'a' = :b", TEST_TABLE),
DeleteQuery.byFields(TEST_TABLE, List.of(Field.equal("a", "", ":b"))),
"Delete query not constructed correctly");
}
@Test
@DisplayName("byContains generates correctly | PostgreSQL")
public void byContainsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("DELETE FROM %s WHERE data @> :criteria", TEST_TABLE),
DeleteQuery.byContains(TEST_TABLE), "Delete query not constructed correctly");
}
@Test
@DisplayName("byContains fails | SQLite")
public void byContainsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> DeleteQuery.byContains(TEST_TABLE));
}
@Test
@DisplayName("byJsonPath generates correctly | PostgreSQL")
public void byJsonPathPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("DELETE FROM %s WHERE jsonb_path_exists(data, :path::jsonpath)", TEST_TABLE),
DeleteQuery.byJsonPath(TEST_TABLE), "Delete query not constructed correctly");
}
@Test
@DisplayName("byJsonPath fails | SQLite")
public void byJsonPathSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> DeleteQuery.byJsonPath(TEST_TABLE));
}
}

View File

@ -0,0 +1,43 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.Dialect;
import solutions.bitbadger.documents.DocumentException;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the `Dialect` enum
*/
@DisplayName("Core | Java | Dialect")
final public class DialectTest {
@Test
@DisplayName("deriveFromConnectionString derives PostgreSQL correctly")
public void derivesPostgres() throws DocumentException {
assertEquals(Dialect.POSTGRESQL, Dialect.deriveFromConnectionString("jdbc:postgresql:db"),
"Dialect should have been PostgreSQL");
}
@Test
@DisplayName("deriveFromConnectionString derives SQLite correctly")
public void derivesSQLite() throws DocumentException {
assertEquals(
Dialect.SQLITE, Dialect.deriveFromConnectionString("jdbc:sqlite:memory"),
"Dialect should have been SQLite");
}
@Test
@DisplayName("deriveFromConnectionString fails when the connection string is unknown")
public void deriveFailsWhenUnknown() {
try {
Dialect.deriveFromConnectionString("SQL Server");
fail("Dialect derivation should have failed");
} catch (DocumentException ex) {
assertNotNull(ex.getMessage(), "The exception message should not have been null");
assertTrue(ex.getMessage().contains("[SQL Server]"),
"The connection string should have been in the exception message");
}
}
}

View File

@ -0,0 +1,26 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.DocumentIndex;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the `DocumentIndex` enum
*/
@DisplayName("Core | Java | DocumentIndex")
final public class DocumentIndexTest {
@Test
@DisplayName("FULL uses proper SQL")
public void fullSQL() {
assertEquals("", DocumentIndex.FULL.getSql(), "The SQL for Full is incorrect");
}
@Test
@DisplayName("OPTIMIZED uses proper SQL")
public void optimizedSQL() {
assertEquals(" jsonb_path_ops", DocumentIndex.OPTIMIZED.getSql(), "The SQL for Optimized is incorrect");
}
}

View File

@ -0,0 +1,134 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.AutoId;
import solutions.bitbadger.documents.Configuration;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.query.DocumentQuery;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import static org.junit.jupiter.api.Assertions.*;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
/**
* Unit tests for the `Document` object
*/
@DisplayName("Core | Java | Query | DocumentQuery")
final public class DocumentQueryTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("insert generates no auto ID | PostgreSQL")
public void insertNoAutoPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("INSERT INTO %s VALUES (:data)", TEST_TABLE), DocumentQuery.insert(TEST_TABLE));
}
@Test
@DisplayName("insert generates no auto ID | SQLite")
public void insertNoAutoSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("INSERT INTO %s VALUES (:data)", TEST_TABLE), DocumentQuery.insert(TEST_TABLE));
}
@Test
@DisplayName("insert generates auto number | PostgreSQL")
public void insertAutoNumberPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("INSERT INTO %1$s VALUES (:data::jsonb || ('{\"id\":' "
+ "|| (SELECT COALESCE(MAX((data->>'id')::numeric), 0) + 1 FROM %1$s) || '}')::jsonb)",
TEST_TABLE),
DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER));
}
@Test
@DisplayName("insert generates auto number | SQLite")
public void insertAutoNumberSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("INSERT INTO %1$s VALUES (json_set(:data, '$.id', "
+ "(SELECT coalesce(max(data->>'id'), 0) + 1 FROM %1$s)))", TEST_TABLE),
DocumentQuery.insert(TEST_TABLE, AutoId.NUMBER));
}
@Test
@DisplayName("insert generates auto UUID | PostgreSQL")
public void insertAutoUUIDPostgres() throws DocumentException {
ForceDialect.postgres();
final String query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID);
assertTrue(query.startsWith(String.format("INSERT INTO %s VALUES (:data::jsonb || '{\"id\":\"", TEST_TABLE)),
String.format("Query start not correct (actual: %s)", query));
assertTrue(query.endsWith("\"}')"), "Query end not correct");
}
@Test
@DisplayName("insert generates auto UUID | SQLite")
public void insertAutoUUIDSQLite() throws DocumentException {
ForceDialect.sqlite();
final String query = DocumentQuery.insert(TEST_TABLE, AutoId.UUID);
assertTrue(query.startsWith(String.format("INSERT INTO %s VALUES (json_set(:data, '$.id', '", TEST_TABLE)),
String.format("Query start not correct (actual: %s)", query));
assertTrue(query.endsWith("'))"), "Query end not correct");
}
@Test
@DisplayName("insert generates auto random string | PostgreSQL")
public void insertAutoRandomPostgres() throws DocumentException {
try {
ForceDialect.postgres();
Configuration.idStringLength = 8;
final String query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING);
final String start = String.format("INSERT INTO %s VALUES (:data::jsonb || '{\"id\":\"", TEST_TABLE);
final String end = "\"}')";
assertTrue(query.startsWith(start), String.format("Query start not correct (actual: %s)", query));
assertTrue(query.endsWith(end), "Query end not correct");
assertEquals(8, query.replace(start, "").replace(end, "").length(), "Random string length incorrect");
} finally {
Configuration.idStringLength = 16;
}
}
@Test
@DisplayName("insert generates auto random string | SQLite")
public void insertAutoRandomSQLite() throws DocumentException {
ForceDialect.sqlite();
final String query = DocumentQuery.insert(TEST_TABLE, AutoId.RANDOM_STRING);
final String start = String.format("INSERT INTO %s VALUES (json_set(:data, '$.id', '", TEST_TABLE);
final String end = "'))";
assertTrue(query.startsWith(start), String.format("Query start not correct (actual: %s)", query));
assertTrue(query.endsWith(end), "Query end not correct");
assertEquals(Configuration.idStringLength, query.replace(start, "").replace(end, "").length(),
"Random string length incorrect");
}
@Test
@DisplayName("insert fails when no dialect is set")
public void insertFailsUnknown() {
assertThrows(DocumentException.class, () -> DocumentQuery.insert(TEST_TABLE));
}
@Test
@DisplayName("save generates correctly")
public void save() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format(
"INSERT INTO %s VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data",
TEST_TABLE),
DocumentQuery.save(TEST_TABLE), "INSERT ON CONFLICT UPDATE statement not constructed correctly");
}
@Test
@DisplayName("update generates successfully")
public void update() {
assertEquals(String.format("UPDATE %s SET data = :data", TEST_TABLE), DocumentQuery.update(TEST_TABLE),
"Update query not constructed correctly");
}
}

View File

@ -0,0 +1,97 @@
package solutions.bitbadger.documents.core.tests.java;
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.query.ExistsQuery;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
/**
* Unit tests for the `Exists` object
*/
@DisplayName("Core | Java | Query | ExistsQuery")
final public class ExistsQueryTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("byId generates correctly | PostgreSQL")
public void byIdPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("SELECT EXISTS (SELECT 1 FROM %s WHERE data->>'id' = :id) AS it", TEST_TABLE),
ExistsQuery.byId(TEST_TABLE), "Exists query not constructed correctly");
}
@Test
@DisplayName("byId generates correctly | SQLite")
public void byIdSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("SELECT EXISTS (SELECT 1 FROM %s WHERE data->>'id' = :id) AS it", TEST_TABLE),
ExistsQuery.byId(TEST_TABLE), "Exists query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | PostgreSQL")
public void byFieldsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format(
"SELECT EXISTS (SELECT 1 FROM %s WHERE (data->>'it')::numeric = :test) AS it", TEST_TABLE),
ExistsQuery.byFields(TEST_TABLE, List.of(Field.equal("it", 7, ":test"))),
"Exists query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | SQLite")
public void byFieldsSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("SELECT EXISTS (SELECT 1 FROM %s WHERE data->>'it' = :test) AS it", TEST_TABLE),
ExistsQuery.byFields(TEST_TABLE, List.of(Field.equal("it", 7, ":test"))),
"Exists query not constructed correctly");
}
@Test
@DisplayName("byContains generates correctly | PostgreSQL")
public void byContainsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("SELECT EXISTS (SELECT 1 FROM %s WHERE data @> :criteria) AS it", TEST_TABLE),
ExistsQuery.byContains(TEST_TABLE), "Exists query not constructed correctly");
}
@Test
@DisplayName("byContains fails | SQLite")
public void byContainsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> ExistsQuery.byContains(TEST_TABLE));
}
@Test
@DisplayName("byJsonPath generates correctly | PostgreSQL")
public void byJsonPathPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format(
"SELECT EXISTS (SELECT 1 FROM %s WHERE jsonb_path_exists(data, :path::jsonpath)) AS it",
TEST_TABLE),
ExistsQuery.byJsonPath(TEST_TABLE), "Exists query not constructed correctly");
}
@Test
@DisplayName("byJsonPath fails | SQLite")
public void byJsonPathSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> ExistsQuery.byJsonPath(TEST_TABLE));
}
}

View File

@ -0,0 +1,26 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.FieldMatch;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the `FieldMatch` enum
*/
@DisplayName("Core | Java | FieldMatch")
final public class FieldMatchTest {
@Test
@DisplayName("ANY uses proper SQL")
public void any() {
assertEquals("OR", FieldMatch.ANY.getSql(), "ANY should use OR");
}
@Test
@DisplayName("ALL uses proper SQL")
public void all() {
assertEquals("AND", FieldMatch.ALL.getSql(), "ALL should use AND");
}
}

View File

@ -0,0 +1,636 @@
package solutions.bitbadger.documents.core.tests.java;
import kotlin.Pair;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.*;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the `Field` class
*/
@DisplayName("Core | Java | Field")
final public class FieldTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
// ~~~ INSTANCE METHODS ~~~
@Test
@DisplayName("withParameterName fails for invalid name")
public void withParamNameFails() {
assertThrows(DocumentException.class, () -> Field.equal("it", "").withParameterName("2424"));
}
@Test
@DisplayName("withParameterName works with colon prefix")
public void withParamNameColon() {
Field<String> field = Field.equal("abc", "22").withQualifier("me");
Field<String> withParam = field.withParameterName(":test");
assertNotSame(field, withParam, "A new Field instance should have been created");
assertEquals(field.getName(), withParam.getName(), "Name should have been preserved");
assertEquals(field.getComparison(), withParam.getComparison(), "Comparison should have been preserved");
assertEquals(":test", withParam.getParameterName(), "Parameter name not set correctly");
assertEquals(field.getQualifier(), withParam.getQualifier(), "Qualifier should have been preserved");
}
@Test
@DisplayName("withParameterName works with at-sign prefix")
public void withParamNameAtSign() {
Field<String> field = Field.equal("def", "44");
Field<String> withParam = field.withParameterName("@unit");
assertNotSame(field, withParam, "A new Field instance should have been created");
assertEquals(field.getName(), withParam.getName(), "Name should have been preserved");
assertEquals(field.getComparison(), withParam.getComparison(), "Comparison should have been preserved");
assertEquals("@unit", withParam.getParameterName(), "Parameter name not set correctly");
assertEquals(field.getQualifier(), withParam.getQualifier(), "Qualifier should have been preserved");
}
@Test
@DisplayName("withQualifier sets qualifier correctly")
public void withQualifier() {
Field<String> field = Field.equal("j", "k");
Field<String> withQual = field.withQualifier("test");
assertNotSame(field, withQual, "A new Field instance should have been created");
assertEquals(field.getName(), withQual.getName(), "Name should have been preserved");
assertEquals(field.getComparison(), withQual.getComparison(), "Comparison should have been preserved");
assertEquals(field.getParameterName(), withQual.getParameterName(),
"Parameter Name should have been preserved");
assertEquals("test", withQual.getQualifier(), "Qualifier not set correctly");
}
@Test
@DisplayName("path generates for simple unqualified field | PostgreSQL")
public void pathPostgresSimpleUnqualified() {
assertEquals("data->>'SomethingCool'",
Field.greaterOrEqual("SomethingCool", 18).path(Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not correct");
}
@Test
@DisplayName("path generates for simple qualified field | PostgreSQL")
public void 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 field | PostgreSQL")
public void 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 field | PostgreSQL")
public void 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 field | SQLite")
public void pathSQLiteSimpleUnqualified() {
assertEquals("data->>'SomethingCool'",
Field.greaterOrEqual("SomethingCool", 18).path(Dialect.SQLITE, FieldFormat.SQL), "Path not correct");
}
@Test
@DisplayName("path generates for simple qualified field | SQLite")
public void 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 field | SQLite")
public void 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 field | SQLite")
public void pathSQLiteNestedQualified() {
assertEquals("bird.data->'Nest'->>'Away'",
Field.equal("Nest.Away", "doc").withQualifier("bird").path(Dialect.SQLITE, FieldFormat.SQL),
"Path not correct");
}
@Test
@DisplayName("toWhere generates for exists w/o qualifier | PostgreSQL")
public void toWhereExistsNoQualPostgres() throws DocumentException {
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")
public void toWhereExistsNoQualSQLite() throws DocumentException {
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")
public void toWhereNotExistsNoQualPostgres() throws DocumentException {
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")
public void toWhereNotExistsNoQualSQLite() throws DocumentException {
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")
public void toWhereBetweenNoQualNumericPostgres() throws DocumentException {
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")
public void toWhereBetweenNoQualAlphaPostgres() throws DocumentException {
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")
public void toWhereBetweenNoQualSQLite() throws DocumentException {
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")
public void toWhereBetweenQualNumericPostgres() throws DocumentException {
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")
public void toWhereBetweenQualAlphaPostgres() throws DocumentException {
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")
public void toWhereBetweenQualSQLite() throws DocumentException {
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")
public void toWhereAnyNumericPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("(data->>'even')::numeric IN (:nbr_0, :nbr_1, :nbr_2)",
Field.any("even", List.of(2, 4, 6), ":nbr").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for IN/any, alphanumeric values | PostgreSQL")
public void toWhereAnyAlphaPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data->>'test' IN (:city_0, :city_1)",
Field.any("test", List.of("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for IN/any | SQLite")
public void toWhereAnySQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("data->>'test' IN (:city_0, :city_1)",
Field.any("test", List.of("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for inArray | PostgreSQL")
public void toWhereInArrayPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("data->'even' ??| ARRAY[:it_0, :it_1, :it_2, :it_3]",
Field.inArray("even", "tbl", List.of(2, 4, 6, 8), ":it").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for inArray | SQLite")
public void toWhereInArraySQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("EXISTS (SELECT 1 FROM json_each(tbl.data, '$.test') WHERE value IN (:city_0, :city_1))",
Field.inArray("test", "tbl", List.of("Atlanta", "Chicago"), ":city").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere generates for others w/o qualifier | PostgreSQL")
public void toWhereOtherNoQualPostgres() throws DocumentException {
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")
public void toWhereOtherNoQualSQLite() throws DocumentException {
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")
public void toWhereNoParamWithQualPostgres() throws DocumentException {
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")
public void toWhereNoParamWithQualSQLite() throws DocumentException {
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")
public void toWhereParamWithQualPostgres() throws DocumentException {
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")
public void toWhereParamWithQualSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("q.data->>'le_field' <= :it",
Field.lessOrEqual("le_field", 18, ":it").withQualifier("q").toWhere(),
"Field WHERE clause not generated correctly");
}
@Test
@DisplayName("toWhere fails when dialect is not set")
public void toWhereFailsNoDialect() {
assertThrows(DocumentException.class, () -> Field.equal("a", "oops").toWhere());
}
// ~~~ STATIC TESTS ~~~
@Test
@DisplayName("equal constructs a field w/o parameter name")
public void equalCtor() {
final Field<Integer> field = Field.equal("Test", 14);
assertEquals("Test", field.getName(), "Field name not filled correctly");
assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(14, field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("equal constructs a field w/ parameter name")
public void equalParameterCtor() {
final Field<Integer> field = Field.equal("Test", 14, ":w");
assertEquals("Test", field.getName(), "Field name not filled correctly");
assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(14, field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":w", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("greater constructs a field w/o parameter name")
public void greaterCtor() {
final Field<String> field = Field.greater("Great", "night");
assertEquals("Great", field.getName(), "Field name not filled correctly");
assertEquals(Op.GREATER, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("night", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("greater constructs a field w/ parameter name")
public void greaterParameterCtor() {
final Field<String> field = Field.greater("Great", "night", ":yeah");
assertEquals("Great", field.getName(), "Field name not filled correctly");
assertEquals(Op.GREATER, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("night", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":yeah", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("greaterOrEqual constructs a field w/o parameter name")
public void greaterOrEqualCtor() {
final Field<Long> field = Field.greaterOrEqual("Nice", 88L);
assertEquals("Nice", field.getName(), "Field name not filled correctly");
assertEquals(Op.GREATER_OR_EQUAL, field.getComparison().getOp(),
"Field comparison operation not filled correctly");
assertEquals(88L, field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("greaterOrEqual constructs a field w/ parameter name")
public void greaterOrEqualParameterCtor() {
final Field<Long> field = Field.greaterOrEqual("Nice", 88L, ":nice");
assertEquals("Nice", field.getName(), "Field name not filled correctly");
assertEquals(Op.GREATER_OR_EQUAL, field.getComparison().getOp(),
"Field comparison operation not filled correctly");
assertEquals(88L, field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":nice", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("less constructs a field w/o parameter name")
public void lessCtor() {
final Field<String> field = Field.less("Lesser", "seven");
assertEquals("Lesser", field.getName(), "Field name not filled correctly");
assertEquals(Op.LESS, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("seven", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("less constructs a field w/ parameter name")
public void lessParameterCtor() {
final Field<String> field = Field.less("Lesser", "seven", ":max");
assertEquals("Lesser", field.getName(), "Field name not filled correctly");
assertEquals(Op.LESS, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("seven", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":max", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("lessOrEqual constructs a field w/o parameter name")
public void lessOrEqualCtor() {
final Field<String> field = Field.lessOrEqual("Nobody", "KNOWS");
assertEquals("Nobody", field.getName(), "Field name not filled correctly");
assertEquals(Op.LESS_OR_EQUAL, field.getComparison().getOp(),
"Field comparison operation not filled correctly");
assertEquals("KNOWS", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("lessOrEqual constructs a field w/ parameter name")
public void lessOrEqualParameterCtor() {
final Field<String> field = Field.lessOrEqual("Nobody", "KNOWS", ":nope");
assertEquals("Nobody", field.getName(), "Field name not filled correctly");
assertEquals(Op.LESS_OR_EQUAL, field.getComparison().getOp(),
"Field comparison operation not filled correctly");
assertEquals("KNOWS", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":nope", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("notEqual constructs a field w/o parameter name")
public void notEqualCtor() {
final Field<String> field = Field.notEqual("Park", "here");
assertEquals("Park", field.getName(), "Field name not filled correctly");
assertEquals(Op.NOT_EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("here", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("notEqual constructs a field w/ parameter name")
public void notEqualParameterCtor() {
final Field<String> field = Field.notEqual("Park", "here", ":now");
assertEquals("Park", field.getName(), "Field name not filled correctly");
assertEquals(Op.NOT_EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("here", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertEquals(":now", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("between constructs a field w/o parameter name")
public void betweenCtor() {
final Field<Pair<Integer, Integer>> field = Field.between("Age", 18, 49);
assertEquals("Age", field.getName(), "Field name not filled correctly");
assertEquals(Op.BETWEEN, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(18, field.getComparison().getValue().getFirst(),
"Field comparison min value not filled correctly");
assertEquals(49, field.getComparison().getValue().getSecond(),
"Field comparison max value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("between constructs a field w/ parameter name")
public void betweenParameterCtor() {
final Field<Pair<Integer, Integer>> field = Field.between("Age", 18, 49, ":limit");
assertEquals("Age", field.getName(), "Field name not filled correctly");
assertEquals(Op.BETWEEN, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(18, field.getComparison().getValue().getFirst(),
"Field comparison min value not filled correctly");
assertEquals(49, field.getComparison().getValue().getSecond(),
"Field comparison max value not filled correctly");
assertEquals(":limit", field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("any constructs a field w/o parameter name")
public void anyCtor() {
final Field<Collection<Integer>> field = Field.any("Here", List.of(8, 16, 32));
assertEquals("Here", field.getName(), "Field name not filled correctly");
assertEquals(Op.IN, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(List.of(8, 16, 32), field.getComparison().getValue(),
"Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("any constructs a field w/ parameter name")
public void anyParameterCtor() {
final Field<Collection<Integer>> field = Field.any("Here", List.of(8, 16, 32), ":list");
assertEquals("Here", field.getName(), "Field name not filled correctly");
assertEquals(Op.IN, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals(List.of(8, 16, 32), field.getComparison().getValue(),
"Field comparison value not filled correctly");
assertEquals(":list", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("inArray constructs a field w/o parameter name")
public void inArrayCtor() {
final Field<Pair<String, Collection<String>>> field = Field.inArray("ArrayField", "table", List.of("z"));
assertEquals("ArrayField", field.getName(), "Field name not filled correctly");
assertEquals(Op.IN_ARRAY, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("table", field.getComparison().getValue().getFirst(),
"Field comparison table not filled correctly");
assertEquals(List.of("z"), field.getComparison().getValue().getSecond(),
"Field comparison values not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("inArray constructs a field w/ parameter name")
public void inArrayParameterCtor() {
final Field<Pair<String, Collection<String>>> field = Field.inArray("ArrayField", "table", List.of("z"), ":a");
assertEquals("ArrayField", field.getName(), "Field name not filled correctly");
assertEquals(Op.IN_ARRAY, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("table", field.getComparison().getValue().getFirst(),
"Field comparison table not filled correctly");
assertEquals(List.of("z"), field.getComparison().getValue().getSecond(),
"Field comparison values not filled correctly");
assertEquals(":a", field.getParameterName(), "Field parameter name not filled correctly");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("exists constructs a field")
public void existsCtor() {
final Field<String> field = Field.exists("Groovy");
assertEquals("Groovy", field.getName(), "Field name not filled correctly");
assertEquals(Op.EXISTS, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("notExists constructs a field")
public void notExistsCtor() {
final Field<String> field = Field.notExists("Groovy");
assertEquals("Groovy", field.getName(), "Field name not filled correctly");
assertEquals(Op.NOT_EXISTS, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("named constructs a field")
public void namedCtor() {
final Field<String> field = Field.named("Tacos");
assertEquals("Tacos", field.getName(), "Field name not filled correctly");
assertEquals(Op.EQUAL, field.getComparison().getOp(), "Field comparison operation not filled correctly");
assertEquals("", field.getComparison().getValue(), "Field comparison value not filled correctly");
assertNull(field.getParameterName(), "The parameter name should have been null");
assertNull(field.getQualifier(), "The qualifier should have been null");
}
@Test
@DisplayName("static constructors fail for invalid parameter name")
public void staticCtorsFailOnParamName() {
assertThrows(DocumentException.class, () -> Field.equal("a", "b", "that ain't it, Jack..."));
}
@Test
@DisplayName("nameToPath creates a simple PostgreSQL SQL name")
public void nameToPathPostgresSimpleSQL() {
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.SQL),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a simple SQLite SQL name")
public void nameToPathSQLiteSimpleSQL() {
assertEquals("data->>'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.SQL),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a nested PostgreSQL SQL name")
public void 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")
public void 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")
public void nameToPathPostgresSimpleJSON() {
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.POSTGRESQL, FieldFormat.JSON),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a simple SQLite JSON name")
public void nameToPathSQLiteSimpleJSON() {
assertEquals("data->'Simple'", Field.nameToPath("Simple", Dialect.SQLITE, FieldFormat.JSON),
"Path not constructed correctly");
}
@Test
@DisplayName("nameToPath creates a nested PostgreSQL JSON name")
public void 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")
public void 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");
}
}

View File

@ -0,0 +1,102 @@
package solutions.bitbadger.documents.core.tests.java;
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.query.FindQuery;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
/**
* Unit tests for the `Find` object
*/
@DisplayName("Core | Java | Query | FindQuery")
final public class FindQueryTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("all generates correctly")
public void all() {
assertEquals(String.format("SELECT data FROM %s", TEST_TABLE), FindQuery.all(TEST_TABLE),
"Find query not constructed correctly");
}
@Test
@DisplayName("byId generates correctly | PostgreSQL")
public void byIdPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("SELECT data FROM %s WHERE data->>'id' = :id", TEST_TABLE),
FindQuery.byId(TEST_TABLE), "Find query not constructed correctly");
}
@Test
@DisplayName("byId generates correctly | SQLite")
public void byIdSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("SELECT data FROM %s WHERE data->>'id' = :id", TEST_TABLE),
FindQuery.byId(TEST_TABLE), "Find query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | PostgreSQL")
public void byFieldsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(
String.format("SELECT data FROM %s WHERE data->>'a' = :b AND (data->>'c')::numeric < :d", TEST_TABLE),
FindQuery.byFields(TEST_TABLE, List.of(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))),
"Find query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | SQLite")
public void byFieldsSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("SELECT data FROM %s WHERE data->>'a' = :b AND data->>'c' < :d", TEST_TABLE),
FindQuery.byFields(TEST_TABLE, List.of(Field.equal("a", "", ":b"), Field.less("c", 14, ":d"))),
"Find query not constructed correctly");
}
@Test
@DisplayName("byContains generates correctly | PostgreSQL")
public void byContainsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("SELECT data FROM %s WHERE data @> :criteria", TEST_TABLE),
FindQuery.byContains(TEST_TABLE), "Find query not constructed correctly");
}
@Test
@DisplayName("byContains fails | SQLite")
public void byContainsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> FindQuery.byContains(TEST_TABLE));
}
@Test
@DisplayName("byJsonPath generates correctly | PostgreSQL")
public void byJsonPathPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("SELECT data FROM %s WHERE jsonb_path_exists(data, :path::jsonpath)", TEST_TABLE),
FindQuery.byJsonPath(TEST_TABLE), "Find query not constructed correctly");
}
@Test
@DisplayName("byJsonPath fails | SQLite")
public void byJsonPathSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> FindQuery.byJsonPath(TEST_TABLE));
}
}

View File

@ -0,0 +1,18 @@
package solutions.bitbadger.documents.core.tests.java;
public class IntIdClass {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public IntIdClass(int id) {
this.id = id;
}
}

View File

@ -0,0 +1,18 @@
package solutions.bitbadger.documents.core.tests.java;
public class LongIdClass {
private long id;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public LongIdClass(long id) {
this.id = id;
}
}

View File

@ -0,0 +1,80 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.Op;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the `Op` enum
*/
@DisplayName("Core | Java | Op")
final public class OpTest {
@Test
@DisplayName("EQUAL uses proper SQL")
public void equalSQL() {
assertEquals("=", Op.EQUAL.getSql(), "The SQL for equal is incorrect");
}
@Test
@DisplayName("GREATER uses proper SQL")
public void greaterSQL() {
assertEquals(">", Op.GREATER.getSql(), "The SQL for greater is incorrect");
}
@Test
@DisplayName("GREATER_OR_EQUAL uses proper SQL")
public void greaterOrEqualSQL() {
assertEquals(">=", Op.GREATER_OR_EQUAL.getSql(), "The SQL for greater-or-equal is incorrect");
}
@Test
@DisplayName("LESS uses proper SQL")
public void lessSQL() {
assertEquals("<", Op.LESS.getSql(), "The SQL for less is incorrect");
}
@Test
@DisplayName("LESS_OR_EQUAL uses proper SQL")
public void lessOrEqualSQL() {
assertEquals("<=", Op.LESS_OR_EQUAL.getSql(), "The SQL for less-or-equal is incorrect");
}
@Test
@DisplayName("NOT_EQUAL uses proper SQL")
public void notEqualSQL() {
assertEquals("<>", Op.NOT_EQUAL.getSql(), "The SQL for not-equal is incorrect");
}
@Test
@DisplayName("BETWEEN uses proper SQL")
public void betweenSQL() {
assertEquals("BETWEEN", Op.BETWEEN.getSql(), "The SQL for between is incorrect");
}
@Test
@DisplayName("IN uses proper SQL")
public void inSQL() {
assertEquals("IN", Op.IN.getSql(), "The SQL for in is incorrect");
}
@Test
@DisplayName("IN_ARRAY uses proper SQL")
public void inArraySQL() {
assertEquals("??|", Op.IN_ARRAY.getSql(), "The SQL for in-array is incorrect");
}
@Test
@DisplayName("EXISTS uses proper SQL")
public void existsSQL() {
assertEquals("IS NOT NULL", Op.EXISTS.getSql(), "The SQL for exists is incorrect");
}
@Test
@DisplayName("NOT_EXISTS uses proper SQL")
public void notExistsSQL() {
assertEquals("IS NULL", Op.NOT_EXISTS.getSql(), "The SQL for not-exists is incorrect");
}
}

View File

@ -0,0 +1,32 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.ParameterName;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the `ParameterName` class
*/
@DisplayName("Core | Java | ParameterName")
final public class ParameterNameTest {
@Test
@DisplayName("derive works when given existing names")
public void withExisting() {
ParameterName names = new 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")
public void allAnonymous() {
ParameterName names = new 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");
}
}

View File

@ -0,0 +1,40 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.Parameter;
import solutions.bitbadger.documents.ParameterType;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the `Parameter` class
*/
@DisplayName("Core | Java | Parameter")
final public class ParameterTest {
@Test
@DisplayName("Construction with colon-prefixed name")
public void ctorWithColon() {
Parameter<String> p = new Parameter<>(":test", ParameterType.STRING, "ABC");
assertEquals(":test", p.getName(), "Parameter name was incorrect");
assertEquals(ParameterType.STRING, p.getType(), "Parameter type was incorrect");
assertEquals("ABC", p.getValue(), "Parameter value was incorrect");
}
@Test
@DisplayName("Construction with at-sign-prefixed name")
public void ctorWithAtSign() {
Parameter<String> p = new Parameter<>("@yo", ParameterType.NUMBER, null);
assertEquals("@yo", p.getName(), "Parameter name was incorrect");
assertEquals(ParameterType.NUMBER, p.getType(), "Parameter type was incorrect");
assertNull(p.getValue(), "Parameter value was incorrect");
}
@Test
@DisplayName("Construction fails with incorrect prefix")
public void ctorFailsForPrefix() {
assertThrows(DocumentException.class, () -> new Parameter<>("it", ParameterType.JSON, ""));
}
}

View File

@ -0,0 +1,121 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.*;
import solutions.bitbadger.documents.java.Parameters;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the `Parameters` object
*/
@DisplayName("Core | Java | Parameters")
final public class ParametersTest {
/**
* Reset the dialect
*/
@AfterEach
public void cleanUp() {
Configuration.setConnectionString(null);
}
@Test
@DisplayName("nameFields works with no changes")
public void nameFieldsNoChange() {
List<Field<?>> fields = List.of(Field.equal("a", "", ":test"), Field.exists("q"), Field.equal("b", "", ":me"));
Field<?>[] named = Parameters.nameFields(fields).toArray(new Field<?>[] { });
assertEquals(fields.size(), named.length, "There should have been 3 fields in the list");
assertSame(fields.get(0), named[0], "The first field should be the same");
assertSame(fields.get(1), named[1], "The second field should be the same");
assertSame(fields.get(2), named[2], "The third field should be the same");
}
@Test
@DisplayName("nameFields works when changing fields")
public void nameFieldsChange() {
List<Field<?>> fields = List.of(
Field.equal("a", ""), Field.equal("e", "", ":hi"), Field.equal("b", ""), Field.notExists("z"));
Field<?>[] named = Parameters.nameFields(fields).toArray(new Field<?>[] { });
assertEquals(fields.size(), named.length, "There should have been 4 fields in the list");
assertNotSame(fields.get(0), named[0], "The first field should not be the same");
assertEquals(":field0", named[0].getParameterName(), "First parameter name incorrect");
assertSame(fields.get(1), named[1], "The second field should be the same");
assertNotSame(fields.get(2), named[2], "The third field should not be the same");
assertEquals(":field1", named[2].getParameterName(), "Third parameter name incorrect");
assertSame(fields.get(3), named[3], "The fourth field should be the same");
}
@Test
@DisplayName("replaceNamesInQuery replaces successfully")
public void replaceNamesInQuery() {
List<Parameter<?>> parameters = List.of(new Parameter<>(":data", ParameterType.JSON, "{}"),
new Parameter<>(":data_ext", ParameterType.STRING, ""));
String query =
"SELECT data, data_ext FROM tbl WHERE data = :data AND data_ext = :data_ext AND more_data = :data";
assertEquals("SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?",
Parameters.replaceNamesInQuery(query, parameters), "Parameters not replaced correctly");
}
@Test
@DisplayName("fieldNames generates a single parameter (PostgreSQL)")
public void fieldNamesSinglePostgres() throws DocumentException {
Configuration.setConnectionString(":postgresql:");
Parameter<?>[] nameParams = Parameters.fieldNames(List.of("test")).toArray(new Parameter<?>[]{});
assertEquals(1, nameParams.length, "There should be one name parameter");
assertEquals(":name", nameParams[0].getName(), "The parameter name is incorrect");
assertEquals(ParameterType.STRING, nameParams[0].getType(), "The parameter type is incorrect");
assertEquals("{test}", nameParams[0].getValue(), "The parameter value is incorrect");
}
@Test
@DisplayName("fieldNames generates multiple parameters (PostgreSQL)")
public void fieldNamesMultiplePostgres() throws DocumentException {
Configuration.setConnectionString(":postgresql:");
Parameter<?>[] nameParams = Parameters.fieldNames(List.of("test", "this", "today"))
.toArray(new Parameter<?>[]{});
assertEquals(1, nameParams.length, "There should be one name parameter");
assertEquals(":name", nameParams[0].getName(), "The parameter name is incorrect");
assertEquals(ParameterType.STRING, nameParams[0].getType(), "The parameter type is incorrect");
assertEquals("{test,this,today}", nameParams[0].getValue(), "The parameter value is incorrect");
}
@Test
@DisplayName("fieldNames generates a single parameter (SQLite)")
public void fieldNamesSingleSQLite() throws DocumentException {
Configuration.setConnectionString(":sqlite:");
Parameter<?>[] nameParams = Parameters.fieldNames(List.of("test")).toArray(new Parameter<?>[]{});
assertEquals(1, nameParams.length, "There should be one name parameter");
assertEquals(":name0", nameParams[0].getName(), "The parameter name is incorrect");
assertEquals(ParameterType.STRING, nameParams[0].getType(), "The parameter type is incorrect");
assertEquals("test", nameParams[0].getValue(), "The parameter value is incorrect");
}
@Test
@DisplayName("fieldNames generates multiple parameters (SQLite)")
public void fieldNamesMultipleSQLite() throws DocumentException {
Configuration.setConnectionString(":sqlite:");
Parameter<?>[] nameParams = Parameters.fieldNames(List.of("test", "this", "today"))
.toArray(new Parameter<?>[]{});
assertEquals(3, nameParams.length, "There should be one name parameter");
assertEquals(":name0", nameParams[0].getName(), "The first parameter name is incorrect");
assertEquals(ParameterType.STRING, nameParams[0].getType(), "The first parameter type is incorrect");
assertEquals("test", nameParams[0].getValue(), "The first parameter value is incorrect");
assertEquals(":name1", nameParams[1].getName(), "The second parameter name is incorrect");
assertEquals(ParameterType.STRING, nameParams[1].getType(), "The second parameter type is incorrect");
assertEquals("this", nameParams[1].getValue(), "The second parameter value is incorrect");
assertEquals(":name2", nameParams[2].getName(), "The third parameter name is incorrect");
assertEquals(ParameterType.STRING, nameParams[2].getType(), "The third parameter type is incorrect");
assertEquals("today", nameParams[2].getValue(), "The third parameter value is incorrect");
}
@Test
@DisplayName("fieldNames fails if dialect not set")
public void fieldNamesFails() {
assertThrows(DocumentException.class, () -> Parameters.fieldNames(List.of()));
}
}

View File

@ -0,0 +1,97 @@
package solutions.bitbadger.documents.core.tests.java;
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.query.PatchQuery;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
/**
* Unit tests for the `Patch` object
*/
@DisplayName("Core | Java | Query | PatchQuery")
final public class PatchQueryTest {
/**
* Reset the dialect
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("byId generates correctly | PostgreSQL")
public void byIdPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("UPDATE %s SET data = data || :data WHERE data->>'id' = :id", TEST_TABLE),
PatchQuery.byId(TEST_TABLE), "Patch query not constructed correctly");
}
@Test
@DisplayName("byId generates correctly | SQLite")
public void byIdSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format(
"UPDATE %s SET data = json_patch(data, json(:data)) WHERE data->>'id' = :id", TEST_TABLE),
PatchQuery.byId(TEST_TABLE), "Patch query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | PostgreSQL")
public void byFieldsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("UPDATE %s SET data = data || :data WHERE data->>'z' = :y", TEST_TABLE),
PatchQuery.byFields(TEST_TABLE, List.of(Field.equal("z", "", ":y"))),
"Patch query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | SQLite")
public void byFieldsSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format(
"UPDATE %s SET data = json_patch(data, json(:data)) WHERE data->>'z' = :y", TEST_TABLE),
PatchQuery.byFields(TEST_TABLE, List.of(Field.equal("z", "", ":y"))),
"Patch query not constructed correctly");
}
@Test
@DisplayName("byContains generates correctly | PostgreSQL")
public void byContainsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("UPDATE %s SET data = data || :data WHERE data @> :criteria", TEST_TABLE),
PatchQuery.byContains(TEST_TABLE), "Patch query not constructed correctly");
}
@Test
@DisplayName("byContains fails | SQLite")
public void byContainsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> PatchQuery.byContains(TEST_TABLE));
}
@Test
@DisplayName("byJsonPath generates correctly | PostgreSQL")
public void byJsonPathPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("UPDATE %s SET data = data || :data WHERE jsonb_path_exists(data, :path::jsonpath)",
TEST_TABLE),
PatchQuery.byJsonPath(TEST_TABLE), "Patch query not constructed correctly");
}
@Test
@DisplayName("byJsonPath fails | SQLite")
public void byJsonPathSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> PatchQuery.byJsonPath(TEST_TABLE));
}
}

View File

@ -0,0 +1,166 @@
package solutions.bitbadger.documents.core.tests.java;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import solutions.bitbadger.documents.Dialect;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.Field;
import solutions.bitbadger.documents.FieldMatch;
import solutions.bitbadger.documents.query.QueryUtils;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Unit tests for the package-level query functions, presented as `QueryUtils` for Java-compatible use
*/
@DisplayName("Core | Java | Query | QueryUtils")
final public class QueryUtilsTest {
/**
* Clear the connection string (resets Dialect)
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("statementWhere generates correctly")
public void statementWhere() {
assertEquals("x WHERE y", QueryUtils.statementWhere("x", "y"), "Statements not combined correctly");
}
@Test
@DisplayName("byId generates a numeric ID query | PostgreSQL")
public void byIdNumericPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("test WHERE (data->>'id')::numeric = :id", QueryUtils.byId("test", 9));
}
@Test
@DisplayName("byId generates an alphanumeric ID query | PostgreSQL")
public void byIdAlphaPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("unit WHERE data->>'id' = :id", QueryUtils.byId("unit", "18"));
}
@Test
@DisplayName("byId generates ID query | SQLite")
public void byIdSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("yo WHERE data->>'id' = :id", QueryUtils.byId("yo", 27));
}
@Test
@DisplayName("byFields generates default field query | PostgreSQL")
public void byFieldsMultipleDefaultPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("this WHERE data->>'a' = :the_a AND (data->>'b')::numeric = :b_value",
QueryUtils.byFields("this", List.of(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value"))));
}
@Test
@DisplayName("byFields generates default field query | SQLite")
public void byFieldsMultipleDefaultSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("this WHERE data->>'a' = :the_a AND data->>'b' = :b_value",
QueryUtils.byFields("this", List.of(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value"))));
}
@Test
@DisplayName("byFields generates ANY field query | PostgreSQL")
public void byFieldsMultipleAnyPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals("that WHERE data->>'a' = :the_a OR (data->>'b')::numeric = :b_value",
QueryUtils.byFields("that", List.of(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")),
FieldMatch.ANY));
}
@Test
@DisplayName("byFields generates ANY field query | SQLite")
public void byFieldsMultipleAnySQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals("that WHERE data->>'a' = :the_a OR data->>'b' = :b_value",
QueryUtils.byFields("that", List.of(Field.equal("a", "", ":the_a"), Field.equal("b", 0, ":b_value")),
FieldMatch.ANY));
}
@Test
@DisplayName("orderBy generates for no fields")
public void orderByNone() throws DocumentException {
assertEquals("", QueryUtils.orderBy(List.of(), Dialect.POSTGRESQL),
"ORDER BY should have been blank (PostgreSQL)");
assertEquals("", QueryUtils.orderBy(List.of(), Dialect.SQLITE), "ORDER BY should have been blank (SQLite)");
}
@Test
@DisplayName("orderBy generates single, no direction | PostgreSQL")
public void orderBySinglePostgres() throws DocumentException {
assertEquals(" ORDER BY data->>'TestField'",
QueryUtils.orderBy(List.of(Field.named("TestField")), Dialect.POSTGRESQL),
"ORDER BY not constructed correctly");
}
@Test
@DisplayName("orderBy generates single, no direction | SQLite")
public void orderBySingleSQLite() throws DocumentException {
assertEquals(" ORDER BY data->>'TestField'",
QueryUtils.orderBy(List.of(Field.named("TestField")), Dialect.SQLITE),
"ORDER BY not constructed correctly");
}
@Test
@DisplayName("orderBy generates multiple with direction | PostgreSQL")
public void orderByMultiplePostgres() throws DocumentException {
assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
QueryUtils.orderBy(List.of(
Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
Dialect.POSTGRESQL),
"ORDER BY not constructed correctly");
}
@Test
@DisplayName("orderBy generates multiple with direction | SQLite")
public void orderByMultipleSQLite() throws DocumentException {
assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
QueryUtils.orderBy(List.of(
Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
Dialect.SQLITE),
"ORDER BY not constructed correctly");
}
@Test
@DisplayName("orderBy generates numeric ordering | PostgreSQL")
public void orderByNumericPostgres() throws DocumentException {
assertEquals(" ORDER BY (data->>'Test')::numeric",
QueryUtils.orderBy(List.of(Field.named("n:Test")), Dialect.POSTGRESQL),
"ORDER BY not constructed correctly");
}
@Test
@DisplayName("orderBy generates numeric ordering | SQLite")
public void orderByNumericSQLite() throws DocumentException {
assertEquals(" ORDER BY data->>'Test'", QueryUtils.orderBy(List.of(Field.named("n:Test")), Dialect.SQLITE),
"ORDER BY not constructed correctly");
}
@Test
@DisplayName("orderBy generates case-insensitive ordering | PostgreSQL")
public void orderByCIPostgres() throws DocumentException {
assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
QueryUtils.orderBy(List.of(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL),
"ORDER BY not constructed correctly");
}
@Test
@DisplayName("orderBy generates case-insensitive ordering | SQLite")
public void orderByCISQLite() throws DocumentException {
assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
QueryUtils.orderBy(List.of(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE),
"ORDER BY not constructed correctly");
}
}

View File

@ -0,0 +1,110 @@
package solutions.bitbadger.documents.core.tests.java;
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.Parameter;
import solutions.bitbadger.documents.ParameterType;
import solutions.bitbadger.documents.query.RemoveFieldsQuery;
import solutions.bitbadger.documents.core.tests.ForceDialect;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
/**
* Unit tests for the `RemoveFields` object
*/
@DisplayName("Core | Java | Query | RemoveFieldsQuery")
final public class RemoveFieldsQueryTest {
/**
* Reset the dialect
*/
@AfterEach
public void cleanUp() {
ForceDialect.none();
}
@Test
@DisplayName("byId generates correctly | PostgreSQL")
public void byIdPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("UPDATE %s SET data = data - :name::text[] WHERE data->>'id' = :id", TEST_TABLE),
RemoveFieldsQuery.byId(TEST_TABLE, List.of(new Parameter<>(":name", ParameterType.STRING, "{a,z}"))),
"Remove Fields query not constructed correctly");
}
@Test
@DisplayName("byId generates correctly | SQLite")
public void byIdSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("UPDATE %s SET data = json_remove(data, :name0, :name1) WHERE data->>'id' = :id",
TEST_TABLE),
RemoveFieldsQuery.byId(TEST_TABLE, List.of(new Parameter<>(":name0", ParameterType.STRING, "a"),
new Parameter<>(":name1", ParameterType.STRING, "z"))),
"Remove Field query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | PostgreSQL")
public void byFieldsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("UPDATE %s SET data = data - :name::text[] WHERE data->>'f' > :g", TEST_TABLE),
RemoveFieldsQuery.byFields(TEST_TABLE, List.of(new Parameter<>(":name", ParameterType.STRING, "{b,c}")),
List.of(Field.greater("f", "", ":g"))),
"Remove Field query not constructed correctly");
}
@Test
@DisplayName("byFields generates correctly | SQLite")
public void byFieldsSQLite() throws DocumentException {
ForceDialect.sqlite();
assertEquals(String.format("UPDATE %s SET data = json_remove(data, :name0, :name1) WHERE data->>'f' > :g",
TEST_TABLE),
RemoveFieldsQuery.byFields(TEST_TABLE, List.of(new Parameter<>(":name0", ParameterType.STRING, "b"),
new Parameter<>(":name1", ParameterType.STRING, "c")),
List.of(Field.greater("f", "", ":g"))),
"Remove Field query not constructed correctly");
}
@Test
@DisplayName("byContains generates correctly | PostgreSQL")
public void byContainsPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format("UPDATE %s SET data = data - :name::text[] WHERE data @> :criteria", TEST_TABLE),
RemoveFieldsQuery.byContains(TEST_TABLE,
List.of(new Parameter<>(":name", ParameterType.STRING, "{m,n}"))),
"Remove Field query not constructed correctly");
}
@Test
@DisplayName("byContains fails | SQLite")
public void byContainsSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> RemoveFieldsQuery.byContains(TEST_TABLE, List.of()));
}
@Test
@DisplayName("byJsonPath generates correctly | PostgreSQL")
public void byJsonPathPostgres() throws DocumentException {
ForceDialect.postgres();
assertEquals(String.format(
"UPDATE %s SET data = data - :name::text[] WHERE jsonb_path_exists(data, :path::jsonpath)",
TEST_TABLE),
RemoveFieldsQuery.byJsonPath(TEST_TABLE,
List.of(new Parameter<>(":name", ParameterType.STRING, "{o,p}"))),
"Remove Field query not constructed correctly");
}
@Test
@DisplayName("byJsonPath fails | SQLite")
public void byJsonPathSQLite() {
ForceDialect.sqlite();
assertThrows(DocumentException.class, () -> RemoveFieldsQuery.byJsonPath(TEST_TABLE, List.of()));
}
}

View File

@ -0,0 +1,18 @@
package solutions.bitbadger.documents.core.tests.java;
public class ShortIdClass {
private short id;
public short getId() {
return id;
}
public void setId(short id) {
this.id = id;
}
public ShortIdClass(short id) {
this.id = id;
}
}

View File

@ -0,0 +1,18 @@
package solutions.bitbadger.documents.core.tests.java;
public class StringIdClass {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public StringIdClass(String id) {
this.id = id;
}
}

View File

@ -0,0 +1,172 @@
package solutions.bitbadger.documents.core.tests.java;
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.core.tests.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("Core | 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

@ -0,0 +1,41 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import java.util.List;
public class ArrayDocument {
private String id;
private List<String> values;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<String> getValues() {
return values;
}
public void setValues(List<String> values) {
this.values = values;
}
public ArrayDocument(String id, List<String> values) {
this.id = id;
this.values = values;
}
public ArrayDocument() {
this("", List.of());
}
public static List<ArrayDocument> testDocuments =
List.of(
new ArrayDocument("first", List.of("a", "b", "c")),
new ArrayDocument("second", List.of("c", "d", "e")),
new ArrayDocument("third", List.of("x", "y", "z")));
}

View File

@ -0,0 +1,62 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.Field;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
import static solutions.bitbadger.documents.java.extensions.ConnExt.*;
/**
* Integration tests for the `Count` object
*/
final public class CountFunctions {
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) 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) 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");
}
public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(2L, countByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")),
"There should have been 2 matching documents");
}
public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(0L, countByContains(db.getConn(), TEST_TABLE, Map.of("value", "magenta")),
"There should have been no matching documents");
}
public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(2L, countByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ < 5)"),
"There should have been 2 matching documents");
}
public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(0L, countByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"),
"There should have been no matching documents");
}
private CountFunctions() {
}
}

View File

@ -0,0 +1,146 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.*;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import solutions.bitbadger.documents.java.Results;
import solutions.bitbadger.documents.query.CountQuery;
import solutions.bitbadger.documents.query.DeleteQuery;
import solutions.bitbadger.documents.query.FindQuery;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
import static solutions.bitbadger.documents.java.extensions.ConnExt.*;
import static solutions.bitbadger.documents.query.QueryUtils.orderBy;
final public class CustomFunctions {
public static void listEmpty(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
deleteByFields(db.getConn(), TEST_TABLE, List.of(Field.exists(Configuration.idField)));
Collection<JsonDocument> result =
customList(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData);
assertEquals(0, result.size(), "There should have been no results");
}
public static void listAll(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
Collection<JsonDocument> result =
customList(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData);
assertEquals(5, result.size(), "There should have been 5 results");
}
public static void jsonArrayEmpty(ThrowawayDatabase db) throws DocumentException {
assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "The test table should be empty");
assertEquals("[]", customJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData),
"An empty list was not represented correctly");
}
public static void jsonArraySingle(ThrowawayDatabase db) throws DocumentException {
insert(db.getConn(), TEST_TABLE, new ArrayDocument("one", List.of("2", "3")));
assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"),
customJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData),
"A single document list was not represented correctly");
}
public static void jsonArrayMany(ThrowawayDatabase db) throws DocumentException {
for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); }
assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"first\",\"values\":[\"a\",\"b\",\"c\"]},"
+ "{\"id\":\"second\",\"values\":[\"c\",\"d\",\"e\"]},"
+ "{\"id\":\"third\",\"values\":[\"x\",\"y\",\"z\"]}]"),
customJsonArray(db.getConn(), FindQuery.all(TEST_TABLE) + orderBy(List.of(Field.named("id"))),
List.of(), Results::jsonFromData),
"A multiple document list was not represented correctly");
}
public static void writeJsonArrayEmpty(ThrowawayDatabase db) throws DocumentException {
assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "The test table should be empty");
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), writer, Results::jsonFromData);
assertEquals("[]", output.toString(), "An empty list was not represented correctly");
}
public static void writeJsonArraySingle(ThrowawayDatabase db) throws DocumentException {
insert(db.getConn(), TEST_TABLE, new ArrayDocument("one", List.of("2", "3")));
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), writer, Results::jsonFromData);
assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"one\",\"values\":[\"2\",\"3\"]}]"), output.toString(),
"A single document list was not represented correctly");
}
public static void writeJsonArrayMany(ThrowawayDatabase db) throws DocumentException {
for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); }
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeCustomJsonArray(db.getConn(), FindQuery.all(TEST_TABLE) + orderBy(List.of(Field.named("id"))), List.of(),
writer, Results::jsonFromData);
assertEquals(JsonFunctions.maybeJsonB("[{\"id\":\"first\",\"values\":[\"a\",\"b\",\"c\"]},"
+ "{\"id\":\"second\",\"values\":[\"c\",\"d\",\"e\"]},"
+ "{\"id\":\"third\",\"values\":[\"x\",\"y\",\"z\"]}]"),
output.toString(), "A multiple document list was not represented correctly");
}
public static void singleNone(ThrowawayDatabase db) throws DocumentException {
assertFalse(
customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData)
.isPresent(),
"There should not have been a document returned");
}
public static void singleOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertTrue(
customSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), JsonDocument.class, Results::fromData)
.isPresent(),
"There should have been a document returned");
}
public static void jsonSingleNone(ThrowawayDatabase db) throws DocumentException {
assertEquals("{}", customJsonSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData),
"An empty document was not represented correctly");
}
public static void jsonSingleOne(ThrowawayDatabase db) throws DocumentException {
insert(db.getConn(), TEST_TABLE, new ArrayDocument("me", List.of("myself", "i")));
assertEquals(JsonFunctions.maybeJsonB("{\"id\":\"me\",\"values\":[\"myself\",\"i\"]}"),
customJsonSingle(db.getConn(), FindQuery.all(TEST_TABLE), List.of(), Results::jsonFromData),
"A single document was not represented correctly");
}
public static void nonQueryChanges(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L,
customScalar(db.getConn(), CountQuery.all(TEST_TABLE), List.of(), Long.class, Results::toCount),
"There should have been 5 documents in the table");
customNonQuery(db.getConn(), String.format("DELETE FROM %s", TEST_TABLE));
assertEquals(0L,
customScalar(db.getConn(), CountQuery.all(TEST_TABLE), List.of(), Long.class, Results::toCount),
"There should have been no documents in the table");
}
public static void nonQueryNoChanges(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L,
customScalar(db.getConn(), CountQuery.all(TEST_TABLE), List.of(), Long.class, Results::toCount),
"There should have been 5 documents in the table");
customNonQuery(db.getConn(), DeleteQuery.byId(TEST_TABLE, "eighty-two"),
List.of(new Parameter<>(":id", ParameterType.STRING, "eighty-two")));
assertEquals(5L,
customScalar(db.getConn(), CountQuery.all(TEST_TABLE), List.of(), Long.class, Results::toCount),
"There should still have been 5 documents in the table");
}
public static void scalar(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(3L,
customScalar(db.getConn(), String.format("SELECT 3 AS it FROM %s LIMIT 1", TEST_TABLE), List.of(),
Long.class, Results::toCount),
"The number 3 should have been returned");
}
}

View File

@ -0,0 +1,47 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.DocumentIndex;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
import static solutions.bitbadger.documents.java.extensions.ConnExt.*;
final public class DefinitionFunctions {
public static void ensuresATable(ThrowawayDatabase db) throws DocumentException {
assertFalse(db.dbObjectExists("ensured"), "The 'ensured' table should not exist");
assertFalse(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should not exist");
ensureTable(db.getConn(), "ensured");
assertTrue(db.dbObjectExists("ensured"), "The 'ensured' table should exist");
assertTrue(db.dbObjectExists("idx_ensured_key"), "The PK index for the 'ensured' table should now exist");
}
public static void ensuresAFieldIndex(ThrowawayDatabase db) throws DocumentException {
assertFalse(db.dbObjectExists(String.format("idx_%s_test", TEST_TABLE)), "The test index should not exist");
ensureFieldIndex(db.getConn(), TEST_TABLE, "test", List.of("id", "category"));
assertTrue(db.dbObjectExists(String.format("idx_%s_test", TEST_TABLE)), "The test index should now exist");
}
public static void ensureDocumentIndexFull(ThrowawayDatabase db) throws DocumentException {
assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist");
ensureTable(db.getConn(), "doc_table");
assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist");
assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist");
ensureDocumentIndex(db.getConn(), "doc_table", DocumentIndex.FULL);
assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist");
}
public static void ensureDocumentIndexOptimized(ThrowawayDatabase db) throws DocumentException {
assertFalse(db.dbObjectExists("doc_table"), "The 'doc_table' table should not exist");
ensureTable(db.getConn(), "doc_table");
assertTrue(db.dbObjectExists("doc_table"), "The 'doc_table' table should exist");
assertFalse(db.dbObjectExists("idx_doc_table_document"), "The document index should not exist");
ensureDocumentIndex(db.getConn(), "doc_table", DocumentIndex.OPTIMIZED);
assertTrue(db.dbObjectExists("idx_doc_table_document"), "The document index should exist");
}
}

View File

@ -0,0 +1,71 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.Field;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
import static solutions.bitbadger.documents.java.extensions.ConnExt.*;
final public class DeleteFunctions {
public static void byIdMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table");
deleteById(db.getConn(), TEST_TABLE, "four");
assertEquals(4L, countAll(db.getConn(), TEST_TABLE), "There should now be 4 documents in the table");
}
public static void byIdNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table");
deleteById(db.getConn(), TEST_TABLE, "negative four");
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should still be 5 documents in the table");
}
public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table");
deleteByFields( db.getConn(), TEST_TABLE, List.of(Field.notEqual("value", "purple")));
assertEquals(2L, countAll(db.getConn(), TEST_TABLE), "There should now be 2 documents in the table");
}
public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table");
deleteByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "crimson")));
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should still be 5 documents in the table");
}
public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table");
deleteByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"));
assertEquals(3L, countAll(db.getConn(), TEST_TABLE), "There should now be 3 documents in the table");
}
public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table");
deleteByContains(db.getConn(), TEST_TABLE, Map.of("target", "acquired"));
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should still be 5 documents in the table");
}
public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table");
deleteByJsonPath(db.getConn(), TEST_TABLE, "$.value ? (@ == \"purple\")");
assertEquals(3L, countAll(db.getConn(), TEST_TABLE), "There should now be 3 documents in the table");
}
public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should be 5 documents in the table");
deleteByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)");
assertEquals(5L, countAll(db.getConn(), TEST_TABLE), "There should still be 5 documents in the table");
}
}

View File

@ -0,0 +1,138 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.AutoId;
import solutions.bitbadger.documents.Configuration;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.Field;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
import static solutions.bitbadger.documents.java.extensions.ConnExt.*;
final public class DocumentFunctions {
public static void insertDefault(ThrowawayDatabase db) throws DocumentException {
assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "There should be no documents in the table");
insert(db.getConn(), TEST_TABLE, new JsonDocument("turkey", "", 0, new SubDocument("gobble", "gobble!")));
final List<JsonDocument> after = findAll(db.getConn(), TEST_TABLE, JsonDocument.class);
assertEquals(1, after.size(), "There should be one document in the table");
final JsonDocument doc = after.get(0);
assertEquals("turkey", doc.getId(), "The inserted document's ID is incorrect");
assertEquals("", doc.getValue(), "The inserted document's value is incorrect");
assertEquals(0, doc.getNumValue(), "The document's numeric value is incorrect");
assertNotNull(doc.getSub(), "The inserted document's subdocument was not created");
assertEquals("gobble", doc.getSub().getFoo(), "The subdocument's \"foo\" property is incorrect");
assertEquals("gobble!", doc.getSub().getBar(), "The subdocument's \"bar\" property is incorrect");
}
public static void insertDupe(ThrowawayDatabase db) throws DocumentException {
insert(db.getConn(), TEST_TABLE, new JsonDocument("a", "", 0));
assertThrows(DocumentException.class, () -> insert(db.getConn(), TEST_TABLE, new JsonDocument("a", "b", 22)),
"Inserting a document with a duplicate key should have thrown an exception");
}
public static void insertNumAutoId(ThrowawayDatabase db) throws DocumentException {
try {
Configuration.autoIdStrategy = AutoId.NUMBER;
Configuration.idField = "key";
assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "There should be no documents in the table");
insert(db.getConn(), TEST_TABLE, new NumIdDocument(0, "one"));
insert(db.getConn(), TEST_TABLE, new NumIdDocument(0, "two"));
insert(db.getConn(), TEST_TABLE, new NumIdDocument(77, "three"));
insert(db.getConn(), TEST_TABLE, new NumIdDocument(0, "four"));
final List<NumIdDocument> after = findAll(db.getConn(), TEST_TABLE, NumIdDocument.class,
List.of(Field.named("key")));
assertEquals(4, after.size(), "There should have been 4 documents returned");
assertEquals("1|2|77|78",
after.stream().map(doc -> String.valueOf(doc.getKey()))
.reduce((acc, item) -> String.format("%s|%s", acc, item)).get(),
"The IDs were not generated correctly");
} finally {
Configuration.autoIdStrategy = AutoId.DISABLED;
Configuration.idField = "id";
}
}
public static void insertUUIDAutoId(ThrowawayDatabase db) throws DocumentException {
try {
Configuration.autoIdStrategy = AutoId.UUID;
assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "There should be no documents in the table");
insert(db.getConn(), TEST_TABLE, new JsonDocument(""));
final List<JsonDocument> after = findAll(db.getConn(), TEST_TABLE, JsonDocument.class);
assertEquals(1, after.size(), "There should have been 1 document returned");
assertEquals(32, after.get(0).getId().length(), "The ID was not generated correctly");
} finally {
Configuration.autoIdStrategy = AutoId.DISABLED;
}
}
public static void insertStringAutoId(ThrowawayDatabase db) throws DocumentException {
try {
Configuration.autoIdStrategy = AutoId.RANDOM_STRING;
assertEquals(0L, countAll(db.getConn(), TEST_TABLE), "There should be no documents in the table");
insert(db.getConn(), TEST_TABLE, new JsonDocument(""));
Configuration.idStringLength = 21;
insert(db.getConn(), TEST_TABLE, new JsonDocument(""));
final List<JsonDocument> after = findAll(db.getConn(), TEST_TABLE, JsonDocument.class);
assertEquals(2, after.size(), "There should have been 2 documents returned");
assertEquals(16, after.get(0).getId().length(), "The first document's ID was not generated correctly");
assertEquals(21, after.get(1).getId().length(), "The second document's ID was not generated correctly");
} finally {
Configuration.autoIdStrategy = AutoId.DISABLED;
Configuration.idStringLength = 16;
}
}
public static void saveMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
save(db.getConn(), TEST_TABLE, new JsonDocument("two", "", 44));
final Optional<JsonDocument> tryDoc = findById(db.getConn(), TEST_TABLE, "two", JsonDocument.class);
assertTrue(tryDoc.isPresent(), "There should have been a document returned");
final JsonDocument doc = tryDoc.get();
assertEquals("two", doc.getId(), "An incorrect document was returned");
assertEquals("", doc.getValue(), "The \"value\" field was not updated");
assertEquals(44, doc.getNumValue(), "The \"numValue\" field was not updated");
assertNull(doc.getSub(), "The \"sub\" field was not updated");
}
public static void saveNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final JsonDocument toSave = new JsonDocument("test");
toSave.setSub(new SubDocument("a", "b"));
save(db.getConn(), TEST_TABLE, toSave);
assertTrue(findById(db.getConn(), TEST_TABLE, "test", JsonDocument.class).isPresent(),
"The test document should have been saved");
}
public static void updateMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
update(db.getConn(), TEST_TABLE, "one", new JsonDocument("one", "howdy", 8, new SubDocument("y", "z")));
final Optional<JsonDocument> tryDoc = findById(db.getConn(), TEST_TABLE, "one", JsonDocument.class);
assertTrue(tryDoc.isPresent(), "There should have been a document returned");
final JsonDocument doc = tryDoc.get();
assertEquals("one", doc.getId(), "An incorrect document was returned");
assertEquals("howdy", doc.getValue(), "The \"value\" field was not updated");
assertEquals(8, doc.getNumValue(), "The \"numValue\" field was not updated");
assertNotNull(doc.getSub(), "The sub-document should not be null");
assertEquals("y", doc.getSub().getFoo(), "The sub-document \"foo\" field was not updated");
assertEquals("z", doc.getSub().getBar(), "The sub-document \"bar\" field was not updated");
}
public static void updateNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(existsById(db.getConn(), TEST_TABLE, "two-hundred"));
update(db.getConn(), TEST_TABLE, "two-hundred", new JsonDocument("two-hundred", "", 200));
assertFalse(existsById(db.getConn(), TEST_TABLE, "two-hundred"));
}
}

View File

@ -0,0 +1,62 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.Field;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
import static solutions.bitbadger.documents.java.extensions.ConnExt.*;
final public class ExistsFunctions {
public static void byIdMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertTrue(existsById(db.getConn(), TEST_TABLE, "three"), "The document with ID \"three\" should exist");
}
public static void byIdNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(existsById(db.getConn(), TEST_TABLE, "seven"), "The document with ID \"seven\" should not exist");
}
public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertTrue(existsByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("numValue", 10))),
"Matching documents should have been found");
}
public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(existsByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("nothing", "none"))),
"No matching documents should have been found");
}
public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertTrue(existsByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")),
"Matching documents should have been found");
}
public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(existsByContains(db.getConn(), TEST_TABLE, Map.of("value", "violet")),
"Matching documents should not have been found");
}
public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertTrue(existsByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10)"),
"Matching documents should have been found");
}
public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(existsByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10.1)"),
"Matching documents should not have been found");
}
}

View File

@ -0,0 +1,279 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.Configuration;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.Field;
import solutions.bitbadger.documents.FieldMatch;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
import static solutions.bitbadger.documents.java.extensions.ConnExt.*;
final public class FindFunctions {
private static String docIds(List<JsonDocument> docs) {
return docs.stream().map(JsonDocument::getId).reduce((acc, docId) -> String.format("%s|%s", acc, docId)).get();
}
public static void allDefault(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(5, findAll(db.getConn(), TEST_TABLE, JsonDocument.class).size(),
"There should have been 5 documents returned");
}
public static void allAscending(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findAll(db.getConn(), TEST_TABLE, JsonDocument.class,
List.of(Field.named("id")));
assertEquals(5, docs.size(), "There should have been 5 documents returned");
assertEquals("five|four|one|three|two", docIds(docs), "The documents were not ordered correctly");
}
public static void allDescending(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findAll(db.getConn(), TEST_TABLE, JsonDocument.class,
List.of(Field.named("id DESC")));
assertEquals(5, docs.size(), "There should have been 5 documents returned");
assertEquals("two|three|one|four|five", docIds(docs), "The documents were not ordered correctly");
}
public static void allNumOrder(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findAll(db.getConn(), TEST_TABLE, JsonDocument.class,
List.of(Field.named("sub.foo NULLS LAST"), Field.named("n:numValue")));
assertEquals(5, docs.size(), "There should have been 5 documents returned");
assertEquals("two|four|one|three|five", docIds(docs), "The documents were not ordered correctly");
}
public static void allEmpty(ThrowawayDatabase db) throws DocumentException {
assertEquals(0, findAll(db.getConn(), TEST_TABLE, JsonDocument.class).size(),
"There should have been no documents returned");
}
public static void byIdString(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findById(db.getConn(), TEST_TABLE, "two", JsonDocument.class);
assertTrue(doc.isPresent(), "The document should have been returned");
assertEquals("two", doc.get().getId(), "An incorrect document was returned");
}
public static void byIdNumber(ThrowawayDatabase db) throws DocumentException {
Configuration.idField = "key";
try {
insert(db.getConn(), TEST_TABLE, new NumIdDocument(18, "howdy"));
final Optional<NumIdDocument> doc = findById(db.getConn(), TEST_TABLE, 18, NumIdDocument.class);
assertTrue(doc.isPresent(), "The document should have been returned");
} finally {
Configuration.idField = "id";
}
}
public static void byIdNotFound(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(findById(db.getConn(), TEST_TABLE, "x", JsonDocument.class).isPresent(),
"There should have been no document returned");
}
public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findByFields(db.getConn(), TEST_TABLE,
List.of(Field.any("value", List.of("blue", "purple")), Field.exists("sub")), JsonDocument.class,
FieldMatch.ALL);
assertEquals(1, docs.size(), "There should have been a document returned");
assertEquals("four", docs.get(0).getId(), "The incorrect document was returned");
}
public static void byFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "purple")),
JsonDocument.class, null, List.of(Field.named("id")));
assertEquals(2, docs.size(), "There should have been 2 documents returned");
assertEquals("five|four", docIds(docs), "The documents were not ordered correctly");
}
public static void byFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findByFields(db.getConn(), TEST_TABLE,
List.of(Field.any("numValue", List.of(2, 4, 6, 8))), JsonDocument.class);
assertEquals(1, docs.size(), "There should have been a document returned");
assertEquals("three", docs.get(0).getId(), "The incorrect document was returned");
}
public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(0,
findByFields(db.getConn(), TEST_TABLE, List.of(Field.greater("numValue", 100)), JsonDocument.class)
.size(),
"There should have been no documents returned");
}
public static void byFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException {
for (final ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); }
final List<ArrayDocument> docs = findByFields(db.getConn(), TEST_TABLE,
List.of(Field.inArray("values", TEST_TABLE, List.of("c"))), ArrayDocument.class);
assertEquals(2, docs.size(), "There should have been two documents returned");
assertTrue(List.of("first", "second").contains(docs.get(0).getId()),
String.format("An incorrect document was returned (%s)", docs.get(0).getId()));
assertTrue(List.of("first", "second").contains(docs.get(1).getId()),
String.format("An incorrect document was returned (%s)", docs.get(1).getId()));
}
public static void byFieldsNoMatchInArray(ThrowawayDatabase db) throws DocumentException {
for (final ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); }
assertEquals(0,
findByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE, List.of("j"))),
ArrayDocument.class).size(),
"There should have been no documents returned");
}
public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"),
JsonDocument.class);
assertEquals(2, docs.size(), "There should have been 2 documents returned");
assertTrue(List.of("four", "five").contains(docs.get(0).getId()),
String.format("An incorrect document was returned (%s)", docs.get(0).getId()));
assertTrue(List.of("four", "five").contains(docs.get(1).getId()),
String.format("An incorrect document was returned (%s)", docs.get(1).getId()));
}
public static void byContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findByContains(db.getConn(), TEST_TABLE, Map.of("sub", Map.of("foo", "green")),
JsonDocument.class, List.of(Field.named("value")));
assertEquals(2, docs.size(), "There should have been 2 documents returned");
assertEquals("two|four", docIds(docs), "The documents were not ordered correctly");
}
public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(0, findByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo"), JsonDocument.class).size(),
"There should have been no documents returned");
}
public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)",
JsonDocument.class);
assertEquals(2, docs.size(), "There should have been 2 documents returned");
assertTrue(List.of("four", "five").contains(docs.get(0).getId()),
String.format("An incorrect document was returned (%s)", docs.get(0).getId()));
assertTrue(List.of("four", "five").contains(docs.get(1).getId()),
String.format("An incorrect document was returned (%s)", docs.get(1).getId())); }
public static void byJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final List<JsonDocument> docs = findByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)",
JsonDocument.class, List.of(Field.named("id")));
assertEquals(2, docs.size(), "There should have been 2 documents returned");
assertEquals("five|four", docIds(docs), "The documents were not ordered correctly");
}
public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertEquals(0, findByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument.class).size(),
"There should have been no documents returned");
}
public static void firstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByFields(db.getConn(), TEST_TABLE,
List.of(Field.equal("value", "another")), JsonDocument.class);
assertTrue(doc.isPresent(), "There should have been a document returned");
assertEquals("two", doc.get().getId(), "The incorrect document was returned");
}
public static void firstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByFields(db.getConn(), TEST_TABLE,
List.of(Field.equal("sub.foo", "green")), JsonDocument.class);
assertTrue(doc.isPresent(), "There should have been a document returned");
assertTrue(List.of("two", "four").contains(doc.get().getId()),
String.format("An incorrect document was returned (%s)", doc.get().getId()));
}
public static void firstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByFields(db.getConn(), TEST_TABLE,
List.of(Field.equal("sub.foo", "green")), JsonDocument.class, null,
List.of(Field.named("n:numValue DESC")));
assertTrue(doc.isPresent(), "There should have been a document returned");
assertEquals("four", doc.get().getId(), "An incorrect document was returned");
}
public static void firstByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(findFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "absent")),
JsonDocument.class).isPresent(),
"There should have been no document returned");
}
public static void firstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "FIRST!"),
JsonDocument.class);
assertTrue(doc.isPresent(), "There should have been a document returned");
assertEquals("one", doc.get().getId(), "An incorrect document was returned");
}
public static void firstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"),
JsonDocument.class);
assertTrue(doc.isPresent(), "There should have been a document returned");
assertTrue(List.of("four", "five").contains(doc.get().getId()),
String.format("An incorrect document was returned (%s)", doc.get().getId()));
}
public static void firstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"),
JsonDocument.class, List.of(Field.named("sub.bar NULLS FIRST")));
assertTrue(doc.isPresent(), "There should have been a document returned");
assertEquals("five", doc.get().getId(), "An incorrect document was returned");
}
public static void firstByContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(findFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo"), JsonDocument.class)
.isPresent(),
"There should have been no document returned");
}
public static void firstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10)",
JsonDocument.class);
assertTrue(doc.isPresent(), "There should have been a document returned");
assertEquals("two", doc.get().getId(), "An incorrect document was returned");
}
public static void firstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)",
JsonDocument.class);
assertTrue(doc.isPresent(), "There should have been a document returned");
assertTrue(List.of("four", "five").contains(doc.get().getId()),
String.format("An incorrect document was returned (%s)", doc.get().getId()));
}
public static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final Optional<JsonDocument> doc = findFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)",
JsonDocument.class, List.of(Field.named("id DESC")));
assertTrue(doc.isPresent(), "There should have been a document returned");
assertEquals("four", doc.get().getId(), "An incorrect document was returned");
}
public static void firstByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
assertFalse(findFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)", JsonDocument.class)
.isPresent(),
"There should have been no document returned");
}
}

View File

@ -0,0 +1,107 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.DocumentException;
import solutions.bitbadger.documents.java.Document;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import java.util.List;
import static org.junit.jupiter.api.Assertions.fail;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
public class JsonDocument {
private String id;
private String value;
private int numValue;
private SubDocument sub;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getNumValue() {
return numValue;
}
public void setNumValue(int numValue) {
this.numValue = numValue;
}
public SubDocument getSub() {
return sub;
}
public void setSub(SubDocument sub) {
this.sub = sub;
}
public JsonDocument(String id, String value, int numValue, SubDocument sub) {
this.id = id;
this.value = value;
this.numValue = numValue;
this.sub = sub;
}
public JsonDocument(String id, String value, int numValue) {
this(id, value, numValue, null);
}
public JsonDocument(String id) {
this(id, "", 0, null);
}
public JsonDocument() {
this(null);
}
private static final List<JsonDocument> testDocuments = List.of(
new JsonDocument("one", "FIRST!", 0),
new JsonDocument("two", "another", 10, new SubDocument("green", "blue")),
new JsonDocument("three", "", 4),
new JsonDocument("four", "purple", 17, new SubDocument("green", "red")),
new JsonDocument("five", "purple", 18));
public static void load(ThrowawayDatabase db, String tableName) {
try {
for (JsonDocument doc : testDocuments) {
Document.insert(tableName, doc, db.getConn());
}
} catch (DocumentException ex) {
fail("Could not load test documents", ex);
}
}
public static void load(ThrowawayDatabase db) {
load(db, TEST_TABLE);
}
/** Document ID <code>one</code> as a JSON string */
public static String one = "{\"id\":\"one\",\"value\":\"FIRST!\",\"numValue\":0,\"sub\":null}";
/** Document ID <code>two</code> as a JSON string */
public static String two = "{\"id\":\"two\",\"value\":\"another\",\"numValue\":10,"
+ "\"sub\":{\"foo\":\"green\",\"bar\":\"blue\"}}";
/** Document ID <code>three</code> as a JSON string */
public static String three = "{\"id\":\"three\",\"value\":\"\",\"numValue\":4,\"sub\":null}";
/** Document ID <code>four</code> as a JSON string */
public static String four = "{\"id\":\"four\",\"value\":\"purple\",\"numValue\":17,"
+ "\"sub\":{\"foo\":\"green\",\"bar\":\"red\"}}";
/** Document ID <code>five</code> as a JSON string */
public static String five = "{\"id\":\"five\",\"value\":\"purple\",\"numValue\":18,\"sub\":null}";
}

View File

@ -0,0 +1,761 @@
package solutions.bitbadger.documents.core.tests.java.integration;
import solutions.bitbadger.documents.*;
import solutions.bitbadger.documents.core.tests.integration.ThrowawayDatabase;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static solutions.bitbadger.documents.core.tests.TypesKt.TEST_TABLE;
import static solutions.bitbadger.documents.java.extensions.ConnExt.*;
/**
* Tests for the JSON-returning functions
* <p>
* NOTE: PostgreSQL JSONB columns do not preserve the original JSON with which a document was stored. These tests are
* the most complex within the library, as they have split testing based on the backing data store. The PostgreSQL tests
* check IDs (and, in the case of ordered queries, which ones occur before which others) vs. the entire JSON string.
* Meanwhile, SQLite stores JSON as text, and will return exactly the JSON it was given when it was originally written.
* These tests can ensure the expected round-trip of the entire JSON string.
*/
final public class JsonFunctions {
/**
* PostgreSQL, when returning JSONB as a string, has spaces after commas and colons delineating fields and values.
* This function will do a crude string replacement to match the target string based on the dialect being tested.
*
* @param json The JSON which should be returned
* @return The actual expected JSON based on the database being tested
*/
public static String maybeJsonB(String json) throws DocumentException {
return switch (Configuration.dialect()) {
case SQLITE -> json;
case POSTGRESQL -> json.replace("\":", "\": ").replace(",\"", ", \"");
};
}
/**
* Create a snippet of JSON to find a document ID
*
* @param id The ID of the document
* @return A connection-aware ID to check for presence and positioning
*/
private static String docId(String id) throws DocumentException {
return maybeJsonB(String.format("{\"id\":\"%s\"", id));
}
private static void checkAllDefault(String json) throws DocumentException {
assertTrue(json.startsWith("["), "JSON should start with '[' ($json)");
switch (Configuration.dialect()) {
case SQLITE:
assertTrue(json.contains(JsonDocument.one),
String.format("Document 'one' not found in JSON (%s)", json));
assertTrue(json.contains(JsonDocument.two),
String.format("Document 'two' not found in JSON (%s)", json));
assertTrue(json.contains(JsonDocument.three),
String.format("Document 'three' not found in JSON (%s)", json));
assertTrue(json.contains(JsonDocument.four),
String.format("Document 'four' not found in JSON (%s)", json));
assertTrue(json.contains(JsonDocument.five),
String.format("Document 'five' not found in JSON (%s)", json));
break;
case POSTGRESQL:
assertTrue(json.contains(docId("one")), String.format("Document 'one' not found in JSON (%s)", json));
assertTrue(json.contains(docId("two")), String.format("Document 'two' not found in JSON (%s)", json));
assertTrue(json.contains(docId("three")),
String.format("Document 'three' not found in JSON (%s)", json));
assertTrue(json.contains(docId("four")), String.format("Document 'four' not found in JSON (%s)", json));
assertTrue(json.contains(docId("five")), String.format("Document 'five' not found in JSON (%s)", json));
break;
}
assertTrue(json.endsWith("]"), "JSON should end with ']' ($json)");
}
public static void allDefault(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkAllDefault(jsonAll(db.getConn(), TEST_TABLE));
}
public static void writeAllDefault(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonAll(db.getConn(), TEST_TABLE, writer);
checkAllDefault(output.toString());
}
private static void checkAllEmpty(String json) {
assertEquals("[]", json, "There should have been no documents returned");
}
public static void allEmpty(ThrowawayDatabase db) throws DocumentException {
checkAllEmpty(jsonAll(db.getConn(), TEST_TABLE));
}
public static void writeAllEmpty(ThrowawayDatabase db) throws DocumentException {
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonAll(db.getConn(), TEST_TABLE, writer);
checkAllEmpty(output.toString());
}
private static void checkByIdString(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(JsonDocument.two, json, "An incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.contains(docId("two")), String.format("An incorrect document was returned (%s)", json));
break;
}
}
public static void byIdString(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByIdString(jsonById(db.getConn(), TEST_TABLE, "two"));
}
public static void writeByIdString(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonById(db.getConn(), TEST_TABLE, writer, "two");
checkByIdString(output.toString());
}
private static void checkByIdNumber(String json) throws DocumentException {
assertEquals(maybeJsonB("{\"key\":18,\"text\":\"howdy\"}"), json,
"The document should have been found by numeric ID");
}
public static void byIdNumber(ThrowawayDatabase db) throws DocumentException {
Configuration.idField = "key";
try {
insert(db.getConn(), TEST_TABLE, new NumIdDocument(18, "howdy"));
checkByIdNumber(jsonById(db.getConn(), TEST_TABLE, 18));
} finally {
Configuration.idField = "id";
}
}
public static void writeByIdNumber(ThrowawayDatabase db) throws DocumentException {
Configuration.idField = "key";
try {
insert(db.getConn(), TEST_TABLE, new NumIdDocument(18, "howdy"));
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonById(db.getConn(), TEST_TABLE, writer, 18);
checkByIdNumber(output.toString());
} finally {
Configuration.idField = "id";
}
}
private static void checkByIdNotFound(String json) {
assertEquals("{}", json, "There should have been no document returned");
}
public static void byIdNotFound(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByIdNotFound(jsonById(db.getConn(), TEST_TABLE, "x"));
}
public static void writeByIdNotFound(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonById(db.getConn(), TEST_TABLE, writer, "x");
checkByIdNotFound(output.toString());
}
private static void checkByFieldsMatch(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(String.format("[%s]", JsonDocument.four), json, "The incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json));
assertTrue(json.contains(docId("four")),
String.format("The incorrect document was returned (%s)", json));
assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json));
break;
}
}
public static void byFieldsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByFieldsMatch(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.any("value", List.of("blue", "purple")),
Field.exists("sub")), FieldMatch.ALL));
}
public static void writeByFieldsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.any("value", List.of("blue", "purple")),
Field.exists("sub")), FieldMatch.ALL);
checkByFieldsMatch(output.toString());
}
private static void checkByFieldsMatchOrdered(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(String.format("[%s,%s]", JsonDocument.five, JsonDocument.four), json,
"The documents were not ordered correctly");
break;
case POSTGRESQL:
final int fiveIdx = json.indexOf(docId("five"));
final int fourIdx = json.indexOf(docId("four"));
assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json));
assertTrue(fiveIdx >= 0, String.format("Document 'five' not found (%s)", json));
assertTrue(fourIdx >= 0, String.format("Document 'four' not found (%s)", json));
assertTrue(fiveIdx < fourIdx,
String.format("Document 'five' should have been before 'four' (%s)", json));
assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json));
break;
}
}
public static void byFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByFieldsMatchOrdered(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "purple")), null,
List.of(Field.named("id"))));
}
public static void writeByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("value", "purple")), null,
List.of(Field.named("id")));
checkByFieldsMatchOrdered(output.toString());
}
private static void checkByFieldsMatchNumIn(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(String.format("[%s]", JsonDocument.three), json, "The incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json));
assertTrue(json.contains(docId("three")),
String.format("The incorrect document was returned (%s)", json));
assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json));
break;
}
}
public static void byFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByFieldsMatchNumIn(jsonByFields(db.getConn(), TEST_TABLE,
List.of(Field.any("numValue", List.of(2, 4, 6, 8)))));
}
public static void writeByFieldsMatchNumIn(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.any("numValue", List.of(2, 4, 6, 8))));
checkByFieldsMatchNumIn(output.toString());
}
private static void checkByFieldsNoMatch(String json) {
assertEquals("[]", json, "There should have been no documents returned");
}
public static void byFieldsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByFieldsNoMatch(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.greater("numValue", 100))));
}
public static void writeByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.greater("numValue", 100)));
checkByFieldsNoMatch(output.toString());
}
private static void checkByFieldsMatchInArray(String json) throws DocumentException {
assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json));
assertTrue(json.contains(docId("first")), String.format("The 'first' document was not found (%s)", json));
assertTrue(json.contains(docId("second")), String.format("The 'second' document was not found (%s)", json));
assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json));
}
public static void byFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException {
for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); }
checkByFieldsMatchInArray(jsonByFields(db.getConn(), TEST_TABLE, List.of(Field.inArray("values", TEST_TABLE,
List.of("c")))));
}
public static void writeByFieldsMatchInArray(ThrowawayDatabase db) throws DocumentException {
for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); }
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.inArray("values", TEST_TABLE, List.of("c"))));
checkByFieldsMatchInArray(output.toString());
}
private static void checkByFieldsNoMatchInArray(String json) {
assertEquals("[]", json, "There should have been no documents returned");
}
public static void byFieldsNoMatchInArray(ThrowawayDatabase db) throws DocumentException {
for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); }
checkByFieldsNoMatchInArray(jsonByFields(db.getConn(), TEST_TABLE,
List.of(Field.inArray("values", TEST_TABLE, List.of("j")))));
}
public static void writeByFieldsNoMatchInArray(ThrowawayDatabase db) throws DocumentException {
for (ArrayDocument doc : ArrayDocument.testDocuments) { insert(db.getConn(), TEST_TABLE, doc); }
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.inArray("values", TEST_TABLE, List.of("j"))));
checkByFieldsNoMatchInArray(output.toString());
}
private static void checkByContainsMatch(String json) throws DocumentException {
assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json));
switch (Configuration.dialect()) {
case SQLITE:
assertTrue(json.contains(JsonDocument.four), String.format("Document 'four' not found (%s)", json));
assertTrue(json.contains(JsonDocument.five), String.format("Document 'five' not found (%s)", json));
break;
case POSTGRESQL:
assertTrue(json.contains(docId("four")), String.format("Document 'four' not found (%s)", json));
assertTrue(json.contains(docId("five")), String.format("Document 'five' not found (%s)", json));
break;
}
assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json));
}
public static void byContainsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByContainsMatch(jsonByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")));
}
public static void writeByContainsMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "purple"));
checkByContainsMatch(output.toString());
}
private static void checkByContainsMatchOrdered(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(String.format("[%s,%s]", JsonDocument.two, JsonDocument.four), json,
"The documents were not ordered correctly");
break;
case POSTGRESQL:
final int twoIdx = json.indexOf(docId("two"));
final int fourIdx = json.indexOf(docId("four"));
assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json));
assertTrue(twoIdx >= 0, String.format("Document 'two' not found (%s)", json));
assertTrue(fourIdx >= 0, String.format("Document 'four' not found (%s)", json));
assertTrue(twoIdx < fourIdx, String.format("Document 'two' should have been before 'four' (%s)", json));
assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json));
break;
}
}
public static void byContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByContainsMatchOrdered(jsonByContains(db.getConn(), TEST_TABLE, Map.of("sub", Map.of("foo", "green")),
List.of(Field.named("value"))));
}
public static void writeByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByContains(db.getConn(), TEST_TABLE, writer, Map.of("sub", Map.of("foo", "green")),
List.of(Field.named("value")));
checkByContainsMatchOrdered(output.toString());
}
private static void checkByContainsNoMatch(String json) {
assertEquals("[]", json, "There should have been no documents returned");
}
public static void byContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByContainsNoMatch(jsonByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo")));
}
public static void writeByContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "indigo"));
checkByContainsNoMatch(output.toString());
}
private static void checkByJsonPathMatch(String json) throws DocumentException {
assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json));
switch (Configuration.dialect()) {
case SQLITE:
assertTrue(json.contains(JsonDocument.four), String.format("Document 'four' not found (%s)", json));
assertTrue(json.contains(JsonDocument.five), String.format("Document 'five' not found (%s)", json));
break;
case POSTGRESQL:
assertTrue(json.contains(docId("four")), String.format("Document 'four' not found (%s)", json));
assertTrue(json.contains(docId("five")), String.format("Document 'five' not found (%s)", json));
break;
}
assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json));
}
public static void byJsonPathMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByJsonPathMatch(jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)"));
}
public static void writeByJsonPathMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 10)");
checkByJsonPathMatch(output.toString());
}
private static void checkByJsonPathMatchOrdered(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(String.format("[%s,%s]", JsonDocument.five, JsonDocument.four), json,
"The documents were not ordered correctly");
break;
case POSTGRESQL:
final int fiveIdx = json.indexOf(docId("five"));
final int fourIdx = json.indexOf(docId("four"));
assertTrue(json.startsWith("["), String.format("JSON should start with '[' (%s)", json));
assertTrue(fiveIdx >= 0, String.format("Document 'five' not found (%s)", json));
assertTrue(fourIdx >= 0, String.format("Document 'four' not found (%s)", json));
assertTrue(fiveIdx < fourIdx,
String.format("Document 'five' should have been before 'four' (%s)", json));
assertTrue(json.endsWith("]"), String.format("JSON should end with ']' (%s)", json));
break;
}
}
public static void byJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByJsonPathMatchOrdered(jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)",
List.of(Field.named("id"))));
}
public static void writeByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 10)", List.of(Field.named("id")));
checkByJsonPathMatchOrdered(output.toString());
}
private static void checkByJsonPathNoMatch(String json) {
assertEquals("[]", json, "There should have been no documents returned");
}
public static void byJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkByJsonPathNoMatch(jsonByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"));
}
public static void writeByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 100)");
checkByJsonPathNoMatch(output.toString());
}
private static void checkFirstByFieldsMatchOne(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(JsonDocument.two, json, "The incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.contains(docId("two")),
String.format("The incorrect document was returned (%s)", json));
break;
}
}
public static void firstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByFieldsMatchOne(jsonFirstByFields(db.getConn(), TEST_TABLE,
List.of(Field.equal("value", "another"))));
}
public static void writeFirstByFieldsMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("value", "another")));
checkFirstByFieldsMatchOne(output.toString());
}
private static void checkFirstByFieldsMatchMany(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertTrue(json.contains(JsonDocument.two) || json.contains(JsonDocument.four),
String.format("Expected document 'two' or 'four' (%s)", json));
break;
case POSTGRESQL:
assertTrue(json.contains(docId("two")) || json.contains(docId("four")),
String.format("Expected document 'two' or 'four' (%s)", json));
break;
}
}
public static void firstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByFieldsMatchMany(jsonFirstByFields(db.getConn(), TEST_TABLE,
List.of(Field.equal("sub.foo", "green"))));
}
public static void writeFirstByFieldsMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("sub.foo", "green")));
checkFirstByFieldsMatchMany(output.toString());
}
private static void checkFirstByFieldsMatchOrdered(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(JsonDocument.four, json, "An incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.contains(docId("four")),
String.format("An incorrect document was returned (%s)", json));
break;
}
}
public static void firstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByFieldsMatchOrdered(jsonFirstByFields(db.getConn(), TEST_TABLE,
List.of(Field.equal("sub.foo", "green")), null, List.of(Field.named("n:numValue DESC"))));
}
public static void writeFirstByFieldsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("sub.foo", "green")), null,
List.of(Field.named("n:numValue DESC")));
checkFirstByFieldsMatchOrdered(output.toString());
}
private static void checkFirstByFieldsNoMatch(String json) {
assertEquals("{}", json, "There should have been no document returned");
}
public static void firstByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByFieldsNoMatch(jsonFirstByFields(db.getConn(), TEST_TABLE, List.of(Field.equal("value", "absent"))));
}
public static void writeFirstByFieldsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByFields(db.getConn(), TEST_TABLE, writer, List.of(Field.equal("value", "absent")));
checkFirstByFieldsNoMatch(output.toString());
}
private static void checkFirstByContainsMatchOne(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(JsonDocument.one, json, "An incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.contains(docId("one")), String.format("An incorrect document was returned (%s)", json));
break;
}
}
public static void firstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByContainsMatchOne(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "FIRST!")));
}
public static void writeFirstByContainsMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "FIRST!"));
checkFirstByContainsMatchOne(output.toString());
}
private static void checkFirstByContainsMatchMany(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertTrue(json.contains(JsonDocument.four) || json.contains(JsonDocument.five),
String.format("Expected document 'four' or 'five' (%s)", json));
break;
case POSTGRESQL:
assertTrue(json.contains(docId("four")) || json.contains(docId("five")),
String.format("Expected document 'four' or 'five' (%s)", json));
break;
}
}
public static void firstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByContainsMatchMany(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple")));
}
public static void writeFirstByContainsMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "purple"));
checkFirstByContainsMatchMany(output.toString());
}
private static void checkFirstByContainsMatchOrdered(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(JsonDocument.five, json, "An incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.contains(docId("five")),
String.format("An incorrect document was returned (%s)", json));
break;
}
}
public static void firstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByContainsMatchOrdered(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "purple"),
List.of(Field.named("sub.bar NULLS FIRST"))));
}
public static void writeFirstByContainsMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "purple"),
List.of(Field.named("sub.bar NULLS FIRST")));
checkFirstByContainsMatchOrdered(output.toString());
}
private static void checkFirstByContainsNoMatch(String json) {
assertEquals("{}", json, "There should have been no document returned");
}
public static void firstByContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByContainsNoMatch(jsonFirstByContains(db.getConn(), TEST_TABLE, Map.of("value", "indigo")));
}
public static void writeFirstByContainsNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByContains(db.getConn(), TEST_TABLE, writer, Map.of("value", "indigo"));
checkFirstByContainsNoMatch(output.toString());
}
private static void checkFirstByJsonPathMatchOne(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(JsonDocument.two, json, "An incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.contains(docId("two")), String.format("An incorrect document was returned (%s)", json));
break;
}
}
public static void firstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByJsonPathMatchOne(jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ == 10)"));
}
public static void writeFirstByJsonPathMatchOne(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ == 10)");
checkFirstByJsonPathMatchOne(output.toString());
}
private static void checkFirstByJsonPathMatchMany(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertTrue(json.contains(JsonDocument.four) || json.contains(JsonDocument.five),
String.format("Expected document 'four' or 'five' (%s)", json));
break;
case POSTGRESQL:
assertTrue(json.contains(docId("four")) || json.contains(docId("five")),
String.format("Expected document 'four' or 'five' (%s)", json));
break;
}
}
public static void firstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByJsonPathMatchMany(jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)"));
}
public static void writeFirstByJsonPathMatchMany(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 10)");
checkFirstByJsonPathMatchMany(output.toString());
}
private static void checkFirstByJsonPathMatchOrdered(String json) throws DocumentException {
switch (Configuration.dialect()) {
case SQLITE:
assertEquals(JsonDocument.four, json, "An incorrect document was returned");
break;
case POSTGRESQL:
assertTrue(json.contains(docId("four")),
String.format("An incorrect document was returned (%s)", json));
break;
}
}
public static void firstByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByJsonPathMatchOrdered(jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 10)",
List.of(Field.named("id DESC"))));
}
public static void writeFirstByJsonPathMatchOrdered(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 10)",
List.of(Field.named("id DESC")));
checkFirstByJsonPathMatchOrdered(output.toString());
}
private static void checkFirstByJsonPathNoMatch(String json) {
assertEquals("{}", json, "There should have been no document returned");
}
public static void firstByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
checkFirstByJsonPathNoMatch(jsonFirstByJsonPath(db.getConn(), TEST_TABLE, "$.numValue ? (@ > 100)"));
}
public static void writeFirstByJsonPathNoMatch(ThrowawayDatabase db) throws DocumentException {
JsonDocument.load(db);
final StringWriter output = new StringWriter();
final PrintWriter writer = new PrintWriter(output);
writeJsonFirstByJsonPath(db.getConn(), TEST_TABLE, writer, "$.numValue ? (@ > 100)");
checkFirstByJsonPathNoMatch(output.toString());
}
}

View File

@ -0,0 +1,32 @@
package solutions.bitbadger.documents.core.tests.java.integration;
public class NumIdDocument {
private int key;
private String text;
public int getKey() {
return key;
}
public void setKey(int key) {
this.key = key;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public NumIdDocument(int key, String text) {
this.key = key;
this.text = text;
}
public NumIdDocument() {
this(0, "");
}
}

Some files were not shown because too many files have changed in this diff Show More