Initial Development (#1)

This project now contains:
- A generic JVM document library (with Kotlin extensions on the JDBC `Connection` object)
- A Groovy library which adds extension methods to the `Connection` object
- A Scala library, which uses native Scala collections and adds Scala-style extension methods to the `Connection` object
- A Kotlin library which uses `kotlinx.serialization` (no reflection, reified generic types) along with `Connection` extensions

Reviewed-on: #1
This commit is contained in:
Daniel J. Summers 2025-04-16 01:29:19 +00:00
parent 995f565255
commit d202c002b5
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/
# 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
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, ()