From aae7ebc682c5334ea116414f29ffb201773b6499 Mon Sep 17 00:00:00 2001 From: Mike Kelp Date: Thu, 16 May 2019 17:17:59 -0700 Subject: [PATCH 1/6] Generalize common functions and add average() aggregate --- .../squash/dialect/BaseSQLDialect.kt | 20 +++------ .../squash/expressions/FunctionExpression.kt | 25 +++++++---- .../org/jetbrains/squash/tests/QueryTests.kt | 41 +++++++++++++++++++ .../jetbrains/squash/tests/data/CitiesData.kt | 31 ++++++++++++-- .../squash/tests/data/CitiesSchema.kt | 6 +++ 5 files changed, 96 insertions(+), 27 deletions(-) diff --git a/squash-core/src/org/jetbrains/squash/dialect/BaseSQLDialect.kt b/squash-core/src/org/jetbrains/squash/dialect/BaseSQLDialect.kt index 5b8e827..377a13f 100644 --- a/squash-core/src/org/jetbrains/squash/dialect/BaseSQLDialect.kt +++ b/squash-core/src/org/jetbrains/squash/dialect/BaseSQLDialect.kt @@ -153,21 +153,11 @@ open class BaseSQLDialect(val name: String) : SQLDialect { appendExpression(this, expression.value) append(")") } - is MaxExpression -> { - append("MAX(") - appendExpression(this, expression.value) - append(")") - } - is MinExpression -> { - append("MIN(") - appendExpression(this, expression.value) - append(")") - } - is SumExpression -> { - append("SUM(") - appendExpression(this, expression.value) - append(")") - } + is GeneralFunctionExpression -> { + append("${expression.name}(") + appendExpression(this, expression.value) + append(")") + } else -> error("Function '$expression' is not supported by ${this@BaseSQLDialect}") } } diff --git a/squash-core/src/org/jetbrains/squash/expressions/FunctionExpression.kt b/squash-core/src/org/jetbrains/squash/expressions/FunctionExpression.kt index 858816e..e79ebe3 100644 --- a/squash-core/src/org/jetbrains/squash/expressions/FunctionExpression.kt +++ b/squash-core/src/org/jetbrains/squash/expressions/FunctionExpression.kt @@ -1,16 +1,23 @@ package org.jetbrains.squash.expressions +import java.math.BigDecimal + interface FunctionExpression : Expression -class CountExpression(val value: Expression<*>) : FunctionExpression -class CountDistinctExpression(val value: Expression<*>) : FunctionExpression -class MinExpression(val value: Expression<*>) : FunctionExpression -class MaxExpression(val value: Expression<*>) : FunctionExpression -class SumExpression(val value: Expression<*>) : FunctionExpression +/** + * Represents any function with a name, single argument, and return value. + */ +class GeneralFunctionExpression( + val name:String, + val value:Expression<*> +) : FunctionExpression + +class CountExpression(val value: Expression<*>? = null) : FunctionExpression +class CountDistinctExpression(val value:Expression<*>? = null) : FunctionExpression fun Expression<*>.count() = CountExpression(this) fun Expression<*>.countDistinct() = CountDistinctExpression(this) -fun Expression<*>.min() = MinExpression(this) -fun Expression<*>.max() = MaxExpression(this) -fun Expression<*>.sum() = SumExpression(this) - +fun Expression<*>.min() = GeneralFunctionExpression("MIN",this) +fun Expression<*>.max() = GeneralFunctionExpression("MAX", this) +fun Expression<*>.sum() = GeneralFunctionExpression("SUM",this) +fun Expression<*>.average() = GeneralFunctionExpression("AVG",this) diff --git a/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt b/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt index da2066a..e90c1a5 100644 --- a/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt +++ b/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt @@ -6,6 +6,7 @@ import org.jetbrains.squash.query.* import org.jetbrains.squash.results.* import org.jetbrains.squash.statements.* import org.jetbrains.squash.tests.data.* +import java.math.BigDecimal import kotlin.test.* abstract class QueryTests : DatabaseTests { @@ -448,6 +449,46 @@ abstract class QueryTests : DatabaseTests { } } + @Test + fun selectAggregate() { + withCities { + val query = select( + CityStats.value.min().alias("minimum"), + CityStats.value.max().alias("maximum"), + CityStats.value.average().alias("average") + ) + .from(Cities) + .innerJoin(CityStats, + (Cities.id eq CityStats.cityId) + .and(CityStats.name eq "population") + ) + + println(connection.dialect.statementSQL(query)) + + val result = query.execute().single() + assertEquals(1500000, result["minimum"], "Minimum city population does not match") + assertEquals(6200000, result["maximum"], "Maximum city population does not match") + assertEquals(BigDecimal("3433333.3333"), result["average"], "Average city population does not match") +/* + connection.dialect.statementSQL(query).assertSQL { + "SELECT Cities.name, COUNT(Citizens.id) AS citizens FROM Cities INNER JOIN Citizens ON Cities.id = Citizens.city_id GROUP BY Cities.name" + } + + query.execute().forEach { + val cityName = it[Cities.name] + val userCount = it.get("citizens") + + when (cityName) { + "Munich" -> assertEquals(2, userCount) + "Prague" -> assertEquals(0, userCount) + "St. Petersburg" -> assertEquals(1, userCount) + else -> error("Unknown city $cityName") + } + } + */ + } + } + @Test fun selectFromNestedQuery() { withCities { val query = from(select(Citizens.name, Citizens.id).from(Citizens).alias("Citizens")) diff --git a/squash-core/test/org/jetbrains/squash/tests/data/CitiesData.kt b/squash-core/test/org/jetbrains/squash/tests/data/CitiesData.kt index 7085875..dc485db 100644 --- a/squash-core/test/org/jetbrains/squash/tests/data/CitiesData.kt +++ b/squash-core/test/org/jetbrains/squash/tests/data/CitiesData.kt @@ -7,7 +7,7 @@ import org.jetbrains.squash.statements.* import org.jetbrains.squash.tests.* fun DatabaseTests.withCities(statement: Transaction.() -> R) :R { - return withTables(Cities, CitizenData, Citizens, CitizenDataLink) { + return withTables(Cities, CityStats, CitizenData, Citizens, CitizenDataLink) { val spbId = insertInto(Cities).values { it[name] = "St. Petersburg" }.fetch(Cities.id).execute() @@ -16,10 +16,35 @@ fun DatabaseTests.withCities(statement: Transaction.() -> R) :R { it[name] = "Munich" }.fetch(Cities.id).execute() - insertInto(Cities).values { + val pragueId = insertInto(Cities).values { it[name] = "Prague" - }.execute() + }.fetch(Cities.id).execute() + + /* + * Insert City Statistics + */ + + insertInto(CityStats).values { + it[cityId] = spbId + it[name] = "population" + it[value] = 6200000 + }.execute() + + insertInto(CityStats).values { + it[cityId] = munichId + it[name] = "population" + it[value] = 1500000 + }.execute() + + insertInto(CityStats).values { + it[cityId] = pragueId + it[name] = "population" + it[value] = 2600000 + }.execute() + /* + * Insert Citizens + */ insertInto(Citizens).query() .select { literal("andrey").alias("id") } .select { literal("Andrey").alias("name") } diff --git a/squash-core/test/org/jetbrains/squash/tests/data/CitiesSchema.kt b/squash-core/test/org/jetbrains/squash/tests/data/CitiesSchema.kt index f5fd009..eea4759 100644 --- a/squash-core/test/org/jetbrains/squash/tests/data/CitiesSchema.kt +++ b/squash-core/test/org/jetbrains/squash/tests/data/CitiesSchema.kt @@ -10,6 +10,12 @@ object Cities : TableDefinition() { val name = varchar("name", 50) } +object CityStats : TableDefinition() { + val cityId = reference(Cities.id, "cityId") + val name = varchar("name", 50) + val value = long("value") +} + object Citizens : TableDefinition() { val id = varchar("id", 10).primaryKey() val name = varchar("name", length = 50) From 8fabb26d8b6cbb596a0b425a3bed2383a8552b5d Mon Sep 17 00:00:00 2001 From: Mike Kelp Date: Thu, 16 May 2019 17:18:46 -0700 Subject: [PATCH 2/6] Testing : Removed commented code and fixed spacing --- .../org/jetbrains/squash/tests/QueryTests.kt | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt b/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt index e90c1a5..1337e35 100644 --- a/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt +++ b/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt @@ -449,8 +449,7 @@ abstract class QueryTests : DatabaseTests { } } - @Test - fun selectAggregate() { + @Test fun selectAggregate() { withCities { val query = select( CityStats.value.min().alias("minimum"), @@ -469,23 +468,7 @@ abstract class QueryTests : DatabaseTests { assertEquals(1500000, result["minimum"], "Minimum city population does not match") assertEquals(6200000, result["maximum"], "Maximum city population does not match") assertEquals(BigDecimal("3433333.3333"), result["average"], "Average city population does not match") -/* - connection.dialect.statementSQL(query).assertSQL { - "SELECT Cities.name, COUNT(Citizens.id) AS citizens FROM Cities INNER JOIN Citizens ON Cities.id = Citizens.city_id GROUP BY Cities.name" - } - - query.execute().forEach { - val cityName = it[Cities.name] - val userCount = it.get("citizens") - - when (cityName) { - "Munich" -> assertEquals(2, userCount) - "Prague" -> assertEquals(0, userCount) - "St. Petersburg" -> assertEquals(1, userCount) - else -> error("Unknown city $cityName") - } - } - */ + } } From fbca2646a96c98e5219b288a573f78263307de45 Mon Sep 17 00:00:00 2001 From: Mike Kelp Date: Fri, 17 May 2019 10:07:41 -0700 Subject: [PATCH 3/6] Correct COUNT() issue with nullable expression value --- .../src/org/jetbrains/squash/dialect/BaseSQLDialect.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squash-core/src/org/jetbrains/squash/dialect/BaseSQLDialect.kt b/squash-core/src/org/jetbrains/squash/dialect/BaseSQLDialect.kt index 377a13f..674daa1 100644 --- a/squash-core/src/org/jetbrains/squash/dialect/BaseSQLDialect.kt +++ b/squash-core/src/org/jetbrains/squash/dialect/BaseSQLDialect.kt @@ -145,12 +145,12 @@ open class BaseSQLDialect(val name: String) : SQLDialect { when (expression) { is CountExpression -> { append("COUNT(") - appendExpression(this, expression.value) + appendExpression(this, expression.value!!) append(")") } is CountDistinctExpression -> { append("COUNT(DISTINCT ") - appendExpression(this, expression.value) + appendExpression(this, expression.value!!) append(")") } is GeneralFunctionExpression -> { From 298319034bd3793ad1658db947fd8150951588e4 Mon Sep 17 00:00:00 2001 From: Mike Kelp Date: Fri, 17 May 2019 10:08:09 -0700 Subject: [PATCH 4/6] Add more type conversions coming from the database --- .../org/jetbrains/squash/drivers/JDBCDataConversion.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCDataConversion.kt b/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCDataConversion.kt index 1d91b80..52ec9fc 100644 --- a/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCDataConversion.kt +++ b/squash-jdbc/src/org/jetbrains/squash/drivers/JDBCDataConversion.kt @@ -1,7 +1,8 @@ package org.jetbrains.squash.drivers import org.jetbrains.squash.connection.* -import java.math.* +import java.math.BigDecimal +import java.math.BigInteger import java.sql.* import java.time.* import kotlin.reflect.* @@ -30,11 +31,16 @@ open class JDBCDataConversion { value is Time -> value.toLocalTime() value is Blob -> JDBCBinaryObject(value.getBytes(1, value.length().toInt())) value is ByteArray && type == BinaryObject::class -> JDBCBinaryObject(value) - type.javaObjectType.isInstance(value) -> value + value is Double && type.javaObjectType == BigDecimal::class.java -> value.toBigDecimal() + value is Int && type.javaObjectType == BigInteger::class.java -> value.toBigInteger() + value is Int && type.javaObjectType == BigDecimal::class.java -> value.toBigDecimal() value is Long && type.javaObjectType == Int::class.javaObjectType -> value.toInt() + value is Long && type.javaObjectType == BigInteger::class.java -> value.toBigInteger() + value is Long && type.javaObjectType == BigDecimal::class.java -> value.toBigDecimal() value is Int && type.javaObjectType == Long::class.javaObjectType -> value.toLong() value is BigInteger && type.javaObjectType == Int::class.javaObjectType -> value.toInt() value is BigInteger && type.javaObjectType == Long::class.javaObjectType -> value.toLong() + type.javaObjectType.isInstance(value) -> value else -> error("Cannot convert value of type `${value.javaClass}` to type `$type`") } } From 5192efa5c8506f0c0cf1c0d82e13842d746a6a85 Mon Sep 17 00:00:00 2001 From: Mike Kelp Date: Fri, 17 May 2019 10:10:02 -0700 Subject: [PATCH 5/6] Add generic type to min, max, and sum min and max will match the type that are passed (numeric, date, etc.) sum only applies to certain types, but could of course be a sum of integers or decimals, which makes maintaining a matching type a requirement. --- .../org/jetbrains/squash/expressions/FunctionExpression.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/squash-core/src/org/jetbrains/squash/expressions/FunctionExpression.kt b/squash-core/src/org/jetbrains/squash/expressions/FunctionExpression.kt index e79ebe3..fccdbb8 100644 --- a/squash-core/src/org/jetbrains/squash/expressions/FunctionExpression.kt +++ b/squash-core/src/org/jetbrains/squash/expressions/FunctionExpression.kt @@ -17,7 +17,7 @@ class CountDistinctExpression(val value:Expression<*>? = null) : FunctionExpress fun Expression<*>.count() = CountExpression(this) fun Expression<*>.countDistinct() = CountDistinctExpression(this) -fun Expression<*>.min() = GeneralFunctionExpression("MIN",this) -fun Expression<*>.max() = GeneralFunctionExpression("MAX", this) -fun Expression<*>.sum() = GeneralFunctionExpression("SUM",this) +fun Expression.min() = GeneralFunctionExpression("MIN",this) +fun Expression.max() = GeneralFunctionExpression("MAX", this) +fun Expression.sum() = GeneralFunctionExpression("SUM",this) fun Expression<*>.average() = GeneralFunctionExpression("AVG",this) From bf613653524c8b0e02f115f726e95309f8203463 Mon Sep 17 00:00:00 2001 From: Mike Kelp Date: Fri, 17 May 2019 10:10:29 -0700 Subject: [PATCH 6/6] Testing : Made aggregate tests more generic for different databases --- .../test/org/jetbrains/squash/tests/QueryTests.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt b/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt index 1337e35..ca2ac2b 100644 --- a/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt +++ b/squash-core/test/org/jetbrains/squash/tests/QueryTests.kt @@ -7,6 +7,7 @@ import org.jetbrains.squash.results.* import org.jetbrains.squash.statements.* import org.jetbrains.squash.tests.data.* import java.math.BigDecimal +import java.math.BigInteger import kotlin.test.* abstract class QueryTests : DatabaseTests { @@ -462,13 +463,10 @@ abstract class QueryTests : DatabaseTests { .and(CityStats.name eq "population") ) - println(connection.dialect.statementSQL(query)) - val result = query.execute().single() - assertEquals(1500000, result["minimum"], "Minimum city population does not match") - assertEquals(6200000, result["maximum"], "Maximum city population does not match") - assertEquals(BigDecimal("3433333.3333"), result["average"], "Average city population does not match") - + assertEquals(BigDecimal("1500000"), result["minimum"], "Minimum city population does not match") + assertEquals(BigDecimal("6200000"), result["maximum"], "Maximum city population does not match") + assertEquals(BigInteger("3433333"), result.get("average").toBigInteger(), "Average city population does not match") } }