diff --git a/partiql-plan/src/main/resources/partiql_plan.ion b/partiql-plan/src/main/resources/partiql_plan.ion index 675d32de7..30ec4258a 100644 --- a/partiql-plan/src/main/resources/partiql_plan.ion +++ b/partiql-plan/src/main/resources/partiql_plan.ion @@ -154,6 +154,19 @@ rex::{ nullifier: rex }, + is_null::{ + value: rex, + }, + + is_missing::{ + value: rex, + }, + + is_type::{ + value: rex, + type: static_type, + }, + coalesce::{ args: list::[rex] }, diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/SqlTypes.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/SqlTypes.kt new file mode 100644 index 000000000..94d0c234d --- /dev/null +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/SqlTypes.kt @@ -0,0 +1,215 @@ +package org.partiql.planner.internal + +import org.partiql.ast.Type +import org.partiql.types.Field +import org.partiql.types.PType + +/** + * A factory for single-source of truth for type creations — DO NOT CREATE PTYPE DIRECTLY. + * + * This allows us to raise an interface if we need custom type factories; for now just use defaults with static methods. + */ +internal object SqlTypes { + + private const val MAX_SIZE = Int.MAX_VALUE + + // + // DYNAMIC + // + + @JvmStatic + fun dynamic(): PType = PType.typeDynamic() + + // + // BOOLEAN + // + + @JvmStatic + fun bool(): PType = PType.typeBool() + + // + // NUMERIC + // + + @JvmStatic + fun tinyint(): PType = PType.typeTinyInt() + + @JvmStatic + fun smallint(): PType = PType.typeSmallInt() + + @JvmStatic + fun int(): PType = PType.typeInt() + + @JvmStatic + fun bigint(): PType = PType.typeBigInt() + + /** + * NUMERIC represents an integer with arbitrary precision. It is equivalent to Ion’s integer type, and is conformant to SQL-99s rules for the NUMERIC type. In SQL-99, if a scale is omitted then we choose zero — and if a precision is omitted then the precision is implementation defined. For PartiQL, we define this precision to be inf — aka arbitrary precision. + * + * @param precision Defaults to inf. + * @param scale Defaults to 0. + * @return + */ + @JvmStatic + fun numeric(precision: Int? = null, scale: Int? = null): PType { + if (scale != null && precision == null) { + error("Precision can never be null while scale is specified.") + } + return when { + precision != null && scale != null -> PType.typeDecimal(precision, scale) + precision != null -> PType.typeDecimal(precision, 0) + else -> PType.typeIntArbitrary() + } + } + + /** + * DECIMAL represents an exact numeric type with arbitrary precision and arbitrary scale. It is equivalent to Ion’s decimal type. For a DECIMAL with no given scale we choose inf rather than the SQL prescribed 0 (zero). Here we diverge from SQL-99 for Ion compatibility. Finally, SQL defines decimals as having precision equal to or greater than the given precision. Like other systems, we truncate extraneous precision so that NUMERIC(p,s) is equivalent to DECIMAL(p,s). The only difference between them is the default scale when it’s not specified — we follow SQL-99 for NUMERIC, and we follow Postgres for DECIMAL. + * + * @param precision Defaults to inf. + * @param scale Defaults to 0 when precision is given, otherwise inf. + * @return + */ + @JvmStatic + fun decimal(precision: Int? = null, scale: Int? = null): PType { + if (scale != null && precision == null) { + error("Precision can never be null while scale is specified.") + } + return when { + precision != null && scale != null -> PType.typeDecimal(precision, scale) + precision != null -> PType.typeDecimal(precision, 0) + else -> PType.typeDecimalArbitrary() + } + } + + @JvmStatic + fun real(): PType = PType.typeReal() + + @JvmStatic + fun double(): PType = PType.typeDoublePrecision() + + // + // CHARACTER STRINGS + // + + @JvmStatic + fun char(length: Int? = null): PType = PType.typeChar(length ?: 1) + + @JvmStatic + fun varchar(length: Int? = null): PType = PType.typeVarChar(length ?: MAX_SIZE) + + @JvmStatic + fun string(): PType = PType.typeString() + + @JvmStatic + fun clob(length: Int? = null) = PType.typeClob(length ?: MAX_SIZE) + + // + // BIT STRINGS + // + + @JvmStatic + fun blob(length: Int? = null) = PType.typeBlob(length ?: MAX_SIZE) + + // + // DATETIME + // + + @JvmStatic + fun date(): PType = TODO() + + @JvmStatic + fun time(precision: Int? = null): PType = PType.typeTimeWithoutTZ(precision ?: 6) + + @JvmStatic + fun timez(precision: Int? = null): PType = PType.typeTimeWithTZ(precision ?: 6) + + @JvmStatic + fun timestamp(precision: Int? = null): PType = PType.typeTimeWithoutTZ(precision ?: 6) + + @JvmStatic + fun timestampz(precision: Int? = null): PType = PType.typeTimestampWithTZ(precision ?: 6) + + // + // COLLECTIONS + // + + @JvmStatic + fun array(element: PType? = null, size: Int? = null): PType { + if (size != null) { + error("Fixed-length ARRAY [N] is not supported.") + } + return when (element) { + null -> PType.typeList() + else -> PType.typeList(element) + } + } + + @JvmStatic + fun bag(element: PType? = null, size: Int? = null): PType { + if (size != null) { + error("Fixed-length BAG [N] is not supported.") + } + return when (element) { + null -> PType.typeBag() + else -> PType.typeBag(element) + } + } + + // + // STRUCTURAL + // + + @JvmStatic + fun struct(): PType = PType.typeStruct() + + @JvmStatic + fun row(fields: List): PType = PType.typeRow(fields) + + /** + * Create PType from the AST type. + */ + @JvmStatic + fun from(type: Type): PType = when (type) { + is Type.NullType -> error("Casting to NULL is not supported.") + is Type.Missing -> error("Casting to MISSING is not supported.") + is Type.Bool -> bool() + is Type.Tinyint -> tinyint() + is Type.Smallint, is Type.Int2 -> smallint() + is Type.Int4, is Type.Int -> int() + is Type.Bigint, is Type.Int8 -> bigint() + is Type.Numeric -> numeric(type.precision, type.scale) + is Type.Decimal -> decimal(type.precision, type.scale) + is Type.Real -> real() + is Type.Float32 -> real() + is Type.Float64 -> double() + is Type.Char -> char(type.length) + is Type.Varchar -> varchar(type.length) + is Type.String -> string() + is Type.Symbol -> { + // TODO will we continue supporting symbol? + PType.typeSymbol() + } + is Type.Bit -> error("BIT is not supported yet.") + is Type.BitVarying -> error("BIT VARYING is not supported yet.") + is Type.ByteString -> error("BINARY is not supported yet.") + is Type.Blob -> blob(type.length) + is Type.Clob -> clob(type.length) + is Type.Date -> date() + is Type.Time -> time(type.precision) + is Type.TimeWithTz -> timez(type.precision) + is Type.Timestamp -> timestamp(type.precision) + is Type.TimestampWithTz -> timestampz(type.precision) + is Type.Interval -> error("INTERVAL is not supported yet.") + is Type.Bag -> bag() + is Type.Sexp -> { + // TODO will we continue supporting s-expression? + PType.typeSexp() + } + is Type.Any -> dynamic() + is Type.List -> array() + is Type.Array -> array(type.type?.let { from(it) }) + is Type.Tuple -> struct() + is Type.Struct -> struct() + is Type.Custom -> TODO("Custom type not supported ") + } +} diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt index ba059f1f7..3940a88a4 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt @@ -61,6 +61,9 @@ import org.partiql.planner.`internal`.ir.builder.RexOpCastUnresolvedBuilder import org.partiql.planner.`internal`.ir.builder.RexOpCoalesceBuilder import org.partiql.planner.`internal`.ir.builder.RexOpCollectionBuilder import org.partiql.planner.`internal`.ir.builder.RexOpErrBuilder +import org.partiql.planner.`internal`.ir.builder.RexOpIsMissingBuilder +import org.partiql.planner.`internal`.ir.builder.RexOpIsNullBuilder +import org.partiql.planner.`internal`.ir.builder.RexOpIsTypeBuilder import org.partiql.planner.`internal`.ir.builder.RexOpLitBuilder import org.partiql.planner.`internal`.ir.builder.RexOpMissingBuilder import org.partiql.planner.`internal`.ir.builder.RexOpNullifBuilder @@ -256,6 +259,9 @@ internal data class Rex( is Call -> visitor.visitRexOpCall(this, ctx) is Case -> visitor.visitRexOpCase(this, ctx) is Nullif -> visitor.visitRexOpNullif(this, ctx) + is IsNull -> visitor.visitRexOpIsNull(this, ctx) + is IsMissing -> visitor.visitRexOpIsMissing(this, ctx) + is IsType -> visitor.visitRexOpIsType(this, ctx) is Coalesce -> visitor.visitRexOpCoalesce(this, ctx) is Collection -> visitor.visitRexOpCollection(this, ctx) is Struct -> visitor.visitRexOpStruct(this, ctx) @@ -646,6 +652,68 @@ internal data class Rex( } } + internal data class IsNull( + @JvmField + internal val `value`: Rex, + ) : Op() { + public override val children: List by lazy { + val kids = mutableListOf() + kids.add(value) + kids.filterNotNull() + } + + + public override fun accept(visitor: PlanVisitor, ctx: C): R = + visitor.visitRexOpIsNull(this, ctx) + + internal companion object { + @JvmStatic + internal fun builder(): RexOpIsNullBuilder = RexOpIsNullBuilder() + } + } + + internal data class IsMissing( + @JvmField + internal val `value`: Rex, + ) : Op() { + public override val children: List by lazy { + val kids = mutableListOf() + kids.add(value) + kids.filterNotNull() + } + + + public override fun accept(visitor: PlanVisitor, ctx: C): R = + visitor.visitRexOpIsMissing(this, ctx) + + internal companion object { + @JvmStatic + internal fun builder(): RexOpIsMissingBuilder = RexOpIsMissingBuilder() + } + } + + internal data class IsType( + @JvmField + internal val `value`: Rex, + @JvmField + internal val type: CompilerType, + ) : Op() { + public override val children: List by lazy { + val kids = mutableListOf() + kids.add(value) + kids.filterNotNull() + } + + + public override fun accept(visitor: PlanVisitor, ctx: C): R = + visitor.visitRexOpIsType(this, ctx) + + internal companion object { + @JvmStatic + internal fun builder(): RexOpIsTypeBuilder = RexOpIsTypeBuilder() + } + } + internal data class Coalesce( @JvmField internal val args: List, diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt index 4e8c7a146..f38ab5002 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt @@ -105,7 +105,7 @@ internal class PlanTransform( super.visitRexOpVar(node, ctx) as org.partiql.plan.Rex.Op override fun visitRexOpVarUnresolved(node: Rex.Op.Var.Unresolved, ctx: Unit) = - error("The Internal Plan Node Rex.Op.Var.Unresolved should be converted to an MISSING Node during type resolution if resolution failed") + error("The Internal Plan Node Rex.Op.Var.Unresolved for `${node.identifier}` should be converted to a MISSING Node during type resolution if resolution failed") override fun visitRexOpVarGlobal(node: Rex.Op.Var.Global, ctx: Unit) = org.partiql.plan.Rex.Op.Global( ref = visitRef(node.ref, ctx) @@ -173,6 +173,21 @@ internal class PlanTransform( ) } + override fun visitRexOpIsNull(node: Rex.Op.IsNull, ctx: Unit): PlanNode { + val value = visitRex(node.value, ctx) + return org.partiql.plan.Rex.Op.IsNull(value) + } + + override fun visitRexOpIsMissing(node: Rex.Op.IsMissing, ctx: Unit): PlanNode { + val value = visitRex(node.value, ctx) + return org.partiql.plan.Rex.Op.IsMissing(value) + } + + override fun visitRexOpIsType(node: Rex.Op.IsType, ctx: Unit): PlanNode { + val value = visitRex(node.value, ctx) + return org.partiql.plan.Rex.Op.IsType(value, node.type) + } + override fun visitRexOpCallDynamicCandidate(node: Rex.Op.Call.Dynamic.Candidate, ctx: Unit): PlanNode { val fn = visitRef(node.fn, ctx) val coercions = node.coercions.map { it?.let { visitRefCast(it, ctx) } } diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt index f7b81d9f5..ff65c43fb 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/RexConverter.kt @@ -26,6 +26,7 @@ import org.partiql.ast.Type import org.partiql.ast.visitor.AstBaseVisitor import org.partiql.planner.catalog.Identifier import org.partiql.planner.internal.Env +import org.partiql.planner.internal.SqlTypes import org.partiql.planner.internal.ir.Rel import org.partiql.planner.internal.ir.Rex import org.partiql.planner.internal.ir.builder.plan @@ -535,57 +536,16 @@ internal object RexConverter { * IS ? */ override fun visitExprIsType(node: Expr.IsType, ctx: Env): Rex { - val type = BOOL - // arg - val arg0 = visitExprCoerce(node.value, ctx) - - var call = when (val targetType = node.type) { - is Type.NullType -> call("is_null", arg0) - is Type.Missing -> call("is_missing", arg0) - is Type.Bool -> call("is_bool", arg0) - is Type.Tinyint -> call("is_int8", arg0) - is Type.Smallint, is Type.Int2 -> call("is_int16", arg0) - is Type.Int4 -> call("is_int32", arg0) - is Type.Bigint, is Type.Int8 -> call("is_int64", arg0) - is Type.Int -> call("is_int", arg0) - is Type.Real -> call("is_real", arg0) - is Type.Float32 -> call("is_float32", arg0) - is Type.Float64 -> call("is_float64", arg0) - is Type.Decimal -> call("is_decimal", targetType.precision.toRex(), targetType.scale.toRex(), arg0) - is Type.Numeric -> call("is_numeric", targetType.precision.toRex(), targetType.scale.toRex(), arg0) - is Type.Char -> call("is_char", targetType.length.toRex(), arg0) - is Type.Varchar -> call("is_varchar", targetType.length.toRex(), arg0) - is Type.String -> call("is_string", targetType.length.toRex(), arg0) - is Type.Symbol -> call("is_symbol", arg0) - is Type.Bit -> call("is_bit", arg0) - is Type.BitVarying -> call("is_bitVarying", arg0) - is Type.ByteString -> call("is_byteString", arg0) - is Type.Blob -> call("is_blob", arg0) - is Type.Clob -> call("is_clob", arg0) - is Type.Date -> call("is_date", arg0) - is Type.Time -> call("is_time", arg0) - // TODO: DO we want to seperate with time zone vs without time zone into two different type in the plan? - // leave the parameterized type out for now until the above is answered - is Type.TimeWithTz -> call("is_timeWithTz", arg0) - is Type.Timestamp -> call("is_timestamp", arg0) - is Type.TimestampWithTz -> call("is_timestampWithTz", arg0) - is Type.Interval -> call("is_interval", arg0) - is Type.Bag -> call("is_bag", arg0) - is Type.Sexp -> call("is_sexp", arg0) - is Type.Any -> call("is_any", arg0) - is Type.Custom -> call("is_custom", arg0) - is Type.List -> call("is_list", arg0) - is Type.Tuple -> call("is_tuple", arg0) - // Note that for is function, the parser will reject parameterized list/struct - is Type.Array -> call("is_list", arg0) - is Type.Struct -> call("is_struct", arg0) + val value = visitExprCoerce(node.value, ctx) + var op = when (val type = node.type) { + is Type.NullType -> Rex.Op.IsNull(value) + is Type.Missing -> Rex.Op.IsMissing(value) + else -> Rex.Op.IsType(value, SqlTypes.from(node.type).toCType()) } - if (node.not == true) { - call = negate(call) + op = negate(op) } - - return rex(type, call) + return rex(BOOL, op) } override fun visitExprCoalesce(node: Expr.Coalesce, ctx: Env): Rex { @@ -711,67 +671,11 @@ internal object RexConverter { } override fun visitExprCast(node: Expr.Cast, ctx: Env): Rex { - val type = visitType(node.asType) + val type = SqlTypes.from(node.asType).toCType() val arg = visitExprCoerce(node.value, ctx) return rex(ANY, rexOpCastUnresolved(type, arg)) } - private fun visitType(type: Type): CompilerType { - return when (type) { - is Type.NullType -> error("Casting to NULL is not supported.") - is Type.Missing -> error("Casting to MISSING is not supported.") - is Type.Bool -> PType.typeBool() - is Type.Tinyint -> PType.typeTinyInt() - is Type.Smallint, is Type.Int2 -> PType.typeSmallInt() - is Type.Int4 -> PType.typeInt() - is Type.Bigint, is Type.Int8 -> PType.typeBigInt() - is Type.Int -> PType.typeIntArbitrary() - is Type.Real -> PType.typeReal() - is Type.Float32 -> PType.typeReal() - is Type.Float64 -> PType.typeDoublePrecision() - is Type.Decimal -> when { - type.precision == null && type.scale == null -> PType.typeDecimalArbitrary() - type.precision != null && type.scale != null -> PType.typeDecimal(type.precision!!, type.scale!!) - type.precision != null && type.scale == null -> PType.typeDecimal(type.precision!!, 0) - else -> error("Precision can never be null while scale is specified.") - } - - is Type.Numeric -> when { - type.precision == null && type.scale == null -> PType.typeDecimalArbitrary() - type.precision != null && type.scale != null -> PType.typeDecimal(type.precision!!, type.scale!!) - type.precision != null && type.scale == null -> PType.typeDecimal(type.precision!!, 0) - else -> error("Precision can never be null while scale is specified.") - } - - is Type.Char -> PType.typeChar(type.length ?: 255) // TODO: What is default? - is Type.Varchar -> error("VARCHAR is not supported yet.") - is Type.String -> PType.typeString() - is Type.Symbol -> PType.typeSymbol() - is Type.Bit -> error("BIT is not supported yet.") - is Type.BitVarying -> error("BIT VARYING is not supported yet.") - is Type.ByteString -> error("BINARY is not supported yet.") - is Type.Blob -> PType.typeBlob(type.length ?: Int.MAX_VALUE) - is Type.Clob -> PType.typeClob(type.length ?: Int.MAX_VALUE) - is Type.Date -> PType.typeDate() - is Type.Time -> PType.typeTimeWithoutTZ(type.precision ?: 6) - is Type.TimeWithTz -> PType.typeTimeWithTZ(type.precision ?: 6) - is Type.Timestamp -> PType.typeTimestampWithoutTZ(type.precision ?: 6) - is Type.TimestampWithTz -> PType.typeTimestampWithTZ(type.precision ?: 6) - is Type.Interval -> error("INTERVAL is not supported yet.") - is Type.Bag -> PType.typeBag() - is Type.Sexp -> PType.typeSexp() - is Type.Any -> PType.typeDynamic() - is Type.Custom -> TODO("Custom type not supported ") - is Type.List -> PType.typeList() - is Type.Tuple -> PType.typeStruct() - is Type.Array -> when (type.type) { - null -> PType.typeList() - else -> PType.typeList(visitType(type.type!!)) - } - is Type.Struct -> PType.typeStruct() - }.toCType() - } - override fun visitExprCanCast(node: Expr.CanCast, ctx: Env): Rex { TODO("PartiQL Special Form CAN_CAST") } @@ -854,7 +758,7 @@ internal object RexConverter { // Helpers - private fun negate(call: Rex.Op.Call): Rex.Op.Call { + private fun negate(call: Rex.Op): Rex.Op.Call { val name = Expr.Unary.Op.NOT.name val id = Identifier.delimited(name.lowercase()) // wrap @@ -872,8 +776,6 @@ internal object RexConverter { return rexOpCallUnresolved(id, args.toList()) } - private fun Int?.toRex() = rex(INT4, rexOpLit(int32Value(this))) - private val ANY: CompilerType = CompilerType(PType.typeDynamic()) private val BOOL: CompilerType = CompilerType(PType.typeBool()) private val STRING: CompilerType = CompilerType(PType.typeString()) diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt index acfcec6fb..fdf233eea 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt @@ -937,6 +937,18 @@ internal class PlanTyper(private val env: Env) { return rex(type, op) } + override fun visitRexOpIsNull(node: Rex.Op.IsNull, ctx: CompilerType?): Rex { + return rex(PType.typeBool().toCType(), node) + } + + override fun visitRexOpIsMissing(node: Rex.Op.IsMissing, ctx: CompilerType?): Rex { + return rex(PType.typeBool().toCType(), node) + } + + override fun visitRexOpIsType(node: Rex.Op.IsType, ctx: CompilerType?): Rex { + return rex(PType.typeBool().toCType(), node) + } + /** * In this context, Boolean means PartiQLValueType Bool, which can be nullable. * Hence, we permit Static Type BOOL, Static Type NULL, Static Type Missing here. diff --git a/partiql-planner/src/main/resources/partiql_plan_internal.ion b/partiql-planner/src/main/resources/partiql_plan_internal.ion index e36194121..d8b138fc4 100644 --- a/partiql-planner/src/main/resources/partiql_plan_internal.ion +++ b/partiql-planner/src/main/resources/partiql_plan_internal.ion @@ -157,6 +157,19 @@ rex::{ nullifier: rex }, + is_null::{ + value: rex, + }, + + is_missing::{ + value: rex, + }, + + is_type::{ + value: rex, + type: static_type, + }, + coalesce::{ args: list::[rex] },