From e25056c4c8494d8713ffb3109645cf7e38e1c4c6 Mon Sep 17 00:00:00 2001 From: John Ed Quinn Date: Fri, 12 Jul 2024 14:35:37 -0700 Subject: [PATCH 1/2] Updates rules for variable resolution Adds support for casting from dynamic Updates tests to give greater visibility into errors --- .../eval/internal/operator/rex/ExprCast.kt | 25 +++++- .../eval/internal/PartiQLEngineDefaultTest.kt | 78 ++++++++----------- .../partiql/planner/internal/FnResolver.kt | 13 +++- .../planner/internal/typer/PlanTyper.kt | 20 ++--- .../partiql/planner/internal/typer/TypeEnv.kt | 55 ++++++++++--- .../planner/PlannerErrorReportingTests.kt | 68 ++++++---------- .../planner/internal/typer/TypeEnvTest.kt | 29 +++++++ .../partiql/spi/connector/sql/SqlBuiltins.kt | 1 - .../spi/connector/sql/builtins/FnNot.kt | 19 ----- .../value/PartiQLValueComparatorInternal.kt | 8 +- test/partiql-tests | 2 +- 11 files changed, 175 insertions(+), 143 deletions(-) diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprCast.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprCast.kt index eb67517299..fed0834d0a 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprCast.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprCast.kt @@ -23,6 +23,7 @@ import org.partiql.value.Int64Value import org.partiql.value.Int8Value import org.partiql.value.IntValue import org.partiql.value.ListValue +import org.partiql.value.MissingValue import org.partiql.value.NullValue import org.partiql.value.NumericValue import org.partiql.value.PartiQLValue @@ -46,6 +47,7 @@ import org.partiql.value.int64Value import org.partiql.value.int8Value import org.partiql.value.intValue import org.partiql.value.listValue +import org.partiql.value.missingValue import org.partiql.value.sexpValue import org.partiql.value.stringValue import org.partiql.value.structValue @@ -59,7 +61,8 @@ import java.math.BigInteger internal class ExprCast(val arg: Operator.Expr, val cast: Ref.Cast) : Operator.Expr { @OptIn(PartiQLValueExperimental::class) override fun eval(env: Environment): Datum { - val arg = arg.eval(env).toPartiQLValue() + val argDatum = arg.eval(env) + val arg = argDatum.toPartiQLValue() try { val partiqlValue = when (PType.fromPartiQLValueType(arg.type).kind) { PType.Kind.DYNAMIC -> TODO("Not Possible") @@ -86,9 +89,9 @@ internal class ExprCast(val arg: Operator.Expr, val cast: Ref.Cast) : Operator.E PType.Kind.BAG -> castFromCollection(arg as BagValue<*>, cast.target) PType.Kind.LIST -> castFromCollection(arg as ListValue<*>, cast.target) PType.Kind.SEXP -> castFromCollection(arg as SexpValue<*>, cast.target) - PType.Kind.STRUCT -> TODO("CAST FROM STRUCT not yet implemented") + PType.Kind.STRUCT -> castFromStruct(argDatum, cast.target).toPartiQLValue() PType.Kind.ROW -> TODO("CAST FROM ROW not yet implemented") - PType.Kind.UNKNOWN -> TODO("CAST FROM UNKNOWN not yet implemented") + PType.Kind.UNKNOWN -> castFromUnknown(arg, cast.target) PType.Kind.VARCHAR -> TODO("CAST FROM VARCHAR not yet implemented") } return Datum.of(partiqlValue) @@ -97,6 +100,22 @@ internal class ExprCast(val arg: Operator.Expr, val cast: Ref.Cast) : Operator.E } } + /** + * For now, we cannot cast from struct to anything else. Throw a type check exception. + */ + private fun castFromStruct(value: Datum, t: PType): Datum { + throw TypeCheckException() + } + + @OptIn(PartiQLValueExperimental::class) + private fun castFromUnknown(value: PartiQLValue, t: PType): PartiQLValue { + return when (value) { + is NullValue -> castFromNull(value, t) + is MissingValue -> missingValue() // TODO: Is this allowed? + else -> error("This shouldn't have happened") + } + } + @OptIn(PartiQLValueExperimental::class) private fun castFromNull(value: NullValue, t: PType): PartiQLValue { return when (t.kind) { diff --git a/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt index 48ad446478..13a9e34156 100644 --- a/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt +++ b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt @@ -1311,7 +1311,16 @@ class PartiQLEngineDefaultTest { internal fun assert() { val permissiveResult = run(mode = PartiQLEngine.Mode.PERMISSIVE) - assert(expectedPermissive == permissiveResult.first) { + val assertionCondition = try { + expectedPermissive == permissiveResult.first + } catch (t: Throwable) { + val str = buildString { + appendLine("Test Name: $name") + PlanPrinter.append(this, permissiveResult.second) + } + throw RuntimeException(str, t) + } + assert(assertionCondition) { comparisonString(expectedPermissive, permissiveResult.first, permissiveResult.second) } var error: Throwable? = null @@ -1344,7 +1353,13 @@ class PartiQLEngineDefaultTest { val prepared = engine.prepare(plan.plan, PartiQLEngine.Session(mapOf("memory" to connector), mode = mode)) when (val result = engine.execute(prepared)) { is PartiQLResult.Value -> return result.value to plan.plan - is PartiQLResult.Error -> throw result.cause + is PartiQLResult.Error -> { + val str = buildString { + appendLine("Execution resulted in an unexpected error. Plan:") + PlanPrinter.append(this, plan.plan) + } + throw RuntimeException(str, result.cause) + } } } @@ -1368,51 +1383,26 @@ class PartiQLEngineDefaultTest { } @Test + @Disabled fun developmentTest() { val tc = SuccessTestCase( input = """ - SELECT * - EXCLUDE - t.a.b.c[*].field_x - FROM [{ - 'a': { - 'b': { - 'c': [ - { -- c[0]; field_x to be removed - 'field_x': 0, - 'field_y': 0 - }, - { -- c[1]; field_x to be removed - 'field_x': 1, - 'field_y': 1 - }, - { -- c[2]; field_x to be removed - 'field_x': 2, - 'field_y': 2 - } - ] - } - } - }] AS t - """.trimIndent(), - expected = bagValue( - structValue( - "a" to structValue( - "b" to structValue( - "c" to listValue( - structValue( - "field_y" to int32Value(0) - ), - structValue( - "field_y" to int32Value(1) - ), - structValue( - "field_y" to int32Value(2) - ) - ) - ) - ) - ) + SELECT VALUE + CASE x + 1 + WHEN NULL THEN 'shouldnt be null' + WHEN MISSING THEN 'shouldnt be missing' + WHEN i THEN 'ONE' + WHEN f THEN 'TWO' + WHEN d THEN 'THREE' + ELSE '?' + END + FROM << i, f, d, null, missing >> AS x + """, + expected = boolValue(true), + globals = listOf( + SuccessTestCase.Global("i", "1"), + SuccessTestCase.Global("f", "2e0"), + SuccessTestCase.Global("d", "3.") ) ) tc.assert() diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/FnResolver.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/FnResolver.kt index ceea6f1c53..33501e4d47 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/FnResolver.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/FnResolver.kt @@ -3,6 +3,7 @@ package org.partiql.planner.internal import org.partiql.planner.internal.casts.Coercions import org.partiql.planner.internal.ir.Ref import org.partiql.planner.internal.typer.CompilerType +import org.partiql.planner.internal.typer.PlanTyper.Companion.toCType import org.partiql.spi.fn.FnExperimental import org.partiql.spi.fn.FnSignature import org.partiql.types.PType.Kind @@ -144,10 +145,14 @@ internal object FnResolver { exactInputTypes++ continue } - // 2. Match ANY, no coercion needed - // TODO: Rewrite args in this scenario - arg.kind == Kind.UNKNOWN || p.type.kind == Kind.DYNAMIC || arg.kind == Kind.DYNAMIC -> continue - // 3. Check for a coercion + // 2. Match ANY parameter, no coercion needed + p.type.kind == Kind.DYNAMIC -> continue + arg.kind == Kind.UNKNOWN -> continue + // 3. Allow for ANY arguments + arg.kind == Kind.DYNAMIC -> { + mapping[i] = Ref.Cast(arg, p.type.toCType(), Ref.Cast.Safety.UNSAFE, true) + } + // 4. Check for a coercion else -> when (val coercion = Coercions.get(arg, p.type)) { null -> return null // short-circuit else -> mapping[i] = coercion 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 50108818b5..3b6d5813c3 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 @@ -437,7 +437,7 @@ internal class PlanTyper(private val env: Env) { // Rewrite LHS and RHS val lhs = visitRel(node.lhs, ctx) val stack = when (node.type) { - Rel.Op.Join.Type.INNER, Rel.Op.Join.Type.LEFT -> outer + listOf(TypeEnv(lhs.type.schema, outer)) + Rel.Op.Join.Type.INNER, Rel.Op.Join.Type.LEFT -> outer + listOf(TypeEnv(env, lhs.type.schema, outer)) Rel.Op.Join.Type.FULL, Rel.Op.Join.Type.RIGHT -> outer } val rhs = RelTyper(stack, Scope.GLOBAL).visitRel(node.rhs, ctx) @@ -447,7 +447,7 @@ internal class PlanTyper(private val env: Env) { val type = relType(schema, ctx!!.props) // Type the condition on the output schema - val condition = node.rex.type(TypeEnv(type.schema, outer)) + val condition = node.rex.type(TypeEnv(env, type.schema, outer)) val op = relOpJoin(lhs, rhs, condition, node.type) return rel(type, op) @@ -500,7 +500,7 @@ internal class PlanTyper(private val env: Env) { val resolvedRoot = when (val root = path.root) { is Rex.Op.Var.Unresolved -> { // resolve `root` to local binding - val locals = TypeEnv(input.type.schema, outer) + val locals = TypeEnv(env, input.type.schema, outer) val path = root.identifier.toBindingPath() val resolved = locals.resolve(path) if (resolved == null) { @@ -540,7 +540,7 @@ internal class PlanTyper(private val env: Env) { val input = visitRel(node.input, ctx) // type the calls and groups - val typer = RexTyper(TypeEnv(input.type.schema, outer), Scope.LOCAL) + val typer = RexTyper(TypeEnv(env, input.type.schema, outer), Scope.LOCAL) // typing of aggregate calls is slightly more complicated because they are not expressions. val calls = node.calls.mapIndexed { i, call -> @@ -610,8 +610,8 @@ internal class PlanTyper(private val env: Env) { Rex.Op.Var.Scope.LOCAL -> Scope.LOCAL } val resolvedVar = when (scope) { - Scope.LOCAL -> locals.resolve(path) ?: env.resolveObj(path) - Scope.GLOBAL -> env.resolveObj(path) ?: locals.resolve(path) + Scope.LOCAL -> locals.resolve(path, TypeEnv.LookupStrategy.LOCALS_FIRST) + Scope.GLOBAL -> locals.resolve(path, TypeEnv.LookupStrategy.GLOBALS_FIRST) } if (resolvedVar == null) { val id = PlanUtils.externalize(node.identifier) @@ -1065,7 +1065,7 @@ internal class PlanTyper(private val env: Env) { override fun visitRexOpPivot(node: Rex.Op.Pivot, ctx: CompilerType?): Rex { val stack = locals.outer + listOf(locals) val rel = node.rel.type(stack) - val typeEnv = TypeEnv(rel.type.schema, stack) + val typeEnv = TypeEnv(env, rel.type.schema, stack) val typer = RexTyper(typeEnv, Scope.LOCAL) val key = typer.visitRex(node.key, null) val value = typer.visitRex(node.value, null) @@ -1075,7 +1075,7 @@ internal class PlanTyper(private val env: Env) { override fun visitRexOpSubquery(node: Rex.Op.Subquery, ctx: CompilerType?): Rex { val rel = node.rel.type(locals.outer + listOf(locals)) - val newTypeEnv = TypeEnv(schema = rel.type.schema, outer = locals.outer + listOf(locals)) + val newTypeEnv = TypeEnv(env, schema = rel.type.schema, outer = locals.outer + listOf(locals)) val constructor = node.constructor.type(newTypeEnv) val subquery = rexOpSubquery(constructor, rel, node.coercion) return when (node.coercion) { @@ -1122,7 +1122,7 @@ internal class PlanTyper(private val env: Env) { // TODO: Should we support the ROW type? override fun visitRexOpSelect(node: Rex.Op.Select, ctx: CompilerType?): Rex { val rel = node.rel.type(locals.outer + listOf(locals)) - val newTypeEnv = TypeEnv(schema = rel.type.schema, outer = locals.outer + listOf(locals)) + val newTypeEnv = TypeEnv(env, schema = rel.type.schema, outer = locals.outer + listOf(locals)) val constructor = node.constructor.type(newTypeEnv) val type = when (rel.isOrdered()) { true -> PType.typeList(constructor.type) @@ -1299,7 +1299,7 @@ internal class PlanTyper(private val env: Env) { * This types the [Rex] given the input record ([input]) and [stack] of [TypeEnv] (representing the outer scopes). */ private fun Rex.type(input: List, stack: List, strategy: Scope = Scope.LOCAL) = - RexTyper(TypeEnv(input, stack), strategy).visitRex(this, this.type) + RexTyper(TypeEnv(env, input, stack), strategy).visitRex(this, this.type) /** * This types the [Rex] given a [TypeEnv]. We use the [TypeEnv.schema] as the input schema and the [TypeEnv.outer] diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt index 17a7acb3c3..97d0c43986 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt @@ -1,5 +1,6 @@ package org.partiql.planner.internal.typer +import org.partiql.planner.internal.Env import org.partiql.planner.internal.ir.Rel import org.partiql.planner.internal.ir.Rex import org.partiql.planner.internal.ir.rex @@ -22,10 +23,16 @@ import org.partiql.value.stringValue * @property outer refers to the outer variable scopes that we have access to. */ internal data class TypeEnv( + private val globals: Env, public val schema: List, public val outer: List ) { + enum class LookupStrategy { + LOCALS_FIRST, + GLOBALS_FIRST + } + internal fun getScope(depth: Int): TypeEnv { return when (depth) { 0 -> this @@ -34,24 +41,48 @@ internal data class TypeEnv( } /** - * We resolve a local with the following rules. See, PartiQL Specification p.35. - * - * 1) Check if the path root unambiguously matches a local binding name, set as root. - * 2) Check if the path root unambiguously matches a local binding struct value field. - * + * Search Algorithm (LOCALS_FIRST): + * 1. Match Binding Name + * - Match Locals + * - Match Globals + * 2. Match Nested Field + * - Match Locals + * Search Algorithm (GLOBALS_FIRST): + * 1. Match Binding Name + * - Match Globals + * - Match Locals + * 2. Match Nested Field + * - Match Locals + */ + fun resolve(path: BindingPath, strategy: LookupStrategy = LookupStrategy.LOCALS_FIRST): Rex? { + return when (strategy) { + LookupStrategy.LOCALS_FIRST -> resolveLocalName(path) ?: globals.resolveObj(path) ?: resolveLocalField(path) + LookupStrategy.GLOBALS_FIRST -> globals.resolveObj(path) ?: resolveLocalName(path) ?: resolveLocalField(path) + } + } + + /** + * Attempts to resolve using just the local binding name. + */ + private fun resolveLocalName(path: BindingPath): Rex? { + val head: BindingName = path.steps[0] + val tail: List = path.steps.drop(1) + val r = matchRoot(head) ?: return null + // Convert any remaining binding names (tail) to an untyped path expression. + return if (tail.isEmpty()) r else r.toPath(tail) + } + + /** + * Check if the path root unambiguously matches a local binding struct value field. * Convert any remaining binding names (tail) to a path expression. * * @param path * @return */ - fun resolve(path: BindingPath): Rex? { + private fun resolveLocalField(path: BindingPath): Rex? { val head: BindingName = path.steps[0] - var tail: List = path.steps.drop(1) - var r = matchRoot(head) - if (r == null) { - r = matchStruct(head) ?: return null - tail = path.steps - } + val r = matchStruct(head) ?: return null + val tail = path.steps // Convert any remaining binding names (tail) to an untyped path expression. return if (tail.isEmpty()) r else r.toPath(tail) } diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/PlannerErrorReportingTests.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/PlannerErrorReportingTests.kt index 4d8f33a87f..e97c546a9b 100644 --- a/partiql-planner/src/test/kotlin/org/partiql/planner/PlannerErrorReportingTests.kt +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/PlannerErrorReportingTests.kt @@ -18,6 +18,7 @@ import org.partiql.types.PType import org.partiql.types.StaticType import org.partiql.types.StructType import org.partiql.types.TupleConstraint +import java.lang.AssertionError import kotlin.test.assertEquals internal class PlannerErrorReportingTests { @@ -61,38 +62,39 @@ internal class PlannerErrorReportingTests { parser.parse(query).root } - fun assertProblem( + private fun assertProblem( plan: org.partiql.plan.PlanNode, problems: List, - vararg block: () -> Boolean + block: (List) -> Unit ) { - block.forEachIndexed { index, function -> - assert(function.invoke()) { - buildString { - this.appendLine("assertion #${index + 1} failed") + try { + block.invoke(problems) + } catch (e: Throwable) { + val str = buildString { + this.appendLine("Assertion failed") - this.appendLine("--------Plan---------") - PlanPrinter.append(this, plan) + this.appendLine("--------Plan---------") + PlanPrinter.append(this, plan) - this.appendLine("----------Problems---------") - problems.forEach { - this.appendLine(it.toString()) - } + this.appendLine("----------Problems---------") + problems.forEach { + this.appendLine(it.toString()) } } + throw AssertionError(str, e) } } data class TestCase( val query: String, val isSignal: Boolean, - val assertion: (List) -> List<() -> Boolean>, + val assertion: (List) -> Unit, val expectedType: CompilerType ) { constructor( query: String, isSignal: Boolean, - assertion: (List) -> List<() -> Boolean>, + assertion: (List) -> Unit, expectedType: StaticType = StaticType.ANY ) : this(query, isSignal, assertion, PType.fromStaticType(expectedType).toCType()) } @@ -110,11 +112,9 @@ internal class PlannerErrorReportingTests { ) ) - private fun assertOnProblemCount(warningCount: Int, errorCount: Int): (List) -> List<() -> Boolean> = { problems -> - listOf( - { problems.filter { it.details.severity == ProblemSeverity.WARNING }.size == warningCount }, - { problems.filter { it.details.severity == ProblemSeverity.ERROR }.size == errorCount }, - ) + private fun assertOnProblemCount(warningCount: Int, errorCount: Int): (List) -> Unit = { problems -> + assertEquals(warningCount, problems.filter { it.details.severity == ProblemSeverity.WARNING }.size, "Number of warnings is wrong.") + assertEquals(errorCount, problems.filter { it.details.severity == ProblemSeverity.ERROR }.size, "Number of errors is wrong.") } /** @@ -278,13 +278,13 @@ internal class PlannerErrorReportingTests { TestCase( "1 + not_a_function(1)", false, - assertOnProblemCount(1, 1), + assertOnProblemCount(0, 1), StaticType.INT4, ), TestCase( "1 + not_a_function(1)", true, - assertOnProblemCount(0, 2), + assertOnProblemCount(0, 1), StaticType.INT4, ), @@ -408,7 +408,7 @@ internal class PlannerErrorReportingTests { assertProblem( plan, problems, - *tc.assertion(problems).toTypedArray() + tc.assertion ) assertEquals(tc.expectedType, (plan.statement as org.partiql.plan.Statement.Query).root.type) } @@ -420,28 +420,4 @@ internal class PlannerErrorReportingTests { @ParameterizedTest @MethodSource("testContinuation") fun testContinuation(tc: TestCase) = runTestCase(tc) - - private fun StaticType.assertStaticTypeEqual(other: StaticType) { - val thisAll = this.allTypes.toSet() - val otherAll = other.allTypes.toSet() - val diff = (thisAll - otherAll) + (otherAll - thisAll) - assert(diff.isEmpty()) { - buildString { - this.appendLine("expected: ") - thisAll.forEach { - this.append("$it, ") - } - this.appendLine() - this.appendLine("actual") - otherAll.forEach { - this.append("$it, ") - } - this.appendLine() - this.appendLine("diff") - diff.forEach { - this.append("$it, ") - } - } - } - } } diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/TypeEnvTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/TypeEnvTest.kt index a5274f20da..7f41920e1d 100644 --- a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/TypeEnvTest.kt +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/TypeEnvTest.kt @@ -4,12 +4,17 @@ import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.ExecutionMode import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource +import org.partiql.planner.PartiQLPlanner +import org.partiql.planner.internal.Env import org.partiql.planner.internal.ir.Rex import org.partiql.planner.internal.ir.relBinding import org.partiql.planner.internal.typer.PlanTyper.Companion.toCType import org.partiql.spi.BindingCase import org.partiql.spi.BindingName import org.partiql.spi.BindingPath +import org.partiql.spi.connector.ConnectorHandle +import org.partiql.spi.connector.ConnectorMetadata +import org.partiql.spi.fn.FnExperimental import org.partiql.types.PType import kotlin.test.assertEquals import kotlin.test.fail @@ -30,6 +35,30 @@ internal class TypeEnvTest { */ @JvmStatic val locals = TypeEnv( + Env( + PartiQLPlanner.Session( + "queryId", + "userId", + "currentCatalog", + catalogs = mapOf( + "currentCatalog" to object : ConnectorMetadata { + override fun getObject(path: BindingPath): ConnectorHandle.Obj? { + return null + } + + @FnExperimental + override fun getFunction(path: BindingPath): ConnectorHandle.Fn? { + return null + } + + @FnExperimental + override fun getAggregation(path: BindingPath): ConnectorHandle.Agg? { + return null + } + } + ) + ) + ), listOf( relBinding("A", struct("B" to PType.typeBool().toCType())), relBinding("a", struct("b" to PType.typeBool().toCType())), diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/connector/sql/SqlBuiltins.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/connector/sql/SqlBuiltins.kt index 6678cb0840..7781f7f91c 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/connector/sql/SqlBuiltins.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/connector/sql/SqlBuiltins.kt @@ -365,7 +365,6 @@ internal object SqlBuiltins { Fn_NEG__FLOAT32__FLOAT32, Fn_NEG__FLOAT64__FLOAT64, Fn_NEG__DECIMAL_ARBITRARY__DECIMAL_ARBITRARY, - Fn_NOT__MISSING__BOOL, Fn_NOT__BOOL__BOOL, Fn_OR__BOOL_BOOL__BOOL, Fn_OCTET_LENGTH__STRING__INT32, diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/connector/sql/builtins/FnNot.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/connector/sql/builtins/FnNot.kt index 5642e5ae5f..935857e927 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/connector/sql/builtins/FnNot.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/connector/sql/builtins/FnNot.kt @@ -11,7 +11,6 @@ import org.partiql.value.BoolValue import org.partiql.value.PartiQLValue import org.partiql.value.PartiQLValueExperimental import org.partiql.value.PartiQLValueType.BOOL -import org.partiql.value.PartiQLValueType.MISSING import org.partiql.value.boolValue import org.partiql.value.check @@ -33,21 +32,3 @@ internal object Fn_NOT__BOOL__BOOL : Fn { return boolValue(value.not()) } } - -@OptIn(PartiQLValueExperimental::class, FnExperimental::class) -internal object Fn_NOT__MISSING__BOOL : Fn { - - override val signature = FnSignature( - name = "not", - returns = BOOL, - parameters = listOf(FnParameter("value", MISSING)), - isNullable = true, - isNullCall = true, - isMissable = false, - isMissingCall = false, - ) - - override fun invoke(args: Array): PartiQLValue { - return boolValue(null) - } -} diff --git a/partiql-types/src/main/kotlin/org/partiql/value/PartiQLValueComparatorInternal.kt b/partiql-types/src/main/kotlin/org/partiql/value/PartiQLValueComparatorInternal.kt index 604d03f192..474c0d4cec 100644 --- a/partiql-types/src/main/kotlin/org/partiql/value/PartiQLValueComparatorInternal.kt +++ b/partiql-types/src/main/kotlin/org/partiql/value/PartiQLValueComparatorInternal.kt @@ -8,9 +8,11 @@ import org.partiql.value.util.isZero @OptIn(PartiQLValueExperimental::class) internal class PartiQLValueComparatorInternal(private val nullsFirst: Boolean) : Comparator { - private val EQUAL = 0 - private val LESS = -1 - private val GREATER = 1 + companion object { + private const val EQUAL = 0 + private const val LESS = -1 + private const val GREATER = 1 + } private fun PartiQLValue.isNullOrMissing(): Boolean = this is NullValue || this is MissingValue || this.isNull private fun PartiQLValue.isLob(): Boolean = this is BlobValue || this is ClobValue diff --git a/test/partiql-tests b/test/partiql-tests index be88ae732b..c65b854e1d 160000 --- a/test/partiql-tests +++ b/test/partiql-tests @@ -1 +1 @@ -Subproject commit be88ae732bec0388c88acab108a392f586094fc7 +Subproject commit c65b854e1dad88354af92fd018f306dca9a8a45a From 86718c2019cf6fe8de3971a1fd80c7361723789a Mon Sep 17 00:00:00 2001 From: John Ed Quinn Date: Mon, 15 Jul 2024 16:03:39 -0700 Subject: [PATCH 2/2] Renames TypeEnv to Scope Renames Scope to Strategy Initializes TypeEnv to represent both locals and globals --- .../org/partiql/planner/internal/Env.kt | 2 +- .../planner/internal/typer/PlanTyper.kt | 88 +++++---- .../partiql/planner/internal/typer/Scope.kt | 179 ++++++++++++++++- .../planner/internal/typer/Strategy.kt | 14 ++ .../partiql/planner/internal/typer/TypeEnv.kt | 184 +----------------- .../typer/{TypeEnvTest.kt => ScopeTest.kt} | 22 ++- 6 files changed, 251 insertions(+), 238 deletions(-) create mode 100644 partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/Strategy.kt rename partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/{TypeEnvTest.kt => ScopeTest.kt} (86%) diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/Env.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/Env.kt index aa7d22491d..69ed9fb2f6 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/Env.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/Env.kt @@ -16,7 +16,7 @@ import org.partiql.planner.internal.ir.rexOpCallDynamicCandidate import org.partiql.planner.internal.ir.rexOpCastResolved import org.partiql.planner.internal.ir.rexOpVarGlobal import org.partiql.planner.internal.typer.CompilerType -import org.partiql.planner.internal.typer.TypeEnv.Companion.toPath +import org.partiql.planner.internal.typer.Scope.Companion.toPath import org.partiql.spi.BindingCase import org.partiql.spi.BindingName import org.partiql.spi.BindingPath 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 3b6d5813c3..976f60829a 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 @@ -64,6 +64,7 @@ import org.partiql.value.MissingValue import org.partiql.value.PartiQLValueExperimental import org.partiql.value.TextValue import org.partiql.value.stringValue +import java.lang.reflect.Type import kotlin.math.max /** @@ -82,7 +83,7 @@ internal class PlanTyper(private val env: Env) { throw IllegalArgumentException("PartiQLPlanner only supports Query statements") } // root TypeEnv has no bindings - val root = statement.root.type(emptyList(), emptyList(), Scope.GLOBAL) + val root = statement.root.type(emptyList(), emptyList(), Strategy.GLOBAL) return statementQuery(root) } internal companion object { @@ -189,8 +190,8 @@ internal class PlanTyper(private val env: Env) { * @property strategy */ private inner class RelTyper( - private val outer: List, - private val strategy: Scope, + private val outer: List, + private val strategy: Strategy, ) : PlanRewriter() { override fun visitRel(node: Rel, ctx: Rel.Type?) = visitRelOp(node.op, node.type) as Rel @@ -200,7 +201,7 @@ internal class PlanTyper(private val env: Env) { */ override fun visitRelOpScan(node: Rel.Op.Scan, ctx: Rel.Type?): Rel { // descend, with GLOBAL resolution strategy - val rex = node.rex.type(emptyList(), outer, Scope.GLOBAL) + val rex = node.rex.type(emptyList(), outer, Strategy.GLOBAL) // compute rel type val valueT = getElementTypeForFromSource(rex.type) val type = ctx!!.copyWithSchema(listOf(valueT)) @@ -214,7 +215,7 @@ internal class PlanTyper(private val env: Env) { */ override fun visitRelOpScanIndexed(node: Rel.Op.ScanIndexed, ctx: Rel.Type?): Rel { // descend, with GLOBAL resolution strategy - val rex = node.rex.type(emptyList(), outer, Scope.GLOBAL) + val rex = node.rex.type(emptyList(), outer, Strategy.GLOBAL) // compute rel type val valueT = getElementTypeForFromSource(rex.type) val indexT = PType.typeBigInt() @@ -228,7 +229,7 @@ internal class PlanTyper(private val env: Env) { * TODO handle NULL|STRUCT type */ override fun visitRelOpUnpivot(node: Rel.Op.Unpivot, ctx: Rel.Type?): Rel { - val rex = node.rex.type(emptyList(), outer, Scope.GLOBAL) + val rex = node.rex.type(emptyList(), outer, Strategy.GLOBAL) val op = relOpUnpivot(rex) val kType = PType.typeString() @@ -383,7 +384,7 @@ internal class PlanTyper(private val env: Env) { val input = visitRel(node.input, ctx) // type limit expression using outer scope with global resolution // TODO: Assert expression doesn't contain locals or upvalues. - val limit = node.limit.type(input.type.schema, outer, Scope.GLOBAL) + val limit = node.limit.type(input.type.schema, outer, Strategy.GLOBAL) // check types if (limit.type.isNumeric().not()) { val err = ProblemGenerator.missingRex( @@ -403,7 +404,7 @@ internal class PlanTyper(private val env: Env) { val input = visitRel(node.input, ctx) // type offset expression using outer scope with global resolution // TODO: Assert expression doesn't contain locals or upvalues. - val offset = node.offset.type(input.type.schema, outer, Scope.GLOBAL) + val offset = node.offset.type(input.type.schema, outer, Strategy.GLOBAL) // check types if (offset.type.isNumeric().not()) { val err = ProblemGenerator.missingRex( @@ -437,17 +438,18 @@ internal class PlanTyper(private val env: Env) { // Rewrite LHS and RHS val lhs = visitRel(node.lhs, ctx) val stack = when (node.type) { - Rel.Op.Join.Type.INNER, Rel.Op.Join.Type.LEFT -> outer + listOf(TypeEnv(env, lhs.type.schema, outer)) + Rel.Op.Join.Type.INNER, Rel.Op.Join.Type.LEFT -> outer + listOf(Scope(lhs.type.schema, outer)) Rel.Op.Join.Type.FULL, Rel.Op.Join.Type.RIGHT -> outer } - val rhs = RelTyper(stack, Scope.GLOBAL).visitRel(node.rhs, ctx) + val rhs = RelTyper(stack, Strategy.GLOBAL).visitRel(node.rhs, ctx) // Calculate output schema given JOIN type val schema = lhs.type.schema + rhs.type.schema val type = relType(schema, ctx!!.props) // Type the condition on the output schema - val condition = node.rex.type(TypeEnv(env, type.schema, outer)) + val typeEnv = TypeEnv(env, Scope(type.schema, outer)) + val condition = node.rex.type(typeEnv) val op = relOpJoin(lhs, rhs, condition, node.type) return rel(type, op) @@ -500,9 +502,10 @@ internal class PlanTyper(private val env: Env) { val resolvedRoot = when (val root = path.root) { is Rex.Op.Var.Unresolved -> { // resolve `root` to local binding - val locals = TypeEnv(env, input.type.schema, outer) + val locals = Scope(input.type.schema, outer) + val typeEnv = TypeEnv(env, locals) val path = root.identifier.toBindingPath() - val resolved = locals.resolve(path) + val resolved = typeEnv.resolve(path) if (resolved == null) { ProblemGenerator.missingRex( emptyList(), @@ -540,7 +543,8 @@ internal class PlanTyper(private val env: Env) { val input = visitRel(node.input, ctx) // type the calls and groups - val typer = RexTyper(TypeEnv(env, input.type.schema, outer), Scope.LOCAL) + val typeEnv = TypeEnv(env, Scope(input.type.schema, outer)) + val typer = RexTyper(typeEnv, Strategy.LOCAL) // typing of aggregate calls is slightly more complicated because they are not expressions. val calls = node.calls.mapIndexed { i, call -> @@ -574,12 +578,12 @@ internal class PlanTyper(private val env: Env) { * * We should consider making the PType? parameter non-nullable. * - * @property locals TypeEnv in which this rex tree is evaluated. + * @property typeEnv TypeEnv in which this rex tree is evaluated. */ @OptIn(PartiQLValueExperimental::class) private inner class RexTyper( - private val locals: TypeEnv, - private val strategy: Scope, + private val typeEnv: TypeEnv, + private val strategy: Strategy, ) : PlanRewriter() { override fun visitRex(node: Rex, ctx: CompilerType?): Rex = visitRexOp(node.op, node.type) as Rex @@ -590,9 +594,9 @@ internal class PlanTyper(private val env: Env) { } override fun visitRexOpVarLocal(node: Rex.Op.Var.Local, ctx: CompilerType?): Rex { - val scope = locals.getScope(node.depth) + val scope = typeEnv.locals.getScope(node.depth) assert(node.ref < scope.schema.size) { - "Invalid resolved variable (var ${node.ref}, stack frame ${node.depth}) in env: $locals" + "Invalid resolved variable (var ${node.ref}, stack frame ${node.depth}) in env: $typeEnv" } val type = scope.schema.getOrNull(node.ref)?.type ?: error("Can't find locals value.") return rex(type, node) @@ -605,17 +609,14 @@ internal class PlanTyper(private val env: Env) { override fun visitRexOpVarUnresolved(node: Rex.Op.Var.Unresolved, ctx: CompilerType?): Rex { val path = node.identifier.toBindingPath() - val scope = when (node.scope) { + val strategy = when (node.scope) { Rex.Op.Var.Scope.DEFAULT -> strategy - Rex.Op.Var.Scope.LOCAL -> Scope.LOCAL - } - val resolvedVar = when (scope) { - Scope.LOCAL -> locals.resolve(path, TypeEnv.LookupStrategy.LOCALS_FIRST) - Scope.GLOBAL -> locals.resolve(path, TypeEnv.LookupStrategy.GLOBALS_FIRST) + Rex.Op.Var.Scope.LOCAL -> Strategy.LOCAL } + val resolvedVar = typeEnv.resolve(path, strategy) if (resolvedVar == null) { val id = PlanUtils.externalize(node.identifier) - val inScopeVariables = locals.schema.map { it.name }.toSet() + val inScopeVariables = typeEnv.locals.schema.map { it.name }.toSet() val err = ProblemGenerator.errorRex( causes = emptyList(), problem = ProblemGenerator.undefinedVariable(id, inScopeVariables) @@ -735,7 +736,7 @@ internal class PlanTyper(private val env: Env) { // Find Type val field = root.type.getSymbol(node.key) ?: run { - val inScopeVariables = locals.schema.map { it.name }.toSet() + val inScopeVariables = typeEnv.locals.schema.map { it.name }.toSet() return ProblemGenerator.missingRex( Rex.Op.Path.Symbol(root, node.key), ProblemGenerator.undefinedVariable( @@ -1063,10 +1064,11 @@ internal class PlanTyper(private val env: Env) { } override fun visitRexOpPivot(node: Rex.Op.Pivot, ctx: CompilerType?): Rex { - val stack = locals.outer + listOf(locals) + val stack = typeEnv.locals.outer + listOf(typeEnv.locals) val rel = node.rel.type(stack) - val typeEnv = TypeEnv(env, rel.type.schema, stack) - val typer = RexTyper(typeEnv, Scope.LOCAL) + val scope = Scope(rel.type.schema, stack) + val typeEnv = TypeEnv(env, scope) + val typer = RexTyper(typeEnv, Strategy.LOCAL) val key = typer.visitRex(node.key, null) val value = typer.visitRex(node.value, null) val op = rexOpPivot(key, value, rel) @@ -1074,9 +1076,10 @@ internal class PlanTyper(private val env: Env) { } override fun visitRexOpSubquery(node: Rex.Op.Subquery, ctx: CompilerType?): Rex { - val rel = node.rel.type(locals.outer + listOf(locals)) - val newTypeEnv = TypeEnv(env, schema = rel.type.schema, outer = locals.outer + listOf(locals)) - val constructor = node.constructor.type(newTypeEnv) + val rel = node.rel.type(typeEnv.locals.outer + listOf(typeEnv.locals)) + val newScope = Scope(schema = rel.type.schema, outer = typeEnv.locals.outer + listOf(typeEnv.locals)) + val typeEnv = TypeEnv(env, newScope) + val constructor = node.constructor.type(typeEnv) val subquery = rexOpSubquery(constructor, rel, node.coercion) return when (node.coercion) { Rex.Op.Subquery.Coercion.SCALAR -> visitRexOpSubqueryScalar(subquery, constructor.type) @@ -1121,9 +1124,10 @@ internal class PlanTyper(private val env: Env) { // TODO: Should we support the ROW type? override fun visitRexOpSelect(node: Rex.Op.Select, ctx: CompilerType?): Rex { - val rel = node.rel.type(locals.outer + listOf(locals)) - val newTypeEnv = TypeEnv(env, schema = rel.type.schema, outer = locals.outer + listOf(locals)) - val constructor = node.constructor.type(newTypeEnv) + val rel = node.rel.type(typeEnv.locals.outer + listOf(typeEnv.locals)) + val newScope = Scope(schema = rel.type.schema, outer = typeEnv.locals.outer + listOf(typeEnv.locals)) + val typeEnv = TypeEnv(env, newScope) + val constructor = node.constructor.type(typeEnv) val type = when (rel.isOrdered()) { true -> PType.typeList(constructor.type) false -> PType.typeBag(constructor.type) @@ -1292,20 +1296,20 @@ internal class PlanTyper(private val env: Env) { // HELPERS - private fun Rel.type(stack: List, strategy: Scope = Scope.LOCAL): Rel = + private fun Rel.type(stack: List, strategy: Strategy = Strategy.LOCAL): Rel = RelTyper(stack, strategy).visitRel(this, null) /** - * This types the [Rex] given the input record ([input]) and [stack] of [TypeEnv] (representing the outer scopes). + * This types the [Rex] given the input record ([input]) and [stack] of [Scope] (representing the outer scopes). */ - private fun Rex.type(input: List, stack: List, strategy: Scope = Scope.LOCAL) = - RexTyper(TypeEnv(env, input, stack), strategy).visitRex(this, this.type) + private fun Rex.type(input: List, stack: List, strategy: Strategy = Strategy.LOCAL) = + RexTyper(TypeEnv(env, Scope(input, stack)), strategy).visitRex(this, this.type) /** - * This types the [Rex] given a [TypeEnv]. We use the [TypeEnv.schema] as the input schema and the [TypeEnv.outer] + * This types the [Rex] given a [Scope]. We use the [Scope.schema] as the input schema and the [Scope.outer] * as the outer scopes/ */ - private fun Rex.type(typeEnv: TypeEnv, strategy: Scope = Scope.LOCAL) = + private fun Rex.type(typeEnv: TypeEnv, strategy: Strategy = Strategy.LOCAL) = RexTyper(typeEnv, strategy).visitRex(this, this.type) /** diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/Scope.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/Scope.kt index 42eda6d22d..1d87b81757 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/Scope.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/Scope.kt @@ -1,14 +1,177 @@ package org.partiql.planner.internal.typer +import org.partiql.planner.internal.ir.Rel +import org.partiql.planner.internal.ir.Rex +import org.partiql.planner.internal.ir.rex +import org.partiql.planner.internal.ir.rexOpLit +import org.partiql.planner.internal.ir.rexOpPathKey +import org.partiql.planner.internal.ir.rexOpPathSymbol +import org.partiql.planner.internal.ir.rexOpVarLocal +import org.partiql.spi.BindingCase +import org.partiql.spi.BindingName +import org.partiql.spi.BindingPath +import org.partiql.types.PType +import org.partiql.types.PType.Kind +import org.partiql.types.StaticType +import org.partiql.value.PartiQLValueExperimental +import org.partiql.value.stringValue + /** - * Variable resolution strategies — https://partiql.org/assets/PartiQL-Specification.pdf#page=35 + * Represents local variable scopes. * - * | Value | Strategy | Scoping Rules | - * |------------+-----------------------+---------------| - * | LOCAL | local-first lookup | Rules 1, 2 | - * | GLOBAL | global-first lookup | Rule 3 | + * @property outer refers to the outer variable scopes that we have access to. */ -internal enum class Scope { - LOCAL, - GLOBAL, +internal data class Scope( + public val schema: List, + public val outer: List +) { + + internal fun getScope(depth: Int): Scope { + return when (depth) { + 0 -> this + else -> outer[outer.size - depth] + } + } + + /** + * Attempts to resolve using just the local binding name. + */ + fun resolveName(path: BindingPath): Rex? { + val head: BindingName = path.steps[0] + val tail: List = path.steps.drop(1) + val r = matchRoot(head) ?: return null + // Convert any remaining binding names (tail) to an untyped path expression. + return if (tail.isEmpty()) r else r.toPath(tail) + } + + /** + * Check if the path root unambiguously matches a local binding struct value field. + * Convert any remaining binding names (tail) to a path expression. + * + * @param path + * @return + */ + fun resolveField(path: BindingPath): Rex? { + val head: BindingName = path.steps[0] + val r = matchStruct(head) ?: return null + val tail = path.steps + // Convert any remaining binding names (tail) to an untyped path expression. + return if (tail.isEmpty()) r else r.toPath(tail) + } + + /** + * Debugging string, ex: < x: int, y: string > + * + * @return + */ + override fun toString(): String = "< " + schema.joinToString { "${it.name}: ${it.type}" } + " >" + + /** + * Check if `name` unambiguously matches a local binding name and return its reference; otherwise return null. + * + * @param name + * @return + */ + private fun matchRoot(name: BindingName, depth: Int = 0): Rex? { + var r: Rex? = null + for (i in schema.indices) { + val local = schema[i] + val type = local.type + if (name.matches(local.name)) { + if (r != null) { + // TODO root was already matched, emit ambiguous error. + return null + } + r = rex(type, rexOpVarLocal(depth, i)) + } + } + if (r == null && outer.isNotEmpty()) { + return outer.last().matchRoot(name, depth + 1) + } + return r + } + + /** + * Check if `name` unambiguously matches a field within a struct and return its reference; otherwise return null. + * + * @param name + * @return + */ + private fun matchStruct(name: BindingName, depth: Int = 0): Rex? { + var c: Rex? = null + var known = false + for (i in schema.indices) { + val local = schema[i] + val type = local.type + when (type.containsKey(name)) { + true -> { + if (c != null && known) { + // TODO root was already definitively matched, emit ambiguous error. + return null + } + c = rex(type, rexOpVarLocal(depth, i)) + known = true + } + null -> { + if (c != null) { + if (known) { + continue + } else { + // TODO we have more than one possible match, emit ambiguous error. + return null + } + } + c = rex(type, rexOpVarLocal(depth, i)) + known = false + } + false -> continue + } + } + if (c == null && outer.isNotEmpty()) { + return outer.last().matchStruct(name, depth + 1) + } + return c + } + + /** + * Searches for the [BindingName] within the given [StaticType]. + * + * Returns + * - true iff known to contain key + * - false iff known to NOT contain key + * - null iff NOT known to contain key + * + * @param name + * @return + */ + private fun CompilerType.containsKey(name: BindingName): Boolean? { + return when (this.kind) { + Kind.ROW -> this.fields!!.any { name.matches(it.name) } + Kind.STRUCT -> null + Kind.DYNAMIC -> null + else -> false + } + } + + companion object { + + /** + * Converts a list of [BindingName] to a path expression. + * + * 1) Case SENSITIVE identifiers become string literal key lookups. + * 2) Case INSENSITIVE identifiers become symbol lookups. + * + * @param steps + * @return + */ + @JvmStatic + @OptIn(PartiQLValueExperimental::class) + internal fun Rex.toPath(steps: List): Rex = steps.fold(this) { curr, step -> + val op = when (step.case) { + BindingCase.SENSITIVE -> rexOpPathKey(curr, rex(CompilerType(PType.typeString()), rexOpLit(stringValue(step.name)))) + BindingCase.INSENSITIVE -> rexOpPathSymbol(curr, step.name) + } + rex(CompilerType(PType.typeDynamic()), op) + } + } } diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/Strategy.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/Strategy.kt new file mode 100644 index 0000000000..c0fae1e547 --- /dev/null +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/Strategy.kt @@ -0,0 +1,14 @@ +package org.partiql.planner.internal.typer + +/** + * Variable resolution strategies — https://partiql.org/assets/PartiQL-Specification.pdf#page=35 + * + * | Value | Strategy | Scoping Rules | + * |------------+-----------------------+---------------| + * | LOCAL | local-first lookup | Rules 1, 2 | + * | GLOBAL | global-first lookup | Rule 3 | + */ +internal enum class Strategy { + LOCAL, + GLOBAL, +} diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt index 97d0c43986..137e513916 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt @@ -1,45 +1,17 @@ package org.partiql.planner.internal.typer import org.partiql.planner.internal.Env -import org.partiql.planner.internal.ir.Rel import org.partiql.planner.internal.ir.Rex -import org.partiql.planner.internal.ir.rex -import org.partiql.planner.internal.ir.rexOpLit -import org.partiql.planner.internal.ir.rexOpPathKey -import org.partiql.planner.internal.ir.rexOpPathSymbol -import org.partiql.planner.internal.ir.rexOpVarLocal -import org.partiql.spi.BindingCase -import org.partiql.spi.BindingName import org.partiql.spi.BindingPath -import org.partiql.types.PType -import org.partiql.types.PType.Kind -import org.partiql.types.StaticType -import org.partiql.value.PartiQLValueExperimental -import org.partiql.value.stringValue /** - * TypeEnv represents a variables type environment. - * - * @property outer refers to the outer variable scopes that we have access to. + * TypeEnv represents the variables type environment (holds references to both locals and globals). */ -internal data class TypeEnv( - private val globals: Env, - public val schema: List, - public val outer: List +internal class TypeEnv( + val globals: Env, + val locals: Scope ) { - enum class LookupStrategy { - LOCALS_FIRST, - GLOBALS_FIRST - } - - internal fun getScope(depth: Int): TypeEnv { - return when (depth) { - 0 -> this - else -> outer[outer.size - depth] - } - } - /** * Search Algorithm (LOCALS_FIRST): * 1. Match Binding Name @@ -54,152 +26,10 @@ internal data class TypeEnv( * 2. Match Nested Field * - Match Locals */ - fun resolve(path: BindingPath, strategy: LookupStrategy = LookupStrategy.LOCALS_FIRST): Rex? { + fun resolve(path: BindingPath, strategy: Strategy = Strategy.LOCAL): Rex? { return when (strategy) { - LookupStrategy.LOCALS_FIRST -> resolveLocalName(path) ?: globals.resolveObj(path) ?: resolveLocalField(path) - LookupStrategy.GLOBALS_FIRST -> globals.resolveObj(path) ?: resolveLocalName(path) ?: resolveLocalField(path) - } - } - - /** - * Attempts to resolve using just the local binding name. - */ - private fun resolveLocalName(path: BindingPath): Rex? { - val head: BindingName = path.steps[0] - val tail: List = path.steps.drop(1) - val r = matchRoot(head) ?: return null - // Convert any remaining binding names (tail) to an untyped path expression. - return if (tail.isEmpty()) r else r.toPath(tail) - } - - /** - * Check if the path root unambiguously matches a local binding struct value field. - * Convert any remaining binding names (tail) to a path expression. - * - * @param path - * @return - */ - private fun resolveLocalField(path: BindingPath): Rex? { - val head: BindingName = path.steps[0] - val r = matchStruct(head) ?: return null - val tail = path.steps - // Convert any remaining binding names (tail) to an untyped path expression. - return if (tail.isEmpty()) r else r.toPath(tail) - } - - /** - * Debugging string, ex: < x: int, y: string > - * - * @return - */ - override fun toString(): String = "< " + schema.joinToString { "${it.name}: ${it.type}" } + " >" - - /** - * Check if `name` unambiguously matches a local binding name and return its reference; otherwise return null. - * - * @param name - * @return - */ - private fun matchRoot(name: BindingName, depth: Int = 0): Rex? { - var r: Rex? = null - for (i in schema.indices) { - val local = schema[i] - val type = local.type - if (name.matches(local.name)) { - if (r != null) { - // TODO root was already matched, emit ambiguous error. - return null - } - r = rex(type, rexOpVarLocal(depth, i)) - } - } - if (r == null && outer.isNotEmpty()) { - return outer.last().matchRoot(name, depth + 1) - } - return r - } - - /** - * Check if `name` unambiguously matches a field within a struct and return its reference; otherwise return null. - * - * @param name - * @return - */ - private fun matchStruct(name: BindingName, depth: Int = 0): Rex? { - var c: Rex? = null - var known = false - for (i in schema.indices) { - val local = schema[i] - val type = local.type - when (type.containsKey(name)) { - true -> { - if (c != null && known) { - // TODO root was already definitively matched, emit ambiguous error. - return null - } - c = rex(type, rexOpVarLocal(depth, i)) - known = true - } - null -> { - if (c != null) { - if (known) { - continue - } else { - // TODO we have more than one possible match, emit ambiguous error. - return null - } - } - c = rex(type, rexOpVarLocal(depth, i)) - known = false - } - false -> continue - } - } - if (c == null && outer.isNotEmpty()) { - return outer.last().matchStruct(name, depth + 1) - } - return c - } - - /** - * Searches for the [BindingName] within the given [StaticType]. - * - * Returns - * - true iff known to contain key - * - false iff known to NOT contain key - * - null iff NOT known to contain key - * - * @param name - * @return - */ - private fun CompilerType.containsKey(name: BindingName): Boolean? { - return when (this.kind) { - Kind.ROW -> this.fields!!.any { name.matches(it.name) } - Kind.STRUCT -> null - Kind.DYNAMIC -> null - else -> false - } - } - - companion object { - - /** - * Converts a list of [BindingName] to a path expression. - * - * 1) Case SENSITIVE identifiers become string literal key lookups. - * 2) Case INSENSITIVE identifiers become symbol lookups. - * - * @param steps - * @return - */ - @JvmStatic - @OptIn(PartiQLValueExperimental::class) - internal fun Rex.toPath(steps: List): Rex = steps.fold(this) { curr, step -> - val op = when (step.case) { - BindingCase.SENSITIVE -> rexOpPathKey(curr, rex(CompilerType(PType.typeString()), rexOpLit(stringValue(step.name)))) - BindingCase.INSENSITIVE -> rexOpPathSymbol(curr, step.name) - } - rex(CompilerType(PType.typeDynamic()), op) + Strategy.LOCAL -> locals.resolveName(path) ?: globals.resolveObj(path) ?: locals.resolveField(path) + Strategy.GLOBAL -> globals.resolveObj(path) ?: locals.resolveName(path) ?: locals.resolveField(path) } } } diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/TypeEnvTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/ScopeTest.kt similarity index 86% rename from partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/TypeEnvTest.kt rename to partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/ScopeTest.kt index 7f41920e1d..97b12a84f1 100644 --- a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/TypeEnvTest.kt +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/ScopeTest.kt @@ -19,7 +19,7 @@ import org.partiql.types.PType import kotlin.test.assertEquals import kotlin.test.fail -internal class TypeEnvTest { +internal class ScopeTest { companion object { @@ -59,15 +59,17 @@ internal class TypeEnvTest { ) ) ), - listOf( - relBinding("A", struct("B" to PType.typeBool().toCType())), - relBinding("a", struct("b" to PType.typeBool().toCType())), - relBinding("X", struct(open = true)), - relBinding("x", struct("Y" to PType.typeBool().toCType(), open = false)), // We currently don't allow for partial schema structs - relBinding("y", struct(open = true)), - relBinding("T", struct("x" to PType.typeBool().toCType(), "x" to PType.typeBool().toCType())), - ), - outer = emptyList() + Scope( + listOf( + relBinding("A", struct("B" to PType.typeBool().toCType())), + relBinding("a", struct("b" to PType.typeBool().toCType())), + relBinding("X", struct(open = true)), + relBinding("x", struct("Y" to PType.typeBool().toCType(), open = false)), // We currently don't allow for partial schema structs + relBinding("y", struct(open = true)), + relBinding("T", struct("x" to PType.typeBool().toCType(), "x" to PType.typeBool().toCType())), + ), + outer = emptyList() + ) ) private fun struct(vararg fields: Pair, open: Boolean = false): CompilerType {