Initial Development #1
src
@ -12,6 +12,8 @@
 | 
			
		||||
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
			
		||||
        <kotlin.code.style>official</kotlin.code.style>
 | 
			
		||||
        <kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
 | 
			
		||||
        <kotlin.version>2.1.0</kotlin.version>
 | 
			
		||||
        <serialization.version>1.8.0</serialization.version>
 | 
			
		||||
    </properties>
 | 
			
		||||
 | 
			
		||||
    <repositories>
 | 
			
		||||
@ -28,7 +30,7 @@
 | 
			
		||||
            <plugin>
 | 
			
		||||
                <groupId>org.jetbrains.kotlin</groupId>
 | 
			
		||||
                <artifactId>kotlin-maven-plugin</artifactId>
 | 
			
		||||
                <version>2.1.0</version>
 | 
			
		||||
                <version>${kotlin.version}</version>
 | 
			
		||||
                <executions>
 | 
			
		||||
                    <execution>
 | 
			
		||||
                        <id>compile</id>
 | 
			
		||||
@ -45,6 +47,18 @@
 | 
			
		||||
                        </goals>
 | 
			
		||||
                    </execution>
 | 
			
		||||
                </executions>
 | 
			
		||||
                <configuration>
 | 
			
		||||
                    <compilerPlugins>
 | 
			
		||||
                        <plugin>kotlinx-serialization</plugin>
 | 
			
		||||
                    </compilerPlugins>
 | 
			
		||||
                </configuration>
 | 
			
		||||
                <dependencies>
 | 
			
		||||
                    <dependency>
 | 
			
		||||
                        <groupId>org.jetbrains.kotlin</groupId>
 | 
			
		||||
                        <artifactId>kotlin-maven-serialization</artifactId>
 | 
			
		||||
                        <version>${kotlin.version}</version>
 | 
			
		||||
                    </dependency>
 | 
			
		||||
                </dependencies>
 | 
			
		||||
            </plugin>
 | 
			
		||||
            <plugin>
 | 
			
		||||
                <artifactId>maven-surefire-plugin</artifactId>
 | 
			
		||||
@ -75,18 +89,23 @@
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.jetbrains.kotlin</groupId>
 | 
			
		||||
            <artifactId>kotlin-test-junit5</artifactId>
 | 
			
		||||
            <version>2.1.0</version>
 | 
			
		||||
            <version>${kotlin.version}</version>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.jetbrains.kotlin</groupId>
 | 
			
		||||
            <artifactId>kotlin-stdlib</artifactId>
 | 
			
		||||
            <version>2.1.0</version>
 | 
			
		||||
            <version>${kotlin.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.jetbrains.kotlin</groupId>
 | 
			
		||||
            <artifactId>kotlin-reflect</artifactId>
 | 
			
		||||
            <version>2.0.20</version>
 | 
			
		||||
            <version>${kotlin.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.jetbrains.kotlinx</groupId>
 | 
			
		||||
            <artifactId>kotlinx-serialization-json</artifactId>
 | 
			
		||||
            <version>${serialization.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -54,19 +54,13 @@ enum class AutoId {
 | 
			
		||||
            if (id == null) throw IllegalArgumentException("$idProp not found in document")
 | 
			
		||||
 | 
			
		||||
            if (strategy == NUMBER) {
 | 
			
		||||
                if (id.returnType == Byte::class.createType()) {
 | 
			
		||||
                    return id.call(document) == 0.toByte()
 | 
			
		||||
                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 IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID")
 | 
			
		||||
                }
 | 
			
		||||
                if (id.returnType == Short::class.createType()) {
 | 
			
		||||
                    return id.call(document) == 0.toShort()
 | 
			
		||||
                }
 | 
			
		||||
                if (id.returnType == Int::class.createType()) {
 | 
			
		||||
                    return id.call(document) == 0
 | 
			
		||||
                }
 | 
			
		||||
                if (id.returnType == Long::class.createType()) {
 | 
			
		||||
                    return id.call(document) == 0.toLong()
 | 
			
		||||
                }
 | 
			
		||||
                throw IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (id.returnType == String::class.createType()) {
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,21 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import java.sql.Connection
 | 
			
		||||
import java.sql.DriverManager
 | 
			
		||||
 | 
			
		||||
object Configuration {
 | 
			
		||||
 | 
			
		||||
    // TODO: var jsonOpts = Json { some cool options }
 | 
			
		||||
    /**
 | 
			
		||||
     * JSON serializer; replace to configure with non-default options
 | 
			
		||||
     *
 | 
			
		||||
     * The default sets `encodeDefaults` to `true` and `explicitNulls` to `false`; see
 | 
			
		||||
     * https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md for all configuration options
 | 
			
		||||
     */
 | 
			
		||||
    var json = Json {
 | 
			
		||||
        encodeDefaults = true
 | 
			
		||||
        explicitNulls  = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** The field in which a document's ID is stored */
 | 
			
		||||
    var idField = "id"
 | 
			
		||||
@ -12,4 +25,40 @@ object Configuration {
 | 
			
		||||
 | 
			
		||||
    /** The length of automatic random hex character string */
 | 
			
		||||
    var idStringLength = 16
 | 
			
		||||
 | 
			
		||||
    /** The connection string for the JDBC connection */
 | 
			
		||||
    var connectionString: String? = null
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieve a new connection to the configured database
 | 
			
		||||
     *
 | 
			
		||||
     * @return A new connection to the configured database
 | 
			
		||||
     * @throws IllegalArgumentException If the connection string is not set before calling this
 | 
			
		||||
     */
 | 
			
		||||
    fun dbConn(): Connection {
 | 
			
		||||
        if (connectionString == null) {
 | 
			
		||||
            throw IllegalArgumentException("Please provide a connection string before attempting data access")
 | 
			
		||||
        }
 | 
			
		||||
        return DriverManager.getConnection(connectionString)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var dialectValue: Dialect? = null
 | 
			
		||||
 | 
			
		||||
    /** The dialect in use */
 | 
			
		||||
    val dialect: Dialect
 | 
			
		||||
        get() {
 | 
			
		||||
            if (dialectValue == null) {
 | 
			
		||||
                if (connectionString == null) {
 | 
			
		||||
                    throw IllegalArgumentException("Please provide a connection string before attempting data access")
 | 
			
		||||
                }
 | 
			
		||||
                val it = connectionString!!
 | 
			
		||||
                dialectValue = when {
 | 
			
		||||
                    it.contains("sqlite") -> Dialect.SQLITE
 | 
			
		||||
                    it.contains("postgresql") -> Dialect.POSTGRESQL
 | 
			
		||||
                    else -> throw IllegalArgumentException("Cannot determine dialect from [$it]")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return dialectValue!!
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										40
									
								
								src/common/src/main/kotlin/ConnectionExtensions.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										40
									
								
								src/common/src/main/kotlin/ConnectionExtensions.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
import java.sql.Connection
 | 
			
		||||
import java.sql.ResultSet
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 mapFunc The mapping function between the document and the domain item
 | 
			
		||||
 * @return A list of results for the given query
 | 
			
		||||
 */
 | 
			
		||||
inline fun <reified TDoc> Connection.customList(query: String, parameters: Collection<Parameter<*>>,
 | 
			
		||||
                                          mapFunc: (ResultSet) -> TDoc): List<TDoc> =
 | 
			
		||||
    Parameters.apply(this, query, parameters).use { stmt ->
 | 
			
		||||
        Results.toCustomList(stmt, 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 mapFunc The mapping function between the document and the domain item
 | 
			
		||||
 * @return The document if one matches the query, `null` otherwise
 | 
			
		||||
 */
 | 
			
		||||
inline fun <reified TDoc> Connection.customSingle(query: String, parameters: Collection<Parameter<*>>,
 | 
			
		||||
                                                  mapFunc: (ResultSet) -> TDoc): TDoc? =
 | 
			
		||||
    this.customList("$query LIMIT 1", parameters, mapFunc).singleOrNull()
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Execute a query that returns no results
 | 
			
		||||
 *
 | 
			
		||||
 * @param query The query to retrieve the results
 | 
			
		||||
 * @param parameters Parameters to use for the query
 | 
			
		||||
 */
 | 
			
		||||
fun Connection.customNonQuery(query: String, parameters: Collection<Parameter<*>>) {
 | 
			
		||||
    Parameters.apply(this, query, parameters).use { it.executeUpdate() }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/common/src/main/kotlin/DocumentException.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										9
									
								
								src/common/src/main/kotlin/DocumentException.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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(message: String, cause: Throwable? = null) : Exception(message, cause)
 | 
			
		||||
							
								
								
									
										15
									
								
								src/common/src/main/kotlin/Parameter.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										15
									
								
								src/common/src/main/kotlin/Parameter.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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)")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/common/src/main/kotlin/ParameterType.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										13
									
								
								src/common/src/main/kotlin/ParameterType.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										83
									
								
								src/common/src/main/kotlin/Parameters.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										83
									
								
								src/common/src/main/kotlin/Parameters.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
import java.sql.Connection
 | 
			
		||||
import java.sql.PreparedStatement
 | 
			
		||||
import java.sql.SQLException
 | 
			
		||||
import java.sql.Types
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Functions to assist with the creation and implementation of parameters for SQL queries
 | 
			
		||||
 *
 | 
			
		||||
 * @author Daniel J. Summers <daniel@bitbadger.solutions>
 | 
			
		||||
 */
 | 
			
		||||
object Parameters {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
     */
 | 
			
		||||
    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
 | 
			
		||||
     */
 | 
			
		||||
    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)
 | 
			
		||||
                .let { conn.prepareStatement(it) }
 | 
			
		||||
                .also { stmt ->
 | 
			
		||||
                    replacements.sortedBy { it.first }.map { it.second }.forEachIndexed { index, param ->
 | 
			
		||||
                        val idx = index + 1
 | 
			
		||||
                        when (param.type) {
 | 
			
		||||
                            ParameterType.NUMBER -> {
 | 
			
		||||
                                when (param.value) {
 | 
			
		||||
                                    null     -> stmt.setNull(idx, Types.NULL)
 | 
			
		||||
                                    is Byte  -> stmt.setByte(idx, param.value)
 | 
			
		||||
                                    is Short -> stmt.setShort(idx, param.value)
 | 
			
		||||
                                    is Int   -> stmt.setInt(idx, param.value)
 | 
			
		||||
                                    is Long  -> stmt.setLong(idx, param.value)
 | 
			
		||||
                                    else     -> throw DocumentException(
 | 
			
		||||
                                        "Number parameter must be Byte, Short, Int, or Long (${param.value::class.simpleName})")
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            ParameterType.STRING -> {
 | 
			
		||||
                                when (param.value) {
 | 
			
		||||
                                    null      -> stmt.setNull(idx, Types.NULL)
 | 
			
		||||
                                    is String -> stmt.setString(idx, param.value)
 | 
			
		||||
                                    else      -> stmt.setString(idx, param.value.toString())
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            ParameterType.JSON -> stmt.setString(idx, Configuration.json.encodeToString(param.value))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        } catch (ex: SQLException) {
 | 
			
		||||
            throw DocumentException("Error creating query / binding parameters: ${ex.message}", ex)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -146,21 +146,20 @@ object Query {
 | 
			
		||||
                } else {
 | 
			
		||||
                    Pair<Field<*>, String?>(it, null)
 | 
			
		||||
                }
 | 
			
		||||
            val path =
 | 
			
		||||
                if (field.name.startsWith("n:")) {
 | 
			
		||||
                    val fld = Field.named(field.name.substring(2))
 | 
			
		||||
            val path = when {
 | 
			
		||||
                field.name.startsWith("n:") -> Field.named(field.name.substring(2)).let { fld ->
 | 
			
		||||
                    when (dialect) {
 | 
			
		||||
                        Dialect.POSTGRESQL -> "(${fld.path(dialect)})::numeric"
 | 
			
		||||
                        Dialect.SQLITE -> fld.path(dialect)
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (field.name.startsWith("i:")) {
 | 
			
		||||
                    val p = Field.named(field.name.substring(2)).path(dialect)
 | 
			
		||||
                }
 | 
			
		||||
                field.name.startsWith("i:") ->  Field.named(field.name.substring(2)).path(dialect).let { p ->
 | 
			
		||||
                    when (dialect) {
 | 
			
		||||
                        Dialect.POSTGRESQL -> "LOWER($p)"
 | 
			
		||||
                        Dialect.SQLITE -> "$p COLLATE NOCASE"
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    field.path(dialect)
 | 
			
		||||
                }
 | 
			
		||||
                else -> field.path(dialect)
 | 
			
		||||
            }
 | 
			
		||||
            "$path${direction ?: ""}"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										75
									
								
								src/common/src/main/kotlin/Results.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										75
									
								
								src/common/src/main/kotlin/Results.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
import java.sql.PreparedStatement
 | 
			
		||||
import java.sql.ResultSet
 | 
			
		||||
import java.sql.SQLException
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper functions for handling results
 | 
			
		||||
 */
 | 
			
		||||
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
 | 
			
		||||
     * @return The constructed domain item
 | 
			
		||||
     */
 | 
			
		||||
    inline fun <reified TDoc> fromDocument(field: String, rs: ResultSet): TDoc =
 | 
			
		||||
        Configuration.json.decodeFromString<TDoc>(rs.getString(field))
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a domain item from a document
 | 
			
		||||
     *
 | 
			
		||||
     * @param rs A `ResultSet` set to the row with the document to be constructed<
 | 
			
		||||
     * @return The constructed domain item
 | 
			
		||||
     */
 | 
			
		||||
    inline fun <reified TDoc> fromData(rs: ResultSet): TDoc =
 | 
			
		||||
        fromDocument("data", rs)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
     * @return A list of items from the query's result
 | 
			
		||||
     * @throws DocumentException If there is a problem executing the query
 | 
			
		||||
     */
 | 
			
		||||
    inline fun <reified TDoc> toCustomList(stmt: PreparedStatement, mapFunc: (ResultSet) -> TDoc): List<TDoc> =
 | 
			
		||||
        try {
 | 
			
		||||
            stmt.executeQuery().use {
 | 
			
		||||
                val results = mutableListOf<TDoc>()
 | 
			
		||||
                while (it.next()) {
 | 
			
		||||
                    results.add(mapFunc(it))
 | 
			
		||||
                }
 | 
			
		||||
                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
 | 
			
		||||
     * @return The count from the row
 | 
			
		||||
     */
 | 
			
		||||
    fun toCount(rs: ResultSet): Long =
 | 
			
		||||
        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
 | 
			
		||||
     * @return The true/false value from the row
 | 
			
		||||
     */
 | 
			
		||||
    fun toExists(rs: ResultSet): Boolean =
 | 
			
		||||
        when (Configuration.dialect) {
 | 
			
		||||
            Dialect.POSTGRESQL -> rs.getBoolean("it")
 | 
			
		||||
            Dialect.SQLITE     -> toCount(rs) > 0L
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								src/common/src/test/kotlin/ParameterTest.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										34
									
								
								src/common/src/test/kotlin/ParameterTest.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.DisplayName
 | 
			
		||||
import org.junit.jupiter.api.assertThrows
 | 
			
		||||
import kotlin.test.Test
 | 
			
		||||
import kotlin.test.assertEquals
 | 
			
		||||
import kotlin.test.assertNull
 | 
			
		||||
 | 
			
		||||
class ParameterTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("Construction with colon-prefixed name")
 | 
			
		||||
    fun ctorWithColon() {
 | 
			
		||||
        val p = Parameter(":test", ParameterType.STRING, "ABC")
 | 
			
		||||
        assertEquals(":test", p.name, "Parameter name was incorrect")
 | 
			
		||||
        assertEquals(ParameterType.STRING, p.type, "Parameter type was incorrect")
 | 
			
		||||
        assertEquals("ABC", p.value, "Parameter value was incorrect")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("Construction with at-sign-prefixed name")
 | 
			
		||||
    fun ctorWithAtSign() {
 | 
			
		||||
        val p = Parameter("@yo", ParameterType.NUMBER, null)
 | 
			
		||||
        assertEquals("@yo", p.name, "Parameter name was incorrect")
 | 
			
		||||
        assertEquals(ParameterType.NUMBER, p.type, "Parameter type was incorrect")
 | 
			
		||||
        assertNull(p.value, "Parameter value was incorrect")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("Construction fails with incorrect prefix")
 | 
			
		||||
    fun ctorFailsForPrefix() {
 | 
			
		||||
        assertThrows<DocumentException> { Parameter("it", ParameterType.JSON, "") }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/common/src/test/kotlin/ParametersTest.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										18
									
								
								src/common/src/test/kotlin/ParametersTest.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
package solutions.bitbadger.documents.common
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.DisplayName
 | 
			
		||||
import kotlin.test.Test
 | 
			
		||||
import kotlin.test.assertEquals
 | 
			
		||||
 | 
			
		||||
class ParametersTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("replaceNamesInQuery replaces successfully")
 | 
			
		||||
    fun replaceNamesInQuery() {
 | 
			
		||||
        val parameters = listOf(Parameter(":data", ParameterType.JSON, "{}"),
 | 
			
		||||
            Parameter(":data_ext", ParameterType.STRING, ""))
 | 
			
		||||
        val query = "SELECT data, data_ext FROM tbl WHERE data = :data AND data_ext = :data_ext AND more_data = :data"
 | 
			
		||||
        assertEquals("SELECT data, data_ext FROM tbl WHERE data = ? AND data_ext = ? AND more_data = ?",
 | 
			
		||||
            Parameters.replaceNamesInQuery(query, parameters), "Parameters not replaced correctly")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
package solutions.bitbadger.documents.sqlite
 | 
			
		||||
 | 
			
		||||
import solutions.bitbadger.documents.common.*
 | 
			
		||||
import solutions.bitbadger.documents.common.Configuration
 | 
			
		||||
import solutions.bitbadger.documents.common.Configuration as BaseConfig;
 | 
			
		||||
import solutions.bitbadger.documents.common.Query
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -30,13 +30,16 @@ object Query {
 | 
			
		||||
                }
 | 
			
		||||
                Op.IN -> {
 | 
			
		||||
                    val p = name.derive(it.parameterName)
 | 
			
		||||
                    val values = comp.value as List<*>
 | 
			
		||||
                    val values = comp.value as Collection<*>
 | 
			
		||||
                    val paramNames = values.indices.joinToString(", ") { idx -> "${p}_$idx" }
 | 
			
		||||
                    "${it.path(Dialect.SQLITE, FieldFormat.SQL)} ${comp.op.sql} ($paramNames)"
 | 
			
		||||
                }
 | 
			
		||||
                Op.IN_ARRAY -> {
 | 
			
		||||
                    val p = name.derive(it.parameterName)
 | 
			
		||||
                    val (table, values) = comp.value as Pair<String, List<*>>
 | 
			
		||||
                    @Suppress("UNCHECKED_CAST")
 | 
			
		||||
                    val tableAndValues = comp.value as? Pair<String, Collection<*>>
 | 
			
		||||
                        ?: throw IllegalArgumentException("InArray field invalid")
 | 
			
		||||
                    val (table, values) = tableAndValues
 | 
			
		||||
                    val paramNames = values.indices.joinToString(", ") { idx -> "${p}_$idx" }
 | 
			
		||||
                    "EXISTS (SELECT 1 FROM json_each($table.data, '$.${it.name}') WHERE value IN ($paramNames)"
 | 
			
		||||
                }
 | 
			
		||||
@ -54,7 +57,8 @@ object Query {
 | 
			
		||||
     * @return A `WHERE` clause fragment identifying a document by its ID
 | 
			
		||||
     */
 | 
			
		||||
    fun <TKey> whereById(docId: TKey): String =
 | 
			
		||||
        whereByFields(FieldMatch.ANY, listOf(Field.equal(Configuration.idField, docId).withParameterName(":id")))
 | 
			
		||||
        whereByFields(FieldMatch.ANY,
 | 
			
		||||
            listOf(Field.equal(BaseConfig.idField, docId).withParameterName(":id")))
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create an `UPDATE` statement to patch documents
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										99
									
								
								src/sqlite/src/test/kotlin/QueryTest.kt
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										99
									
								
								src/sqlite/src/test/kotlin/QueryTest.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
			
		||||
package solutions.bitbadger.documents.sqlite
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.DisplayName
 | 
			
		||||
import solutions.bitbadger.documents.common.Field
 | 
			
		||||
import solutions.bitbadger.documents.common.FieldMatch
 | 
			
		||||
import kotlin.test.Test
 | 
			
		||||
import kotlin.test.assertEquals
 | 
			
		||||
 | 
			
		||||
class QueryTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("whereByFields generates for a single field with logical operator")
 | 
			
		||||
    fun whereByFieldSingleLogical() {
 | 
			
		||||
        assertEquals("data->>'theField' > :test",
 | 
			
		||||
            Query.whereByFields(FieldMatch.ANY, listOf(Field.greater("theField", 0).withParameterName(":test"))),
 | 
			
		||||
            "WHERE clause not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("whereByFields generates for a single field with existence operator")
 | 
			
		||||
    fun whereByFieldSingleExistence() {
 | 
			
		||||
        assertEquals("data->>'thatField' IS NULL",
 | 
			
		||||
            Query.whereByFields(FieldMatch.ANY, listOf(Field.notExists("thatField"))), "WHERE clause not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("whereByFields generates for a single field with between operator")
 | 
			
		||||
    fun whereByFieldSingleBetween() {
 | 
			
		||||
        assertEquals("data->>'aField' BETWEEN :rangemin AND :rangemax",
 | 
			
		||||
            Query.whereByFields(FieldMatch.ALL, listOf(Field.between("aField", 50, 99).withParameterName(":range"))),
 | 
			
		||||
            "WHERE clause not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("whereByFields generates for all multiple fields with logical operator")
 | 
			
		||||
    fun whereByFieldAllMultipleLogical() {
 | 
			
		||||
        assertEquals("data->>'theFirst' = :field0 AND data->>'numberTwo' = :field1",
 | 
			
		||||
            Query.whereByFields(FieldMatch.ALL, listOf(Field.equal("theFirst", "1"), Field.equal("numberTwo", "2"))),
 | 
			
		||||
            "WHERE clause not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("whereByFields generates for any multiple fields with existence operator")
 | 
			
		||||
    fun whereByFieldAnyMultipleExistence() {
 | 
			
		||||
        assertEquals("data->>'thatField' IS NULL OR data->>'thisField' >= :field0",
 | 
			
		||||
            Query.whereByFields(FieldMatch.ANY,
 | 
			
		||||
                listOf(Field.notExists("thatField"), Field.greaterOrEqual("thisField", 18))),
 | 
			
		||||
            "WHERE clause not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("whereByFields generates for an In comparison")
 | 
			
		||||
    fun whereByFieldIn() {
 | 
			
		||||
        assertEquals("data->>'this' IN (:field0_0, :field0_1, :field0_2)",
 | 
			
		||||
            Query.whereByFields(FieldMatch.ALL, listOf(Field.any("this", listOf("a", "b", "c")))),
 | 
			
		||||
            "WHERE clause not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("whereByFields generates for an InArray comparison")
 | 
			
		||||
    fun whereByFieldInArray() {
 | 
			
		||||
        assertEquals("EXISTS (SELECT 1 FROM json_each(the_table.data, '$.this') WHERE value IN (:field0_0, :field0_1)",
 | 
			
		||||
            Query.whereByFields(FieldMatch.ALL, listOf(Field.inArray("this", "the_table", listOf("a", "b")))),
 | 
			
		||||
            "WHERE clause not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("whereById generates correctly")
 | 
			
		||||
    fun whereById() {
 | 
			
		||||
        assertEquals("data->>'id' = :id", Query.whereById("abc"), "WHERE clause not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("patch generates the correct query")
 | 
			
		||||
    fun patch() {
 | 
			
		||||
        assertEquals("UPDATE my_table SET data = json_patch(data, json(:data))", Query.patch("my_table"),
 | 
			
		||||
            "Query not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("byId generates the correct query")
 | 
			
		||||
    fun byId() {
 | 
			
		||||
        assertEquals("test WHERE data->>'id' = :id", Query.byId("test", "14"), "By-ID query not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("byFields generates the correct query")
 | 
			
		||||
    fun byFields() {
 | 
			
		||||
        assertEquals("unit WHERE data->>'That' > :field0",
 | 
			
		||||
            Query.byFields("unit", FieldMatch.ANY, listOf(Field.greater("That", 14))), "By-fields query not correct")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @DisplayName("Definition.ensureTable generates the correct query")
 | 
			
		||||
    fun ensureTable() {
 | 
			
		||||
        assertEquals("CREATE TABLE IF NOT EXISTS tbl (data TEXT NOT NULL)", Query.Definition.ensureTable("tbl"),
 | 
			
		||||
            "CREATE TABLE statement not correct")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user