diff --git a/pom.xml b/pom.xml index 32c8c29..7fe94b1 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ 0.2.5-SNAPSHOT - 1.3.0 + 1.3.21 0.9.17 diff --git a/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCDataConversion.kt b/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCDataConversion.kt index 1d91b80..a4ab1e1 100644 --- a/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCDataConversion.kt +++ b/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCDataConversion.kt @@ -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) { diff --git a/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCTransaction.kt b/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCTransaction.kt index 9ba9a05..8dc198f 100644 --- a/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCTransaction.kt +++ b/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCTransaction.kt @@ -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 } diff --git a/squash-mysql/src/org/jetbrains/squash/dialects/mysql/MySqlDataConversion.kt b/squash-mysql/src/org/jetbrains/squash/dialects/mysql/MySqlDataConversion.kt index cfe4184..f0431ac 100644 --- a/squash-mysql/src/org/jetbrains/squash/dialects/mysql/MySqlDataConversion.kt +++ b/squash-mysql/src/org/jetbrains/squash/dialects/mysql/MySqlDataConversion.kt @@ -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? { diff --git a/squash-postgres/src/org/jetbrains/squash/definition/ColumnBuilder.kt b/squash-postgres/src/org/jetbrains/squash/definition/ColumnBuilder.kt new file mode 100644 index 0000000..e2c2590 --- /dev/null +++ b/squash-postgres/src/org/jetbrains/squash/definition/ColumnBuilder.kt @@ -0,0 +1,32 @@ +package org.jetbrains.squash.definition + +import java.time.OffsetDateTime + +/** + * Creates a [OffsetDateTime] column + */ +fun TableDefinition.offsetDatetime(name: String): ColumnDefinition { + return createColumn(name, OffsetDateTimeColumnType) +} + +/** + * Creates a [Int[]] column + */ +fun TableDefinition.intArray(name: String): ColumnDefinition> { + return createColumn(name, IntArrayColumnType) +} + +/** + * Creates a [String[]] column + */ +fun TableDefinition.textArray(name: String): ColumnDefinition> { + return createColumn(name, TextArrayColumnType) +} + +/** + * Creates a [Json] column + */ +fun TableDefinition.jsonb(name: String): ColumnDefinition { + return createColumn(name, JsonbColumnType) +} + diff --git a/squash-postgres/src/org/jetbrains/squash/definition/ColumnType.kt b/squash-postgres/src/org/jetbrains/squash/definition/ColumnType.kt new file mode 100644 index 0000000..7390f4b --- /dev/null +++ b/squash-postgres/src/org/jetbrains/squash/definition/ColumnType.kt @@ -0,0 +1,8 @@ +package org.jetbrains.squash.definition + +import java.time.OffsetDateTime + +object OffsetDateTimeColumnType : ColumnType(OffsetDateTime::class) +object IntArrayColumnType : ColumnType(Array::class) +object TextArrayColumnType : ColumnType(Array::class) +object JsonbColumnType : ColumnType(Json::class) \ No newline at end of file diff --git a/squash-postgres/src/org/jetbrains/squash/definition/Json.kt b/squash-postgres/src/org/jetbrains/squash/definition/Json.kt new file mode 100644 index 0000000..6fb046a --- /dev/null +++ b/squash-postgres/src/org/jetbrains/squash/definition/Json.kt @@ -0,0 +1,3 @@ +package org.jetbrains.squash.definition + +data class Json(val json: String?) \ No newline at end of file diff --git a/squash-postgres/src/org/jetbrains/squash/dialects/postgres/PgDataConversion.kt b/squash-postgres/src/org/jetbrains/squash/dialects/postgres/PgDataConversion.kt index f0b6c79..4da592d 100644 --- a/squash-postgres/src/org/jetbrains/squash/dialects/postgres/PgDataConversion.kt +++ b/squash-postgres/src/org/jetbrains/squash/dialects/postgres/PgDataConversion.kt @@ -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() \ No newline at end of file +class PgDataConversion : JDBCDataConversion() { + override fun convertValueToDatabase(value: Any?, connection: Connection): Any? { + return when { + value is OffsetDateTime -> { value } + Array::class.isInstance(value) -> { + val typedArray = value as Array + connection.createArrayOf("int4", typedArray) + } + Array::class.isInstance(value) -> { + val typedArray = value as Array + 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::class && value is java.sql.Array -> { + val array = value as java.sql.Array + array.array as Array + } + type == kotlin.Array::class && value is java.sql.Array -> { + val array = value as java.sql.Array + array.array as Array + } + 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? + "_text" -> resultSet.getArray(dbColumnIndex)?.array as Array? + "timestamptz" -> resultSet.getObject(dbColumnIndex, OffsetDateTime::class.java) + "json" -> Json(resultSet.getString(dbColumnIndex)) + else -> super.fetch(resultSet, dbColumnIndex, column) + } + } +} \ No newline at end of file diff --git a/squash-postgres/src/org/jetbrains/squash/dialects/postgres/PgDialect.kt b/squash-postgres/src/org/jetbrains/squash/dialects/postgres/PgDialect.kt index 31faf5c..6f29246 100644 --- a/squash-postgres/src/org/jetbrains/squash/dialects/postgres/PgDialect.kt +++ b/squash-postgres/src/org/jetbrains/squash/dialects/postgres/PgDialect.kt @@ -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) { @@ -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 appendExpression(builder: SQLStatementBuilder, expression: Expression): 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) + } + } } \ No newline at end of file diff --git a/squash-postgres/src/org/jetbrains/squash/expressions/PgExpression.kt b/squash-postgres/src/org/jetbrains/squash/expressions/PgExpression.kt new file mode 100644 index 0000000..e18be7b --- /dev/null +++ b/squash-postgres/src/org/jetbrains/squash/expressions/PgExpression.kt @@ -0,0 +1,4 @@ +package org.jetbrains.squash.expressions + +class ArrayInExpression(val value: Expression, val values: Collection) : Expression +class ArrayOverlapExpression(val value: Expression, val values: Collection) : Expression \ No newline at end of file diff --git a/squash-postgres/src/org/jetbrains/squash/expressions/PgExpressionBuilder.kt b/squash-postgres/src/org/jetbrains/squash/expressions/PgExpressionBuilder.kt new file mode 100644 index 0000000..11630ee --- /dev/null +++ b/squash-postgres/src/org/jetbrains/squash/expressions/PgExpressionBuilder.kt @@ -0,0 +1,4 @@ +package org.jetbrains.squash.expressions + +infix fun Expression.contains(values: Collection): ArrayInExpression = ArrayInExpression(this, values) +infix fun Expression.containsAny(values: Collection): ArrayOverlapExpression = ArrayOverlapExpression(this, values) diff --git a/squash-postgres/test/org/jetbrains/squash/dialects/postgres/tests/PgDialectColumnTypes.kt b/squash-postgres/test/org/jetbrains/squash/dialects/postgres/tests/PgDialectColumnTypes.kt new file mode 100644 index 0000000..fd494ae --- /dev/null +++ b/squash-postgres/test/org/jetbrains/squash/dialects/postgres/tests/PgDialectColumnTypes.kt @@ -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() +} diff --git a/squash-postgres/test/org/jetbrains/squash/dialects/postgres/tests/PgDialectColumnTypesTests.kt b/squash-postgres/test/org/jetbrains/squash/dialects/postgres/tests/PgDialectColumnTypesTests.kt new file mode 100644 index 0000000..2c3cc6c --- /dev/null +++ b/squash-postgres/test/org/jetbrains/squash/dialects/postgres/tests/PgDialectColumnTypesTests.kt @@ -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::class.java, row[PgDialectColumnTypes.notnullIntarray].javaClass) + assertTrue { Arrays.equals(arrayOfInt, row[PgDialectColumnTypes.notnullIntarray]) } + + assertEquals(Array::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() + } +} \ No newline at end of file diff --git a/squash.iml b/squash.iml index 3af2333..a923e9c 100644 --- a/squash.iml +++ b/squash.iml @@ -1,6 +1,29 @@ - + + + + + + + + + + + + + + @@ -16,8 +39,19 @@ + + + + + + + + + + + - + \ No newline at end of file