Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for java 8 OffsetDateTime via postgres timestamp with time zone #8

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<version>0.2.5-SNAPSHOT</version>

<properties>
<kotlin.version>1.3.0</kotlin.version>
<kotlin.version>1.3.21</kotlin.version>
<dokka.version>0.9.17</dokka.version>
</properties>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import java.time.*
import kotlin.reflect.*

open class JDBCDataConversion {
open fun convertValueToDatabase(value: Any?): Any? {
open fun convertValueToDatabase(value: Any?, connection: Connection): Any? {
if (value == null)
return null
return when (value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ open class JDBCTransaction(override val connection: JDBCConnection) : Transactio
prepareStatement(statementSQL.sql, arrayOf(connection.dialect.idSQL(returnColumn.name)))

statementSQL.arguments.forEach { arg ->
preparedStatement.setObject(arg.index + 1, connection.conversion.convertValueToDatabase(arg.value))
preparedStatement.setObject(arg.index + 1, connection.conversion.convertValueToDatabase(arg.value, preparedStatement.connection))
}
return preparedStatement
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ package org.jetbrains.squash.dialects.mysql
import org.jetbrains.squash.connection.*
import org.jetbrains.squash.drivers.*
import java.nio.*
import java.sql.Connection
import java.util.*
import kotlin.reflect.*


class MySqlDataConversion : JDBCDataConversion() {
override fun convertValueToDatabase(value: Any?): Any? {
override fun convertValueToDatabase(value: Any?, connection: Connection): Any? {
if (value is UUID) {
val bb = ByteBuffer.wrap(ByteArray(16))
bb.putLong(value.mostSignificantBits)
bb.putLong(value.leastSignificantBits)
return bb.array()
}
return super.convertValueToDatabase(value)
return super.convertValueToDatabase(value, connection)
}

override fun convertValueFromDatabase(value: Any?, type: KClass<*>): Any? {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.jetbrains.squash.definition

import java.time.OffsetDateTime

/**
* Creates a [OffsetDateTime] column
*/
fun TableDefinition.offsetDatetime(name: String): ColumnDefinition<OffsetDateTime> {
return createColumn(name, OffsetDateTimeColumnType)
}

/**
* Creates a [Int[]] column
*/
fun TableDefinition.intArray(name: String): ColumnDefinition<Array<Int>> {
return createColumn(name, IntArrayColumnType)
}

/**
* Creates a [String[]] column
*/
fun TableDefinition.textArray(name: String): ColumnDefinition<Array<String>> {
return createColumn(name, TextArrayColumnType)
}

/**
* Creates a [Json] column
*/
fun TableDefinition.jsonb(name: String): ColumnDefinition<Json> {
return createColumn(name, JsonbColumnType)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.jetbrains.squash.definition

import java.time.OffsetDateTime

object OffsetDateTimeColumnType : ColumnType(OffsetDateTime::class)
object IntArrayColumnType : ColumnType(Array<Int>::class)
object TextArrayColumnType : ColumnType(Array<String>::class)
object JsonbColumnType : ColumnType(Json::class)
3 changes: 3 additions & 0 deletions squash-postgres/src/org/jetbrains/squash/definition/Json.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.jetbrains.squash.definition

data class Json(val json: String?)
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
package org.jetbrains.squash.dialects.postgres

import org.jetbrains.squash.drivers.*
import org.jetbrains.squash.definition.Json
import org.jetbrains.squash.drivers.JDBCDataConversion
import org.jetbrains.squash.drivers.JDBCResponseColumn
import org.postgresql.util.PGobject
import java.sql.Connection
import java.sql.ResultSet
import java.time.OffsetDateTime
import kotlin.reflect.KClass

class PgDataConversion : JDBCDataConversion()
class PgDataConversion : JDBCDataConversion() {
override fun convertValueToDatabase(value: Any?, connection: Connection): Any? {
return when {
value is OffsetDateTime -> { value }
Array<Int>::class.isInstance(value) -> {
val typedArray = value as Array<Int>
connection.createArrayOf("int4", typedArray)
}
Array<String>::class.isInstance(value) -> {
val typedArray = value as Array<String>
connection.createArrayOf("text", typedArray)
}
Json::class.isInstance(value) -> {
val json = value as Json
PGobject().apply {
this.type = "json"
this.value = json.json
}
}
else -> super.convertValueToDatabase(value, connection)
}
}

override fun convertValueFromDatabase(value: Any?, type: KClass<*>): Any? {
return when (value) {
type == java.time.OffsetDateTime::class && value is java.time.OffsetDateTime -> { value }
type == kotlin.Array<Int>::class && value is java.sql.Array -> {
val array = value as java.sql.Array
array.array as Array<Int>
}
type == kotlin.Array<String>::class && value is java.sql.Array -> {
val array = value as java.sql.Array
array.array as Array<String>
}
type == Json::class && value is Json -> {
val json = value as Json
json.json
}
else -> super.convertValueFromDatabase(value, type)
}
}

override fun fetch(resultSet: ResultSet, dbColumnIndex: Int, column: JDBCResponseColumn): Any? {
return when (column.databaseType) {
"_int4" -> resultSet.getArray(dbColumnIndex)?.array as Array<Int>?
"_text" -> resultSet.getArray(dbColumnIndex)?.array as Array<String>?
"timestamptz" -> resultSet.getObject(dbColumnIndex, OffsetDateTime::class.java)
"json" -> Json(resultSet.getString(dbColumnIndex))
else -> super.fetch(resultSet, dbColumnIndex, column)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.jetbrains.squash.dialects.postgres

import org.jetbrains.squash.definition.*
import org.jetbrains.squash.dialect.*
import org.jetbrains.squash.dialect.BaseDefinitionSQLDialect
import org.jetbrains.squash.dialect.BaseSQLDialect
import org.jetbrains.squash.dialect.DefinitionSQLDialect
import org.jetbrains.squash.dialect.SQLStatementBuilder
import org.jetbrains.squash.expressions.Expression
import org.jetbrains.squash.expressions.ArrayInExpression
import org.jetbrains.squash.expressions.ArrayOverlapExpression

object PgDialect : BaseSQLDialect("Postgres") {
override val definition: DefinitionSQLDialect = object : BaseDefinitionSQLDialect(this) {
Expand Down Expand Up @@ -32,8 +38,43 @@ object PgDialect : BaseSQLDialect("Postgres") {
is BlobColumnType -> builder.append("BYTEA")
is BinaryColumnType -> builder.append("BYTEA")
is DateTimeColumnType -> builder.append("TIMESTAMP")
is OffsetDateTimeColumnType -> builder.append("TIMESTAMP WITH TIME ZONE")
is IntArrayColumnType -> builder.append("INT[]")
is TextArrayColumnType -> builder.append("TEXT[]")
is JsonbColumnType -> builder.append("JSONB")
else -> super.columnTypeSQL(builder, type)
}
}
}

override fun <T> appendExpression(builder: SQLStatementBuilder, expression: Expression<T>): Unit = with(builder) {
when (expression) {
is ArrayInExpression<*> -> {
appendExpression(this, expression.value)
append(" @> ARRAY[")
expression.values.forEachIndexed { index, value ->
if (index > 0)
append(", ")
appendLiteralSQL(this, value)
}
append("]")
}
is ArrayOverlapExpression<*> -> {
appendExpression(this, expression.value)
append(" && ARRAY[")
var anyValueIsString = false
expression.values.forEachIndexed { index, value ->
if (index > 0)
append(", ")
appendLiteralSQL(this, value)
if (value is String)
anyValueIsString = true
}
append("]")
if (anyValueIsString)
append("::text[]")
}
else -> super.appendExpression(builder, expression)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.jetbrains.squash.expressions

class ArrayInExpression<out V>(val value: Expression<V>, val values: Collection<V>) : Expression<Boolean>
class ArrayOverlapExpression<out V>(val value: Expression<V>, val values: Collection<V>) : Expression<Boolean>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.jetbrains.squash.expressions

infix fun <V> Expression<V>.contains(values: Collection<V>): ArrayInExpression<V> = ArrayInExpression(this, values)
infix fun <V> Expression<V>.containsAny(values: Collection<V>): ArrayOverlapExpression<V> = ArrayOverlapExpression(this, values)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.jetbrains.squash.dialects.postgres.tests

import org.jetbrains.squash.definition.*

object PgDialectColumnTypes : TableDefinition() {
val id = integer("id").autoIncrement().primaryKey()
val offsetdatetime = offsetDatetime("offsetdatetime")
val notnullIntarray = intArray("intarray")
val nullableIntarray = intArray("nullable_intarray").nullable()
val notnullTextarray = textArray("textarray")
val nullableTextarray = textArray("nullable_textarray").nullable()
val notnullJsonb = jsonb("notnull_jsonb")
val nullableJsonb = jsonb("nullable_jsonb").nullable()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.jetbrains.squash.dialects.postgres.tests

import org.jetbrains.squash.connection.Transaction
import org.jetbrains.squash.definition.IntColumnType
import org.jetbrains.squash.definition.Json
import org.jetbrains.squash.expressions.contains
import org.jetbrains.squash.expressions.containsAny
import org.jetbrains.squash.query.from
import org.jetbrains.squash.query.where
import org.jetbrains.squash.results.ResultRow
import org.jetbrains.squash.results.get
import org.jetbrains.squash.statements.insertInto
import org.jetbrains.squash.statements.values
import org.jetbrains.squash.tests.DatabaseTests
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue

class PgDialectColumnTypesTests: DatabaseTests by PgDatabaseTests() {
private val dialectColumnsTableSQL: String get() = "CREATE TABLE IF NOT EXISTS PgDialectColumnTypes (" +
"id ${getIdColumnType(IntColumnType)}, " +
"offsetdatetime TIMESTAMP WITH TIME ZONE NOT NULL, " +
"intarray INT[] NOT NULL, " +
"nullable_intarray INT[] NULL, " +
"textarray TEXT[] NOT NULL, " +
"nullable_textarray TEXT[] NULL, " +
"notnull_jsonb JSONB NOT NULL, " +
"nullable_jsonb JSONB NULL, " +
"CONSTRAINT PK_PgDialectColumnTypes PRIMARY KEY (id))"

@Test fun sql() {
withTransaction {
connection.dialect.definition.tableSQL(PgDialectColumnTypes).assertSQL { dialectColumnsTableSQL }
}
}

@Test fun insert() {
withTables(PgDialectColumnTypes) {
insertData()
}
}

private val offsetDate = OffsetDateTime.of(1976, 11, 24, 12, 1, 1, 0, ZoneOffset.ofHours(-6))
private val arrayOfInt = arrayOf(1, 2, 3, 4)
private val arrayOfString = arrayOf("see", "spot", "run")
private val jsonb = Json("{}")

@Test fun query() {
withTables(PgDialectColumnTypes) {
insertData()

fun checkRow(row: ResultRow) {
assertEquals(OffsetDateTime::class.java, row[PgDialectColumnTypes.offsetdatetime].javaClass)
assertEquals(offsetDate.toEpochSecond(), row[PgDialectColumnTypes.offsetdatetime].toEpochSecond())

assertEquals(Array<Int>::class.java, row[PgDialectColumnTypes.notnullIntarray].javaClass)
assertTrue { Arrays.equals(arrayOfInt, row[PgDialectColumnTypes.notnullIntarray]) }

assertEquals(Array<String>::class.java, row[PgDialectColumnTypes.notnullTextarray].javaClass)
assertTrue { Arrays.equals(arrayOfString, row[PgDialectColumnTypes.notnullTextarray]) }
}

val containsRow = from(PgDialectColumnTypes).where { PgDialectColumnTypes.notnullIntarray contains arrayOfInt.take(2) }.execute().single()
checkRow(containsRow)

val containsAnyRow = from(PgDialectColumnTypes).where { PgDialectColumnTypes.notnullTextarray containsAny listOf(arrayOfString.first(), "other")}.execute().single()
checkRow(containsAnyRow)

val noRow = from(PgDialectColumnTypes).where { PgDialectColumnTypes.notnullIntarray contains listOf(9) }.execute().singleOrNull()
assertNull(noRow)
}
}

private fun Transaction.insertData() {
insertInto(PgDialectColumnTypes).values {
it[offsetdatetime] = offsetDate
it[notnullIntarray] = arrayOfInt
it[nullableIntarray] = null
it[notnullTextarray] = arrayOfString
it[nullableTextarray] = arrayOfString
it[notnullJsonb] = jsonb
it[nullableJsonb] = null
}.execute()
}
}
38 changes: 36 additions & 2 deletions squash.iml
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_5" inherit-compiler-output="false">
<component name="FacetManager">
<facet type="kotlin-language" name="Kotlin">
<configuration version="3" platform="JVM 1.8" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xallow-kotlin-package" />
</compilerSettings>
<compilerArguments>
<option name="jvmTarget" value="1.8" />
<option name="languageVersion" value="1.3" />
<option name="apiVersion" value="1.3" />
<option name="pluginOptions">
<array />
</option>
<option name="pluginClasspaths">
<array />
</option>
<option name="errors">
<ArgumentParseErrors />
</option>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_5">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
Expand All @@ -16,8 +39,19 @@
<orderEntry type="library" scope="TEST" name="Maven: org.jetbrains.kotlin:kotlin-test-junit:1.0.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.jetbrains.kotlin:kotlin-test:1.0.1" level="project" />
<orderEntry type="library" name="Maven: org.jetbrains.kotlinx:kotlinx-support-jdk7:0.1-alpha-2" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.6.5" level="project" />
<orderEntry type="library" name="Maven: org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.21" level="project" />
<orderEntry type="library" name="Maven: org.jetbrains.kotlin:kotlin-stdlib:1.3.21" level="project" />
<orderEntry type="library" name="Maven: org.jetbrains.kotlin:kotlin-stdlib-common:1.3.21" level="project" />
<orderEntry type="library" name="Maven: org.jetbrains:annotations:13.0" level="project" />
<orderEntry type="library" name="Maven: org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.21" level="project" />
<orderEntry type="library" name="Maven: org.jetbrains.kotlin:kotlin-reflect:1.3.21" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.jetbrains.kotlin:kotlin-test-junit:1.3.21" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.jetbrains.kotlin:kotlin-test-annotations-common:1.3.21" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.jetbrains.kotlin:kotlin-test:1.3.21" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.jetbrains.kotlin:kotlin-test-common:1.3.21" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.6.5" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.25" level="project" />
</component>
</module>