diff --git a/build.gradle b/build.gradle index e99a089..6b64002 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ buildscript { hsqlVersion = '2.3.4' postgresVersion = '9.4.1208' mysqlVersion = '5.1.38' + mssqlVersion = '6.3.6.jre8-preview' jerseyVersion = '2.23.1' dropwizardVersion = '1.0.0' jacksonVersion = '2.7.0' @@ -143,6 +144,7 @@ project(':core') { "org.postgresql:postgresql:$postgresVersion", "mysql:mysql-connector-java:$mysqlVersion", "org.xerial:sqlite-jdbc:3.8.11.2", + "com.microsoft.sqlserver:mssql-jdbc:$mssqlVersion", ) } } diff --git a/core/src/main/kotlin/com/github/andrewoma/kwery/core/DefaultSession.kt b/core/src/main/kotlin/com/github/andrewoma/kwery/core/DefaultSession.kt index a491f51..918d8ac 100644 --- a/core/src/main/kotlin/com/github/andrewoma/kwery/core/DefaultSession.kt +++ b/core/src/main/kotlin/com/github/andrewoma/kwery/core/DefaultSession.kt @@ -229,8 +229,12 @@ class DefaultSession(override val connection: Connection, return DefaultTransaction(this) } - private fun withPreparedStatement(sql: String, parameters: List> = listOf(), options: StatementOptions, - f: (ExecutingStatement, PreparedStatement) -> Pair): R { + private fun withPreparedStatement( + sql: String, + parameters: List> = listOf(), + options: StatementOptions, + f: (ExecutingStatement, PreparedStatement) -> Pair + ): R { var statement = ExecutingStatement(this, hashMapOf(), sql, parameters, options) try { statement = interceptor.construct(statement) @@ -263,10 +267,13 @@ class DefaultSession(override val connection: Connection, } } - private fun createStatementCacheKey(options: StatementOptions, sql: String, statement: ExecutingStatement): StatementCacheKey { - return StatementCacheKey(sql, statement.inClauseSizes, if (options.applyNameToQuery) options.name else null, - options.limit != null, options.offset != null) - } + private fun createStatementCacheKey(options: StatementOptions, sql: String, statement: ExecutingStatement) = StatementCacheKey( + sql = sql, + collections = statement.inClauseSizes, + name = if (options.applyNameToQuery) options.name else null, + limit = options.limit != null, + offset = options.offset != null + ) private fun prepareStatement(sql: String, options: StatementOptions): PreparedStatement { val statement = if (options.useGeneratedKeys || options.generatedKeyColumns.isNotEmpty()) { @@ -358,5 +365,10 @@ data class ExecutingStatement( /** * StatementCacheKey contains the sql and any options that modify the generated prepared statement */ -data class StatementCacheKey(val sql: String, val collections: Map, val name: String?, - val limit: Boolean, val offset: Boolean) +data class StatementCacheKey( + val sql: String, + val collections: Map, + val name: String?, + val limit: Boolean, + val offset: Boolean +) diff --git a/core/src/main/kotlin/com/github/andrewoma/kwery/core/dialect/Dialect.kt b/core/src/main/kotlin/com/github/andrewoma/kwery/core/dialect/Dialect.kt index 273e89f..1bcfb0d 100644 --- a/core/src/main/kotlin/com/github/andrewoma/kwery/core/dialect/Dialect.kt +++ b/core/src/main/kotlin/com/github/andrewoma/kwery/core/dialect/Dialect.kt @@ -45,7 +45,7 @@ interface Dialect { val supportsFetchingGeneratedKeysByName: Boolean fun arrayBasedIn(name: String): String - + fun arrayBasedIn(paramName: String, values: Collection): String = throw TODO("Not Implemented") fun allocateIds(count: Int, sequence: String, columnName: String): String fun applyLimitAndOffset(limit: Int?, offset: Int?, sql: String): String { diff --git a/core/src/main/kotlin/com/github/andrewoma/kwery/core/dialect/SqlServerDialect.kt b/core/src/main/kotlin/com/github/andrewoma/kwery/core/dialect/SqlServerDialect.kt new file mode 100644 index 0000000..6641983 --- /dev/null +++ b/core/src/main/kotlin/com/github/andrewoma/kwery/core/dialect/SqlServerDialect.kt @@ -0,0 +1,33 @@ +package com.github.andrewoma.kwery.core.dialect + +import java.sql.Date +import java.sql.Time +import java.sql.Timestamp + + +open class SqlServerDialect : Dialect { + override fun bind(value: Any, limit: Int) = when (value) { + is String -> escapeSingleQuotedString(value.truncate(limit)) + is Timestamp -> timestampFormat.get().format(value) + is Date -> "'$value'" + is Time -> "'$value'" + else -> value.toString() + } + + override fun arrayBasedIn(name: String) = TODO("Fail") + + override val supportsArrayBasedIn = false + + override val supportsAllocateIds = false + + override fun allocateIds(count: Int, sequence: String, columnName: String) = throw UnsupportedOperationException() + + override val supportsFetchingGeneratedKeysByName = false + + override fun applyLimitAndOffset(limit: Int?, offset: Int?, sql: String) = when { + limit != null && offset != null -> "$sql OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY" + offset != null -> "$sql OFFSET $offset ROWS" + limit != null -> "$sql OFFSET 0 ROWS FETCH NEXT $limit ROWS ONLY" + else -> sql + } +} diff --git a/core/src/test/kotlin/com/github/andrewoma/kwery/core/AbstractDialectTest.kt b/core/src/test/kotlin/com/github/andrewoma/kwery/core/AbstractDialectTest.kt index c9c79e6..f1b5ad1 100644 --- a/core/src/test/kotlin/com/github/andrewoma/kwery/core/AbstractDialectTest.kt +++ b/core/src/test/kotlin/com/github/andrewoma/kwery/core/AbstractDialectTest.kt @@ -22,10 +22,7 @@ package com.github.andrewoma.kwery.core -import com.github.andrewoma.kwery.core.dialect.Dialect -import com.github.andrewoma.kwery.core.dialect.MysqlDialect -import com.github.andrewoma.kwery.core.dialect.PostgresDialect -import com.github.andrewoma.kwery.core.dialect.SqliteDialect +import com.github.andrewoma.kwery.core.dialect.* import com.zaxxer.hikari.pool.ProxyConnection import org.junit.Test import org.postgresql.largeobject.LargeObjectManager @@ -52,8 +49,16 @@ abstract class AbstractDialectTest(dataSource: DataSource, dialect: Dialect) : A session.update("delete from test") } - data class Value(val time: Time, val date: Date, val timestamp: Timestamp, val binary: String, - val varchar: String, val blob: String, val clob: String, val ints: List) + data class Value( + val time: Time, + val date: Date, + val timestamp: Timestamp, + val binary: String, + val varchar: String, + val blob: String, + val clob: String, + val ints: List + ) @Test fun `Array based select should work inlined`() { if (!dialect.supportsArrayBasedIn) return @@ -74,16 +79,20 @@ abstract class AbstractDialectTest(dataSource: DataSource, dialect: Dialect) : A } @Test fun `Bindings to blobs and clobs via streams`() { - if (session.dialect is PostgresDialect || session.dialect is SqliteDialect) return + if ( + session.dialect is PostgresDialect || + session.dialect is SqliteDialect || + session.dialect is SqlServerDialect + ) return val now = System.currentTimeMillis() val value = Value(Time(now), Date(now), Timestamp(now), "binary", - "var'char", "blob", "clob", listOf(1, 2, 3)) + "var'char", "blob", "clob", listOf(1, 2, 3)) val params = createParams(value) + mapOf( - "id" to "streams", - "blob_col" to ByteArrayInputStream(value.blob.toByteArray(Charsets.UTF_8)), - "clob_col" to StringReader(value.clob) + "id" to "streams", + "blob_col" to ByteArrayInputStream(value.blob.toByteArray(Charsets.UTF_8)), + "clob_col" to StringReader(value.clob) ) assertEquals(1, session.update(insertSql, params)) @@ -105,7 +114,7 @@ abstract class AbstractDialectTest(dataSource: DataSource, dialect: Dialect) : A @Test fun `Bindings to literals should return the same values when fetched`() { val now = System.currentTimeMillis() val value = Value(Time(now), Date(now), Timestamp(now), "binary", - "var'char", "blob", "clob", listOf(1, 2, 3)) + "var'char", "blob", "clob", listOf(1, 2, 3)) session.update(insertSql, createParams(value) + mapOf("id" to "params")) @@ -120,14 +129,14 @@ abstract class AbstractDialectTest(dataSource: DataSource, dialect: Dialect) : A } private fun createParams(value: Value) = mapOf( - "time_col" to value.time, - "date_col" to value.date, - "timestamp_col" to value.timestamp, - "binary_col" to value.binary.toByteArray(Charsets.UTF_8), - "varchar_col" to value.varchar, - "blob_col" to toBlob(value.blob), - "clob_col" to toClob(value.clob), - "array_col" to if (dialect is MysqlDialect) "" else session.connection.createArrayOf("int", value.ints.toTypedArray()) + "time_col" to value.time, + "date_col" to value.date, + "timestamp_col" to value.timestamp, + "binary_col" to value.binary.toByteArray(Charsets.UTF_8), + "varchar_col" to value.varchar, + "blob_col" to toBlob(value.blob), + "clob_col" to toClob(value.clob), + "array_col" to if (dialect is MysqlDialect || dialect is SqlServerDialect) "" else session.connection.createArrayOf("int", value.ints.toTypedArray()) ) @Test fun `Allocate ids should contain a unique sequence of ids`() { @@ -197,13 +206,13 @@ abstract class AbstractDialectTest(dataSource: DataSource, dialect: Dialect) : A private fun findById(id: String): Value { return session.select("select * from dialect_test where id = '$id'") { row -> Value(row.time("time_col"), - row.date("date_col"), - row.timestamp("timestamp_col"), - String(row.bytes("binary_col"), Charsets.UTF_8), - row.string("varchar_col"), - fromBlob(row, "blob_col"), - fromClob(row, "clob_col"), - if (dialect is MysqlDialect || dialect is SqliteDialect) listOf() else row.array("array_col")) + row.date("date_col"), + row.timestamp("timestamp_col"), + String(row.bytes("binary_col"), Charsets.UTF_8), + row.string("varchar_col"), + fromBlob(row, "blob_col"), + fromClob(row, "clob_col"), + if (dialect is MysqlDialect || dialect is SqliteDialect) listOf() else row.array("array_col")) }.single() } @@ -258,4 +267,4 @@ abstract class AbstractDialectTest(dataSource: DataSource, dialect: Dialect) : A assertEquals(expected.timestamp.toString(), actual.timestamp.toString()) // travis doesn't support millis } } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/com/github/andrewoma/kwery/core/Datasources.kt b/core/src/test/kotlin/com/github/andrewoma/kwery/core/Datasources.kt index 603fc46..854f5ba 100644 --- a/core/src/test/kotlin/com/github/andrewoma/kwery/core/Datasources.kt +++ b/core/src/test/kotlin/com/github/andrewoma/kwery/core/Datasources.kt @@ -38,6 +38,13 @@ val mysqlDataSource = HikariDataSource().apply { password = "kwery" } +val sqlserverDataSource = HikariDataSource().apply { + jdbcUrl = "jdbc:sqlserver://localhost:1433;databaseName=model" + username = "sa" + password = "yourStrong(!)Password" +} + + val sqliteDataSource = HikariDataSource().apply { jdbcUrl = "jdbc:sqlite::memory:" } diff --git a/core/src/test/kotlin/com/github/andrewoma/kwery/core/SqlServerDialectTest.kt b/core/src/test/kotlin/com/github/andrewoma/kwery/core/SqlServerDialectTest.kt new file mode 100644 index 0000000..5c3ae70 --- /dev/null +++ b/core/src/test/kotlin/com/github/andrewoma/kwery/core/SqlServerDialectTest.kt @@ -0,0 +1,31 @@ +package com.github.andrewoma.kwery.core + +import com.github.andrewoma.kwery.core.dialect.SqlServerDialect + +class SqlServerDialectTest : AbstractDialectTest(sqlserverDataSource, SqlServerDialect()) { + //language=tsql + override val sql = """ + drop table if exists dialect_test; + + create table dialect_test ( + id varchar(255), + time_col time, + date_col date, + timestamp_col timestamp, + -- timestamp_col timestamp(3), Waiting on travis to support this + binary_col binary(50), + varchar_col varchar(1000), + blob_col binary(50), + clob_col text, + array_col text -- Not supported + ); + + drop table if exists test; + + create table test ( + id varchar(255), + value varchar(255) + ) + """.trimIndent() + +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4998104 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.6' + +services: + mysql: + image: mysql + environment: + MYSQL_DATABASE: kwery + MYSQL_ROOT_PASSWORD: kwery + ports: + - 3306:3306 + + postgres: + image: postgres + environment: + POSTGRES_DB: kwery + POSTGRES_USER: postgres + POSTGRES_PASSWORD: kwery + ports: + - 5432:5432 + + sqlserver: + image: mcr.microsoft.com/mssql/server + environment: + ACCEPT_EULA: Y + SA_PASSWORD: yourStrong(!)Password + ports: + - 1433:1433 diff --git a/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/AbstractDao.kt b/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/AbstractDao.kt index 4d63339..d09ad3b 100644 --- a/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/AbstractDao.kt +++ b/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/AbstractDao.kt @@ -432,6 +432,16 @@ abstract class AbstractDao( id(table.rowMapper(table.idColumns, nf)(row)) } } + + private fun Session.createArrayOf(key: String, sqlType: String?, array: kotlin.Array): Array { + return connection.createArrayOf(sqlType, array) + } + + private fun Session.createWheresOf(key: String, sqlType: String?, array: kotlin.Array): Map { + return array.mapIndexed { index, value -> + "$key$index" to value + }.toMap() + } } enum class IdStrategy {