Skip to content
Merged
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions partiql-parser/src/main/antlr/PartiQLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PRuntimeException> {
FnTestUtils.getQueryResultType("CAST('some string' AS VARCHAR($maxInt)) || CAST('a' AS VARCHAR(1))")
}
}

@Test
fun `concat with CHAR overflow throws exception`() {
assertThrows<PRuntimeException> {
FnTestUtils.getQueryResultType("CAST('some string' AS CHAR($maxInt)) || CAST('a' AS CHAR(1))")
}
}

@Test
fun `concat with CLOB overflow throws exception`() {
assertThrows<PRuntimeException> {
FnTestUtils.getQueryResultType("CAST('some string' AS CLOB($maxInt)) || CAST('a' AS CLOB(1))")
}
}

@Test
fun `concat with VARCHAR and CHAR overflow throws exception`() {
assertThrows<PRuntimeException> {
FnTestUtils.getQueryResultType("CAST('some string' AS VARCHAR($maxInt)) || CAST('a' AS CHAR(1))")
}
}

@Test
fun `concat with CLOB and CHAR overflow throws exception`() {
assertThrows<PRuntimeException> {
FnTestUtils.getQueryResultType("CAST('some string' AS CLOB($maxInt)) || CAST('a' AS CHAR(1))")
}
}

@Test
fun `concat with CLOB and VARCHAR overflow throws exception`() {
assertThrows<PRuntimeException> {
FnTestUtils.getQueryResultType("CAST('some string' AS CLOB($maxInt)) || CAST('a' AS VARCHAR(1))")
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<PRuntimeException> {
FnTestUtils.getQueryResultType("LOWER(42)")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PRuntimeException> {
FnTestUtils.getQueryResultType("TRIM(42)")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PRuntimeException> {
FnTestUtils.getQueryResultType("UPPER(42)")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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()) {
Expand All @@ -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()) {
Expand Down
3 changes: 2 additions & 1 deletion partiql-spi/src/main/java/org/partiql/spi/value/Datum.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 6 additions & 14 deletions partiql-spi/src/main/kotlin/org/partiql/spi/function/Builtins.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
//
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading
Loading