diff --git a/CHANGELOG.md b/CHANGELOG.md index d0891f16b..596582132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,12 @@ Thank you to all who have contributed! ### Added - **EXPERIMENTAL**: Adds end-to-end support for window functions including RANK, DENSE_RANK, LAG, LEAD, and ROW_NUMBER functions alongside the WINDOW clause. +- `UPPER`, `LOWER`, `TRIM` functions support for `CHAR/VARCHAR` +- `LIKE` and `LIKE_ESCAPE` functions support for `CHAR/VARCHAR/CLOB/STRING` with dynamic type coercion +- `CLOB/BLOB` now supports length parameters with `CLOB(max_length)/BLOB(max_length)` syntax ### Changed +- Formalize `UPPER`, `LOWER`, `TRIM` and `CONCAT` length and type handling on string types ### Deprecated - Deprecated previous modeling of window functions. @@ -43,12 +47,12 @@ Thank you to all who have contributed! ### Contributors Thank you to all who have contributed! - @johnedquinn +- @XuechunHHH ## [1.2.3](https://github.com/partiql/partiql-lang-kotlin/releases/tag/v1.2.3) - 2025-09-29 ### Added - partiql-ast: add `With` and `WithListElement` to `AstVisitor` and `SqlDialect` - - **EXPERIMENTAL** partiql-plan: add representation of `RelWith` and `WithListElement` to the plan and the `OperatorVisitor` - **EXPERIMENTAL** partiql-planner: add planner builder function to control whether `With` table references are diff --git a/partiql-parser/src/main/antlr/PartiQLParser.g4 b/partiql-parser/src/main/antlr/PartiQLParser.g4 index f004e68e6..418719873 100644 --- a/partiql-parser/src/main/antlr/PartiQLParser.g4 +++ b/partiql-parser/src/main/antlr/PartiQLParser.g4 @@ -1262,11 +1262,11 @@ type : datatype=( BOOL | BOOLEAN | TINYINT | SMALLINT | INTEGER2 | INT2 | INTEGER | INT | INTEGER4 | INT4 | INTEGER8 | INT8 | BIGINT | REAL | CHAR | CHARACTER - | STRING | SYMBOL | BLOB | CLOB | DATE | ANY + | STRING | SYMBOL | DATE | ANY ) # TypeAtomic | datatype=( STRUCT | TUPLE | LIST | ARRAY | SEXP | BAG ) # TypeComplexAtomic | datatype=DOUBLE PRECISION # TypeAtomic - | datatype=(CHARACTER|CHAR|FLOAT|VARCHAR) ( PAREN_LEFT arg0=LITERAL_INTEGER PAREN_RIGHT )? # TypeArgSingle + | datatype=(CHARACTER|CHAR|FLOAT|VARCHAR|CLOB|BLOB) ( PAREN_LEFT arg0=LITERAL_INTEGER PAREN_RIGHT )? # TypeArgSingle | CHARACTER VARYING ( PAREN_LEFT arg0=LITERAL_INTEGER PAREN_RIGHT )? # TypeVarChar | datatype=(DECIMAL|DEC|NUMERIC) ( PAREN_LEFT arg0=LITERAL_INTEGER ( COMMA arg1=LITERAL_INTEGER )? PAREN_RIGHT )? # TypeArgDouble | datatype=(TIME|TIMESTAMP) ( PAREN_LEFT precision=LITERAL_INTEGER PAREN_RIGHT )? (WITH TIME ZONE | WITHOUT TIME ZONE)? # TypeTimeZone diff --git a/partiql-parser/src/main/kotlin/org/partiql/parser/internal/PartiQLParserDefault.kt b/partiql-parser/src/main/kotlin/org/partiql/parser/internal/PartiQLParserDefault.kt index ed15023e7..425be8b70 100644 --- a/partiql-parser/src/main/kotlin/org/partiql/parser/internal/PartiQLParserDefault.kt +++ b/partiql-parser/src/main/kotlin/org/partiql/parser/internal/PartiQLParserDefault.kt @@ -2376,6 +2376,14 @@ internal class PartiQLParserDefault : PartiQLParser { null -> DataType.VARCHAR() else -> DataType.VARCHAR(n) } + GeneratedParser.CLOB -> when (n) { + null -> DataType.CLOB() + else -> DataType.CLOB(n) + } + GeneratedParser.BLOB -> when (n) { + null -> DataType.BLOB() + else -> DataType.BLOB(n) + } else -> throw error(ctx.datatype, "Invalid datatype") } } diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/ConcatTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/ConcatTest.kt new file mode 100644 index 000000000..0d4ea280b --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/ConcatTest.kt @@ -0,0 +1,52 @@ +package org.partiql.planner.internal.typer.functions + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.partiql.spi.errors.PRuntimeException + +// Basic concat tests are covered in OpConcatTest.kt +class ConcatTest { + private val maxInt = Int.MAX_VALUE + + @Test + fun `concat with VARCHAR overflow throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("CAST('some string' AS VARCHAR($maxInt)) || CAST('a' AS VARCHAR(1))") + } + } + + @Test + fun `concat with CHAR overflow throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("CAST('some string' AS CHAR($maxInt)) || CAST('a' AS CHAR(1))") + } + } + + @Test + fun `concat with CLOB overflow throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("CAST('some string' AS CLOB($maxInt)) || CAST('a' AS CLOB(1))") + } + } + + @Test + fun `concat with VARCHAR and CHAR overflow throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("CAST('some string' AS VARCHAR($maxInt)) || CAST('a' AS CHAR(1))") + } + } + + @Test + fun `concat with CLOB and CHAR overflow throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("CAST('some string' AS CLOB($maxInt)) || CAST('a' AS CHAR(1))") + } + } + + @Test + fun `concat with CLOB and VARCHAR overflow throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("CAST('some string' AS CLOB($maxInt)) || CAST('a' AS VARCHAR(1))") + } + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/FnTestUtils.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/FnTestUtils.kt new file mode 100644 index 000000000..e3032052f --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/FnTestUtils.kt @@ -0,0 +1,30 @@ +package org.partiql.planner.internal.typer.functions + +import org.partiql.parser.PartiQLParser +import org.partiql.plan.Action +import org.partiql.planner.PartiQLPlanner +import org.partiql.planner.internal.TestCatalog +import org.partiql.spi.catalog.Session +import org.partiql.spi.types.PType + +object FnTestUtils { + /** + * Helper function to parse, plan, and extract the result type from a PartiQL query. + */ + fun getQueryResultType(query: String): PType { + val session = Session.builder() + .catalog("default") + .catalogs( + TestCatalog.builder() + .name("default") + .build() + ) + .build() + val parseResult = PartiQLParser.standard().parse(query) + val ast = parseResult.statements[0] + val planner = PartiQLPlanner.builder().build() + val result = planner.plan(ast, session) + val queryAction = result.plan.action as Action.Query + return queryAction.rex.type.pType + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/LowerTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/LowerTest.kt new file mode 100644 index 000000000..66b5c4eb2 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/LowerTest.kt @@ -0,0 +1,44 @@ +package org.partiql.planner.internal.typer.functions + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.partiql.spi.errors.PRuntimeException +import org.partiql.spi.types.PType +import kotlin.test.assertEquals + +class LowerTest { + + @Test + fun `lower preserves CHAR length and type`() { + val actualType = FnTestUtils.getQueryResultType("LOWER(CAST('HELLO' AS CHAR(5)))") + assertEquals(PType.CHAR, actualType.code()) + assertEquals(5, actualType.length) + } + + @Test + fun `lower preserves VARCHAR length and type`() { + val actualType = FnTestUtils.getQueryResultType("LOWER(CAST('HELLO ' AS VARCHAR(10)))") + assertEquals(PType.VARCHAR, actualType.code()) + assertEquals(10, actualType.length) + } + + @Test + fun `lower preserves CLOB length and type`() { + val actualType = FnTestUtils.getQueryResultType("LOWER(CAST(' HELLO' AS CLOB(20)))") + assertEquals(PType.CLOB, actualType.code()) + assertEquals(20, actualType.length) + } + + @Test + fun `lower preserves STRING type`() { + val actualType = FnTestUtils.getQueryResultType("LOWER('HELLO')") + assertEquals(PType.STRING, actualType.code()) + } + + @Test + fun `lower with unsupported type throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("LOWER(42)") + } + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/TrimTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/TrimTest.kt new file mode 100644 index 000000000..1eb7a982e --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/TrimTest.kt @@ -0,0 +1,44 @@ +package org.partiql.planner.internal.typer.functions + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.partiql.spi.errors.PRuntimeException +import org.partiql.spi.types.PType +import kotlin.test.assertEquals + +class TrimTest { + + @Test + fun `trim preserves CHAR length and returns VARCHAR type`() { + val actualType = FnTestUtils.getQueryResultType("TRIM(CAST(' HELLO ' AS CHAR(9)))") + assertEquals(PType.VARCHAR, actualType.code()) + assertEquals(9, actualType.length) + } + + @Test + fun `trim preserves VARCHAR length and type`() { + val actualType = FnTestUtils.getQueryResultType("TRIM(CAST(' HELLO ' AS VARCHAR(15)))") + assertEquals(PType.VARCHAR, actualType.code()) + assertEquals(15, actualType.length) + } + + @Test + fun `trim preserves CLOB length and type`() { + val actualType = FnTestUtils.getQueryResultType("TRIM(CAST(' HELLO ' AS CLOB(20)))") + assertEquals(PType.CLOB, actualType.code()) + assertEquals(20, actualType.length) + } + + @Test + fun `trim preserves STRING type`() { + val actualType = FnTestUtils.getQueryResultType("TRIM(' HELLO ')") + assertEquals(PType.STRING, actualType.code()) + } + + @Test + fun `trim with unsupported type throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("TRIM(42)") + } + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/UpperTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/UpperTest.kt new file mode 100644 index 000000000..2409eca98 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/functions/UpperTest.kt @@ -0,0 +1,44 @@ +package org.partiql.planner.internal.typer.functions + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.partiql.spi.errors.PRuntimeException +import org.partiql.spi.types.PType +import kotlin.test.assertEquals + +class UpperTest { + + @Test + fun `upper preserves CHAR length and type`() { + val actualType = FnTestUtils.getQueryResultType("UPPER(CAST('hello' AS CHAR(5)))") + assertEquals(PType.CHAR, actualType.code()) + assertEquals(5, actualType.length) + } + + @Test + fun `upper preserves VARCHAR length and type`() { + val actualType = FnTestUtils.getQueryResultType("UPPER(CAST('hello ' AS VARCHAR(10)))") + assertEquals(PType.VARCHAR, actualType.code()) + assertEquals(10, actualType.length) + } + + @Test + fun `upper preserves CLOB length and type`() { + val actualType = FnTestUtils.getQueryResultType("UPPER(CAST(' hello' AS CLOB(20)))") + assertEquals(PType.CLOB, actualType.code()) + assertEquals(20, actualType.length) + } + + @Test + fun `upper preserves STRING type`() { + val actualType = FnTestUtils.getQueryResultType("UPPER(' hello ')") + assertEquals(PType.STRING, actualType.code()) + } + + @Test + fun `upper with unsupported type throws exception`() { + assertThrows { + FnTestUtils.getQueryResultType("UPPER(42)") + } + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/operator/OpConcatTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/operator/OpConcatTest.kt index 09eeb4213..7d094b990 100644 --- a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/operator/OpConcatTest.kt +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/operator/OpConcatTest.kt @@ -33,8 +33,8 @@ class OpConcatTest : PartiQLTyperTestBase() { val arg1 = args[1] val output = when { arg0 == arg1 -> arg1 - castTablePType(arg1, arg0) == CastType.COERCION -> arg0 castTablePType(arg0, arg1) == CastType.COERCION -> arg1 + castTablePType(arg1, arg0) == CastType.COERCION -> arg0 else -> error("Arguments do not conform to parameters. Args: $args") } accumulateSuccess(output, args) diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/util/Utils.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/util/Utils.kt index 173064804..851b5759d 100644 --- a/partiql-planner/src/test/kotlin/org/partiql/planner/util/Utils.kt +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/util/Utils.kt @@ -72,7 +72,7 @@ val allCharStringPType = setOf( PType.character(256), // TODO: Length PType.varchar(256), // TODO: Length PType.string(), - PType.clob(Int.MAX_VALUE), // TODO: Length + PType.clob(256), // TODO: Length ) val allBinaryPType = setOf( @@ -314,7 +314,7 @@ val castTablePType: ((PType, PType) -> CastType) = { from, to -> else -> CastType.UNSAFE } PType.CLOB -> when (to.code()) { - PType.CLOB -> CastType.COERCION + PType.CLOB, PType.STRING -> CastType.COERCION else -> CastType.UNSAFE } PType.BAG -> when (to.code()) { @@ -368,7 +368,7 @@ val castTablePType: ((PType, PType) -> CastType) = { from, to -> else -> CastType.UNSAFE } PType.STRING -> when (to.code()) { - PType.STRING, PType.CLOB -> CastType.COERCION + PType.STRING -> CastType.COERCION else -> CastType.UNSAFE } PType.STRUCT -> when (to.code()) { @@ -392,11 +392,11 @@ val castTablePType: ((PType, PType) -> CastType) = { from, to -> else -> CastType.UNSAFE } PType.CHAR -> when (to.code()) { - PType.CHAR, PType.VARCHAR, PType.STRING, PType.CLOB -> CastType.COERCION + PType.CHAR, PType.VARCHAR, PType.CLOB, PType.STRING -> CastType.COERCION else -> CastType.UNSAFE } PType.VARCHAR -> when (to.code()) { - PType.VARCHAR, PType.STRING, PType.CLOB -> CastType.COERCION + PType.VARCHAR, PType.CLOB, PType.STRING -> CastType.COERCION else -> CastType.UNSAFE } PType.ROW -> when (to.code()) { diff --git a/partiql-spi/src/main/java/org/partiql/spi/value/Datum.java b/partiql-spi/src/main/java/org/partiql/spi/value/Datum.java index b8c101f2e..4edc8bc84 100644 --- a/partiql-spi/src/main/java/org/partiql/spi/value/Datum.java +++ b/partiql-spi/src/main/java/org/partiql/spi/value/Datum.java @@ -60,7 +60,8 @@ default boolean isMissing() { /** * @return the underlying value applicable to the types: * {@link PType#STRING}, - * {@link PType#CHAR} + * {@link PType#CHAR}, + * {@link PType#VARCHAR} * @throws InvalidOperationException if the operation is not applicable to the type returned from * {@link #getType()}; for example, if {@link #getType()} returns a {@link PType#INTEGER}, then this method * will throw this exception upon invocation. diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/Builtins.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/Builtins.kt index 083def0ee..bad67b170 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/Builtins.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/Builtins.kt @@ -58,10 +58,7 @@ internal object Builtins { Fn_COLL_AGG__BAG__ANY.MIN_DISTINCT, Fn_COLL_AGG__BAG__ANY.SOME_DISTINCT, Fn_COLL_AGG__BAG__ANY.SUM_DISTINCT, - Fn_CONCAT__CHAR_CHAR__CHAR, - Fn_CONCAT__VARCHAR_VARCHAR__VARCHAR, - Fn_CONCAT__STRING_STRING__STRING, - Fn_CONCAT__CLOB_CLOB__CLOB, + FnConcat, Fn_CURRENT_DATE____DATE, Fn_CURRENT_USER____STRING, @@ -141,14 +138,11 @@ internal object Builtins { Fn_IS_TIME__ANY__BOOL, Fn_IS_TIMESTAMP__BOOL_INT32_ANY__BOOL, Fn_IS_TIMESTAMP__ANY__BOOL, - Fn_LIKE__STRING_STRING__BOOL, - Fn_LIKE__CLOB_CLOB__BOOL, - Fn_LIKE_ESCAPE__STRING_STRING_STRING__BOOL, - Fn_LIKE_ESCAPE__CLOB_CLOB_CLOB__BOOL, + FnLike, + FnLikeEscape, - Fn_LOWER__STRING__STRING, - Fn_LOWER__CLOB__CLOB, + FnLower, FnLt, FnLte, @@ -184,8 +178,7 @@ internal object Builtins { Fn_SUBSTRING__CLOB_INT64_INT64__CLOB, FnTimes, - Fn_TRIM__STRING__STRING, - Fn_TRIM__CLOB__CLOB, + FnTrim, Fn_TRIM_CHARS__STRING_STRING__STRING, Fn_TRIM_CHARS__CLOB_CLOB__CLOB, @@ -202,8 +195,7 @@ internal object Builtins { Fn_TRIM_TRAILING_CHARS__STRING_STRING__STRING, Fn_TRIM_TRAILING_CHARS__CLOB_CLOB__CLOB, - Fn_UPPER__STRING__STRING, - Fn_UPPER__CLOB__CLOB, + FnUpper, Fn_UTCNOW____TIMESTAMP, // diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnBitLength.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnBitLength.kt index fd5503423..b5603401d 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnBitLength.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnBitLength.kt @@ -1,5 +1,6 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnCharLength.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnCharLength.kt index 5bf0a6407..9e46788e1 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnCharLength.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnCharLength.kt @@ -3,6 +3,7 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnConcat.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnConcat.kt index 1ad46d0ba..d57a3c894 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnConcat.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnConcat.kt @@ -3,63 +3,106 @@ package org.partiql.spi.function.builtins +import org.partiql.spi.function.Fn +import org.partiql.spi.function.FnOverload +import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter +import org.partiql.spi.function.RoutineOverloadSignature +import org.partiql.spi.internal.SqlTypeFamily import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils import org.partiql.spi.value.Datum -internal val Fn_CONCAT__CHAR_CHAR__CHAR = FunctionUtils.hidden( - name = "concat", - returns = PType.character(256), // TODO: Handle length - parameters = arrayOf( - Parameter("lhs", PType.character(256)), // TODO: Handle length - Parameter("rhs", PType.character(256)), // TODO: Handle length - ), -) { args -> - val arg0 = args[0].string - val arg1 = args[1].string - Datum.character(arg0 + arg1, 256) -} - -internal val Fn_CONCAT__VARCHAR_VARCHAR__VARCHAR = FunctionUtils.hidden( - name = "concat", - returns = PType.varchar(256), // TODO: Handle length - parameters = arrayOf( - Parameter("lhs", PType.varchar(256)), // TODO: Handle length - Parameter("rhs", PType.varchar(256)), // TODO: Handle length - ), -) { args -> - val arg0 = args[0].string - val arg1 = args[1].string - Datum.varchar(arg0 + arg1, 256) -} - -internal val Fn_CONCAT__STRING_STRING__STRING = FunctionUtils.hidden( - - name = "concat", - returns = PType.string(), - parameters = arrayOf( - Parameter("lhs", PType.string()), - Parameter("rhs", PType.string()), - ), - -) { args -> - val arg0 = args[0].string - val arg1 = args[1].string - Datum.string(arg0 + arg1) -} +/** + * SQL concatenation function implementation. + * + * Implements the SQL as defined in SQL2023 section 6.32 . + * + * According to SQL specification, result type is determined by coercibility: + * - If either argument is CLOB: result is CLOB with length = min(L1 + L2, max_clob_length) + * - If either argument is VARCHAR: result is VARCHAR with length = min(L1 + L2, max_varchar_length) + * - If both arguments are CHAR: result is CHAR with length = min(L1 + L2, max_char_length) + * + * PartiQL extensions: + * - STRING type (PartiQL-specific unlimited length string) has the highest coercibility + * + * Coercibility order: STRING > CLOB > VARCHAR > CHAR + * - STRING || any → STRING (PartiQL extension) + * - CLOB(L1) || CHAR(L2)/VARCHAR(L2) → CLOB(L1 + L2) + * - VARCHAR(L1) || CHAR(L2) → VARCHAR(L1 + L2) + * - CHAR(L1) || CHAR(L2) → CHAR(L1 + L2) + * + * Length overflow handling: + * - If L1 + L2 exceeds maximum allowed length, an exception is raised at compile time + */ +internal object FnConcat : FnOverload() { -internal val Fn_CONCAT__CLOB_CLOB__CLOB = FunctionUtils.hidden( + override fun getSignature(): RoutineOverloadSignature { + return RoutineOverloadSignature(FunctionUtils.hide("concat"), listOf(PType.dynamic(), PType.dynamic())) + } - name = "concat", - returns = PType.clob(Int.MAX_VALUE), - parameters = arrayOf( - Parameter("lhs", PType.clob(Int.MAX_VALUE)), - Parameter("rhs", PType.clob(Int.MAX_VALUE)), - ), + override fun getInstance(args: Array): Fn? { + val lhsType = args[0] + val rhsType = args[1] + // Check if both are string types + if (lhsType !in SqlTypeFamily.TEXT || rhsType !in SqlTypeFamily.TEXT) return null + // If string types are different, use coercibility: STRING > CLOB > VARCHAR > CHAR + val resultType = if (lhsType.code() != rhsType.code()) { + FnUtils.getHigherCoercibilityType(lhsType.code(), rhsType.code()) + } else { + lhsType.code() + } + return createConcatFunction(lhsType, rhsType, resultType) + } -) { args -> - val arg0 = args[0].bytes - val arg1 = args[1].bytes - Datum.clob(arg0 + arg1) + private fun createConcatFunction(lhsType: PType, rhsType: PType, resultType: Int): Fn? { + return when (resultType) { + PType.CHAR -> { + val totalLength = FnUtils.addLengths(FnUtils.getTypeLength(lhsType), FnUtils.getTypeLength(rhsType)) + Function.instance( + name = FunctionUtils.hide("concat"), + returns = PType.character(totalLength), + parameters = arrayOf(Parameter("lhs", lhsType), Parameter("rhs", rhsType)), + ) { args -> + val arg0 = args[0].string + val arg1 = args[1].string + Datum.character(arg0 + arg1, totalLength) + } + } + PType.VARCHAR -> { + val totalLength = FnUtils.addLengths(FnUtils.getTypeLength(lhsType), FnUtils.getTypeLength(rhsType)) + Function.instance( + name = FunctionUtils.hide("concat"), + returns = PType.varchar(totalLength), + parameters = arrayOf(Parameter("lhs", lhsType), Parameter("rhs", rhsType)), + ) { args -> + val arg0 = args[0].string + val arg1 = args[1].string + Datum.varchar(arg0 + arg1, totalLength) + } + } + PType.CLOB -> { + val totalLength = FnUtils.addLengths(FnUtils.getTypeLength(lhsType), FnUtils.getTypeLength(rhsType)) + Function.instance( + name = FunctionUtils.hide("concat"), + returns = PType.clob(totalLength), + parameters = arrayOf(Parameter("lhs", lhsType), Parameter("rhs", rhsType)), + ) { args -> + val arg0 = args[0].string + val arg1 = args[1].string + Datum.clob((arg0 + arg1).toByteArray(), totalLength) + } + } + PType.STRING -> Function.instance( + name = FunctionUtils.hide("concat"), + returns = PType.string(), + parameters = arrayOf(Parameter("lhs", lhsType), Parameter("rhs", rhsType)), + ) { args -> + val arg0 = args[0].string + val arg1 = args[1].string + Datum.string(arg0 + arg1) + } + else -> null + } + } } diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLike.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLike.kt index 5202ae145..5a6c4efac 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLike.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLike.kt @@ -3,7 +3,12 @@ package org.partiql.spi.function.builtins +import org.partiql.spi.function.Fn +import org.partiql.spi.function.FnOverload +import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter +import org.partiql.spi.function.RoutineOverloadSignature +import org.partiql.spi.internal.SqlTypeFamily import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils import org.partiql.spi.utils.PatternUtils.matchRegexPattern @@ -11,46 +16,85 @@ import org.partiql.spi.utils.PatternUtils.parsePattern import org.partiql.spi.value.Datum import java.util.regex.Pattern -internal val Fn_LIKE__STRING_STRING__BOOL = FunctionUtils.hidden( +/** + * SQL predicate implementation. + * + * Implements the SQL as defined in SQL:1999 section 8.5. + * + * Evaluates whether a character string matches a specified pattern using the SQL standard + * pattern matching rules. + * + * Pattern special characters: + * - `'_'` matches exactly one character + * - `'%'` matches zero or more characters + * + * Type coercion follows SQL coercibility with PartiQL extensions: + * - Coercibility order: STRING > CLOB > VARCHAR > CHAR + * - STRING has highest coercibility (PartiQL extension) + * + * Behavior: + * - If either value or pattern is NULL, result is UNKNOWN (null). + * - The pattern must be a valid string; otherwise, result is UNKNOWN (null). + * - The pattern is translated to a regular expression internally. + * + * Example: + * ``` + * 'abc' LIKE 'a_c' -- true + * 'abc' LIKE 'a%' -- true + * 'abc' LIKE 'a%z' -- false + * ``` + * + * @see FnLikeEscape for the variant with ESCAPE clause. + */ +internal object FnLike : FnOverload() { - name = "like", - returns = PType.bool(), - parameters = arrayOf( - Parameter("value", PType.string()), - Parameter("pattern", PType.string()), - ), - -) { args -> - val value = args[0].string - val pattern = args[1].string - val likeRegexPattern = when { - pattern.isEmpty() -> Pattern.compile("") - else -> parsePattern(pattern, null) - } - when (matchRegexPattern(value, likeRegexPattern)) { - true -> Datum.bool(true) - else -> Datum.bool(false) + override fun getSignature(): RoutineOverloadSignature { + return RoutineOverloadSignature(FunctionUtils.hide("like"), listOf(PType.dynamic(), PType.dynamic())) } -} -internal val Fn_LIKE__CLOB_CLOB__BOOL = FunctionUtils.hidden( - - name = "like", - returns = PType.bool(), - parameters = arrayOf( - Parameter("value", PType.clob(Int.MAX_VALUE)), - Parameter("pattern", PType.clob(Int.MAX_VALUE)), - ), - -) { args -> - val value = args[0].bytes.toString(Charsets.UTF_8) - val pattern = args[1].bytes.toString(Charsets.UTF_8) - val likeRegexPattern = when { - pattern.isEmpty() -> Pattern.compile("") - else -> parsePattern(pattern, null) - } - when (matchRegexPattern(value, likeRegexPattern)) { - true -> Datum.bool(true) - else -> Datum.bool(false) + override fun getInstance(args: Array): Fn? { + val valueType = args[0] + val patternType = args[1] + // Check if both are string types + if (valueType !in SqlTypeFamily.TEXT || patternType !in SqlTypeFamily.TEXT) return null + // Use type coercibility for coercion: STRING > CLOB > VARCHAR > CHAR + val resultType = if (valueType.code() != patternType.code()) { + FnUtils.getHigherCoercibilityType(valueType.code(), patternType.code()) + } else { + valueType.code() + } + return when (resultType) { + PType.CHAR, PType.VARCHAR, PType.STRING -> { + Function.instance( + name = FunctionUtils.hide("like"), + returns = PType.bool(), + parameters = arrayOf(Parameter("value", valueType), Parameter("pattern", patternType)), + ) { params -> + val value = params[0].string + val pattern = params[1].string + val likeRegexPattern = when { + pattern.isEmpty() -> Pattern.compile("") + else -> parsePattern(pattern, null) + } + Datum.bool(matchRegexPattern(value, likeRegexPattern)) + } + } + PType.CLOB -> { + Function.instance( + name = FunctionUtils.hide("like"), + returns = PType.bool(), + parameters = arrayOf(Parameter("value", valueType), Parameter("pattern", patternType)), + ) { params -> + val value = params[0].bytes.toString(Charsets.UTF_8) + val pattern = params[1].bytes.toString(Charsets.UTF_8) + val likeRegexPattern = when { + pattern.isEmpty() -> Pattern.compile("") + else -> parsePattern(pattern, null) + } + Datum.bool(matchRegexPattern(value, likeRegexPattern)) + } + } + else -> null + } } } diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLikeEscape.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLikeEscape.kt index 22c12dfcf..9bf7f114d 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLikeEscape.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLikeEscape.kt @@ -3,8 +3,13 @@ package org.partiql.spi.function.builtins +import org.partiql.spi.function.Fn +import org.partiql.spi.function.FnOverload +import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter +import org.partiql.spi.function.RoutineOverloadSignature import org.partiql.spi.function.builtins.internal.PErrors +import org.partiql.spi.internal.SqlTypeFamily import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils import org.partiql.spi.utils.PatternUtils @@ -13,62 +18,104 @@ import org.partiql.spi.utils.PatternUtils.parsePattern import org.partiql.spi.value.Datum import java.util.regex.Pattern -internal val Fn_LIKE_ESCAPE__STRING_STRING_STRING__BOOL = FunctionUtils.hidden( +/** + * SQL predicate implementation. + * + * Implements the SQL with an ESCAPE character as defined in SQL:1999 section 8.5. + * + * The ESCAPE clause allows treating pattern special characters (`'_'`, `'%'`) as literals + * by prefixing them with the escape character. + * + * Pattern special characters: + * - `'_'` matches a single character + * - `'%'` matches zero or more characters + * - If an ESCAPE character is specified, then: + * - A substring of length 2 starting with the ESCAPE character followed by `_`, `%`, or the ESCAPE character + * represents a literal. + * - Invalid escape sequences raise a data exception (e.g. ESCAPE character length ≠ 1). + * + * Type coercion follows SQL coercibility with PartiQL extensions: + * - Coercibility order: STRING > CLOB > VARCHAR > CHAR + * - STRING has highest coercibility (PartiQL extension) + * + * Behavior: + * - If any of value, pattern, or escape are NULL, the result is UNKNOWN (null). + * - An escape character must be a single character; result is UNKNOWN (null). + * - Pattern is converted to a regular expression with escape logic handled. + * + * SQL Exception Conditions: + * - If ESCAPE character length ≠ 1 → data exception — invalid escape character + * - If pattern contains invalid escape sequences → data exception — invalid escape sequence + * + * Example: + * ``` + * 'abc' LIKE 'a\_c' ESCAPE '\\' -- true (matches literal underscore) + * 'a_c' LIKE 'a\_c' ESCAPE '\' -- true + * 'abc' LIKE 'a%z' ESCAPE '#' -- false + * ``` + * + * @see FnLike for the variant without ESCAPE clause. + */ +internal object FnLikeEscape : FnOverload() { - name = "like_escape", - returns = PType.bool(), - parameters = arrayOf( - Parameter("value", PType.string()), - Parameter("pattern", PType.string()), - Parameter("escape", PType.string()), - ), - -) { args -> - val value = args[0].string - val pattern = args[1].string - val escape = args[2].string - val (patternString, escapeChar) = - try { - checkPattern(pattern, escape) - } catch (e: IllegalStateException) { - throw PErrors.internalErrorException(e) - } - val likeRegexPattern = when { - patternString.isEmpty() -> Pattern.compile("") - else -> parsePattern(patternString, escapeChar) + override fun getSignature(): RoutineOverloadSignature { + return RoutineOverloadSignature(FunctionUtils.hide("like_escape"), listOf(PType.dynamic(), PType.dynamic(), PType.dynamic())) } - when (PatternUtils.matchRegexPattern(value, likeRegexPattern)) { - true -> Datum.bool(true) - else -> Datum.bool(false) - } -} -internal val Fn_LIKE_ESCAPE__CLOB_CLOB_CLOB__BOOL = FunctionUtils.hidden( - - name = "like_escape", - returns = PType.bool(), - parameters = arrayOf( - Parameter("value", PType.clob(Int.MAX_VALUE)), - Parameter("pattern", PType.clob(Int.MAX_VALUE)), - Parameter("escape", PType.clob(Int.MAX_VALUE)), - ), - -) { args -> - val value = args[0].bytes.toString(Charsets.UTF_8) - val pattern = args[1].bytes.toString(Charsets.UTF_8) - val escape = args[2].bytes.toString(Charsets.UTF_8) - val (patternString, escapeChar) = - try { - checkPattern(pattern, escape) - } catch (e: IllegalStateException) { - throw PErrors.internalErrorException(e) + override fun getInstance(args: Array): Fn? { + val valueType = args[0] + val patternType = args[1] + val escapeType = args[2] + // Check if all are string types + if (valueType !in SqlTypeFamily.TEXT || patternType !in SqlTypeFamily.TEXT || escapeType !in SqlTypeFamily.TEXT) return null + // Use type coercibility for coercion: STRING > CLOB > VARCHAR > CHAR + val resultType = FnUtils.getHigherCoercibilityType(FnUtils.getHigherCoercibilityType(valueType.code(), patternType.code()), escapeType.code()) + return when (resultType) { + PType.CHAR, PType.VARCHAR, PType.STRING -> { + Function.instance( + name = FunctionUtils.hide("like_escape"), + returns = PType.bool(), + parameters = arrayOf(Parameter("value", valueType), Parameter("pattern", patternType), Parameter("escape", escapeType)), + ) { params -> + val value = params[0].string + val pattern = params[1].string + val escape = params[2].string + val (patternString, escapeChar) = + try { + checkPattern(pattern, escape) + } catch (e: IllegalStateException) { + throw PErrors.internalErrorException(e) + } + val likeRegexPattern = when { + patternString.isEmpty() -> Pattern.compile("") + else -> parsePattern(patternString, escapeChar) + } + Datum.bool(PatternUtils.matchRegexPattern(value, likeRegexPattern)) + } + } + PType.CLOB -> { + Function.instance( + name = FunctionUtils.hide("like_escape"), + returns = PType.bool(), + parameters = arrayOf(Parameter("value", valueType), Parameter("pattern", patternType), Parameter("escape", escapeType)), + ) { params -> + val value = params[0].bytes.toString(Charsets.UTF_8) + val pattern = params[1].bytes.toString(Charsets.UTF_8) + val escape = params[2].bytes.toString(Charsets.UTF_8) + val (patternString, escapeChar) = + try { + checkPattern(pattern, escape) + } catch (e: IllegalStateException) { + throw PErrors.internalErrorException(e) + } + val likeRegexPattern = when { + patternString.isEmpty() -> Pattern.compile("") + else -> parsePattern(patternString, escapeChar) + } + Datum.bool(PatternUtils.matchRegexPattern(value, likeRegexPattern)) + } + } + else -> null } - val likeRegexPattern = when { - patternString.isEmpty() -> Pattern.compile("") - else -> parsePattern(patternString, escapeChar) - } - when (PatternUtils.matchRegexPattern(value, likeRegexPattern)) { - true -> Datum.bool(true) - else -> Datum.bool(false) } } diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLower.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLower.kt index 0d9833751..36ba7f1eb 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLower.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnLower.kt @@ -3,31 +3,84 @@ package org.partiql.spi.function.builtins +import org.partiql.spi.function.Fn +import org.partiql.spi.function.FnOverload import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter +import org.partiql.spi.function.RoutineOverloadSignature import org.partiql.spi.types.PType import org.partiql.spi.value.Datum -internal val Fn_LOWER__STRING__STRING = Function.overload( +/** + * SQL LOWER function implementation. + * + * Implements the SQL function as defined in SQL2023 section 6.33 . + * + * According to SQL specification: + * - The declared type of the result is the declared type of the + * - For CHAR, VARCHAR, and CLOB types, the length parameter is preserved from the input type + * + * PartiQL extensions: + * - STRING type (PartiQL-specific unlimited length string) preserves its type + * + * Type preservation behavior: + * - CHAR(n) → CHAR(n) + * - VARCHAR(n) → VARCHAR(n) + * - CLOB(n) → CLOB(n) + * - STRING → STRING (PartiQL extension) + */ +internal object FnLower : FnOverload() { - name = "lower", - returns = PType.string(), - parameters = arrayOf(Parameter("value", PType.string())), + override fun getSignature(): RoutineOverloadSignature { + return RoutineOverloadSignature("lower", listOf(PType.dynamic())) + } -) { args -> - val string = args[0].string - val result = string.lowercase() - Datum.string(result) -} - -internal val Fn_LOWER__CLOB__CLOB = Function.overload( - - name = "lower", - returns = PType.clob(Int.MAX_VALUE), - parameters = arrayOf(Parameter("value", PType.clob(Int.MAX_VALUE))), - -) { args -> - val string = args[0].bytes.toString(Charsets.UTF_8) - val result = string.lowercase() - Datum.clob(result.toByteArray()) + override fun getInstance(args: Array): Fn? { + val inputType = args[0] + return when (inputType.code()) { + PType.CHAR -> { + Function.instance( + name = "lower", + returns = PType.character(inputType.length), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].string + val result = string.lowercase() + Datum.character(result, inputType.length) + } + } + PType.VARCHAR -> { + Function.instance( + name = "lower", + returns = PType.varchar(inputType.length), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].string + val result = string.lowercase() + Datum.varchar(result, inputType.length) + } + } + PType.CLOB -> { + Function.instance( + name = "lower", + returns = PType.clob(inputType.length), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].bytes.toString(Charsets.UTF_8) + val result = string.lowercase() + Datum.clob(result.toByteArray(), inputType.length) + } + } + PType.STRING -> Function.instance( + name = "lower", + returns = PType.string(), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].string + val result = string.lowercase() + Datum.string(result) + } + else -> error("Unsupported type for LOWER function: ${inputType.code()}") + } + } } diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnOctetLength.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnOctetLength.kt index 59a685a6e..3ae2fa886 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnOctetLength.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnOctetLength.kt @@ -1,5 +1,6 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnPosition.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnPosition.kt index 80912fa27..7664a978c 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnPosition.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnPosition.kt @@ -3,6 +3,7 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnSubstring.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnSubstring.kt index ffa2f612b..13150dd86 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnSubstring.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnSubstring.kt @@ -3,6 +3,7 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.FnOverload import org.partiql.spi.function.builtins.internal.PErrors import org.partiql.spi.types.PType diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrim.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrim.kt index 99bcc1213..fac1b00a9 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrim.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrim.kt @@ -3,61 +3,75 @@ package org.partiql.spi.function.builtins +import org.partiql.spi.function.Fn +import org.partiql.spi.function.FnOverload +import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter +import org.partiql.spi.function.RoutineOverloadSignature import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils import org.partiql.spi.utils.StringUtils.codepointTrim import org.partiql.spi.value.Datum /** - * From section 6.7 of SQL 92 spec: - * ``` - * 6) If is specified, then - * a) If FROM is specified, then either or or both shall be specified. + * SQL TRIM function implementation. * - * b) If is not specified, then BOTH is implicit. + * Implements the SQL as defined in SQL2023 section 6.33 . * - * c) If is not specified, then ' ' is implicit. + * According to SQL specification: + * - For CHAR/VARCHAR: result type is variable-length character string (VARCHAR) with maximum length equal to the input length + * - For CLOB: result type is character large object type (CLOB) with maximum length equal to the input length * - * d) If TRIM ( SRC ) is specified, then TRIM ( BOTH ' ' FROM SRC ) is implicit. + * PartiQL extensions: + * - STRING type (PartiQL-specific unlimited length string) preserves its type * - * e) The data type of the is variable-length character string with maximum length equal to the - * fixed length or maximum variable length of the . - * - * f) If a is specified, then and shall be comparable. - * - * g) The character repertoire and form-of-use of the are the same as those of the . - * - * h) The collating sequence and the coercibility attribute are determined as specified for monadic operators in - * Subclause 4.2.3, "Rules determining collating sequence usage", where the of TRIM plays the - * role of the monadic operand. - * ``` - * - * Where: - * * ` ::= LEADING | TRAILING | BOTH` - * * ` ::= ` - * * ` ::= ` + * Type preservation behavior: + * - CHAR(n) → VARCHAR(n) + * - VARCHAR(n) → VARCHAR(n) + * - CLOB(n) → CLOB(n) + * - STRING → STRING (PartiQL extension) */ -internal val Fn_TRIM__STRING__STRING = FunctionUtils.hidden( - - name = "trim", - returns = PType.string(), - parameters = arrayOf(Parameter("value", PType.string())), - -) { args -> - val value = args[0].string - val result = value.codepointTrim() - Datum.string(result) -} - -internal val Fn_TRIM__CLOB__CLOB = FunctionUtils.hidden( +internal object FnTrim : FnOverload() { - name = "trim", - returns = PType.clob(Int.MAX_VALUE), - parameters = arrayOf(Parameter("value", PType.clob(Int.MAX_VALUE))), + override fun getSignature(): RoutineOverloadSignature { + return RoutineOverloadSignature(FunctionUtils.hide("trim"), listOf(PType.dynamic())) + } -) { args -> - val string = args[0].bytes.toString(Charsets.UTF_8) - val result = string.codepointTrim() - Datum.clob(result.toByteArray()) + override fun getInstance(args: Array): Fn? { + val inputType = args[0] + return when (inputType.code()) { + PType.CHAR, PType.VARCHAR -> { + Function.instance( + name = FunctionUtils.hide("trim"), + returns = PType.varchar(inputType.length), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].string + val result = string.codepointTrim() + Datum.varchar(result, inputType.length) + } + } + PType.CLOB -> { + Function.instance( + name = FunctionUtils.hide("trim"), + returns = PType.clob(inputType.length), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].bytes.toString(Charsets.UTF_8) + val result = string.codepointTrim() + Datum.clob(result.toByteArray(), inputType.length) + } + } + PType.STRING -> Function.instance( + name = FunctionUtils.hide("trim"), + returns = PType.string(), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val value = params[0].string + val result = value.codepointTrim() + Datum.string(result) + } + else -> error("Unsupported type for TRIM function: ${inputType.code()}") + } + } } diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimChars.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimChars.kt index 38fa32b51..17525b4f0 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimChars.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimChars.kt @@ -3,6 +3,7 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimLeading.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimLeading.kt index 544d9e2a3..48cd5d689 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimLeading.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimLeading.kt @@ -3,6 +3,7 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimLeadingChars.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimLeadingChars.kt index c292007c9..94f1418ac 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimLeadingChars.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimLeadingChars.kt @@ -3,6 +3,7 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimTrailing.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimTrailing.kt index ff1670241..76bc40465 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimTrailing.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimTrailing.kt @@ -3,6 +3,7 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimTrailingChars.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimTrailingChars.kt index 514f104ea..1dd7d55d8 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimTrailingChars.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnTrimTrailingChars.kt @@ -3,6 +3,7 @@ package org.partiql.spi.function.builtins +// TODO: add support for CHAR/VARCHAR - https://github.com/partiql/partiql-lang-kotlin/issues/1838 import org.partiql.spi.function.Parameter import org.partiql.spi.types.PType import org.partiql.spi.utils.FunctionUtils diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnUpper.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnUpper.kt index 9b9ff3309..9dacaf216 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnUpper.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnUpper.kt @@ -3,31 +3,84 @@ package org.partiql.spi.function.builtins +import org.partiql.spi.function.Fn +import org.partiql.spi.function.FnOverload import org.partiql.spi.function.Function import org.partiql.spi.function.Parameter +import org.partiql.spi.function.RoutineOverloadSignature import org.partiql.spi.types.PType import org.partiql.spi.value.Datum -internal val Fn_UPPER__STRING__STRING = Function.overload( +/** + * SQL UPPER function implementation. + * + * Implements the SQL function as defined in SQL2023 section 6.33 . + * + * According to SQL specification: + * - The declared type of the result is the declared type of the + * - For CHAR, VARCHAR, and CLOB types, the length parameter is preserved from the input type + * + * PartiQL extensions: + * - STRING type (PartiQL-specific unlimited length string) preserves its type + * + * Type preservation behavior: + * - CHAR(n) → CHAR(n) + * - VARCHAR(n) → VARCHAR(n) + * - CLOB(n) → CLOB(n) + * - STRING → STRING (PartiQL extension) + */ +internal object FnUpper : FnOverload() { - name = "upper", - returns = PType.string(), - parameters = arrayOf(Parameter("value", PType.string())), + override fun getSignature(): RoutineOverloadSignature { + return RoutineOverloadSignature("upper", listOf(PType.dynamic())) + } -) { args -> - val string = args[0].string - val result = string.uppercase() - Datum.string(result) -} - -internal val Fn_UPPER__CLOB__CLOB = Function.overload( - - name = "upper", - returns = PType.clob(Int.MAX_VALUE), - parameters = arrayOf(Parameter("value", PType.clob(Int.MAX_VALUE))), - -) { args -> - val string = args[0].bytes.toString(Charsets.UTF_8) - val result = string.uppercase() - Datum.clob(result.toByteArray()) + override fun getInstance(args: Array): Fn? { + val inputType = args[0] + return when (inputType.code()) { + PType.CHAR -> { + Function.instance( + name = "upper", + returns = PType.character(inputType.length), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].string + val result = string.uppercase() + Datum.character(result, inputType.length) + } + } + PType.VARCHAR -> { + Function.instance( + name = "upper", + returns = PType.varchar(inputType.length), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].string + val result = string.uppercase() + Datum.varchar(result, inputType.length) + } + } + PType.CLOB -> { + Function.instance( + name = "upper", + returns = PType.clob(inputType.length), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].bytes.toString(Charsets.UTF_8) + val result = string.uppercase() + Datum.clob(result.toByteArray(), inputType.length) + } + } + PType.STRING -> Function.instance( + name = "upper", + returns = PType.string(), + parameters = arrayOf(Parameter("value", inputType)), + ) { params -> + val string = params[0].string + val result = string.uppercase() + Datum.string(result) + } + else -> error("Unsupported type for UPPER function: ${inputType.code()}") + } + } } diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnUtils.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnUtils.kt new file mode 100644 index 000000000..2404d972d --- /dev/null +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/function/builtins/FnUtils.kt @@ -0,0 +1,51 @@ +package org.partiql.spi.function.builtins + +import org.partiql.spi.types.PType + +internal object FnUtils { + const val MAXLENGTH = Int.MAX_VALUE + + /** + * Checks if adding two integers would cause overflow. + * Uses the property: arg1 >= 0 && arg2 >= 0 && arg1 + arg2 < 0 => overflow occurred + */ + fun checkLengthOverflow(length1: Int, length2: Int) { + if (length1 >= 0 && length2 >= 0 && length1 + length2 < 0) { + throw IllegalArgumentException("String length overflow: $length1 + $length2 exceeds maximum allowed length ($MAXLENGTH)") + } + } + + /** + * Safely adds two lengths and returns the result, throwing if overflow occurs. + */ + fun addLengths(length1: Int, length2: Int): Int { + checkLengthOverflow(length1, length2) + return length1 + length2 + } + + /** + * Gets the length of a type, handling STRING types that don't have length constraints. + */ + fun getTypeLength(type: PType): Int { + return when (type.code()) { + PType.STRING -> error("STRING type does not have length constraints") + else -> type.length + } + } + + /** + * Returns the type with higher coercibility for string type coercion. + * Coercibility order: STRING > CLOB > VARCHAR > CHAR + */ + fun getHigherCoercibilityType(type1: Int, type2: Int): Int { + val coercibility = mapOf( + PType.STRING to 4, + PType.CLOB to 3, + PType.VARCHAR to 2, + PType.CHAR to 1 + ) + val coer1 = coercibility[type1] ?: error("Unknown type: $type1") + val coer2 = coercibility[type2] ?: error("Unknown type: $type2") + return if (coer1 >= coer2) type1 else type2 + } +} diff --git a/test/partiql-tests b/test/partiql-tests index c40ef9738..67d22dcd5 160000 --- a/test/partiql-tests +++ b/test/partiql-tests @@ -1 +1 @@ -Subproject commit c40ef9738ed812ac8322c8c63aa3cbe3e2fee18f +Subproject commit 67d22dcd5a7eeaaa2624be9d1bb3da6a8b9faf0a