From 7ac5b8f39b1a6737e2e48253d67e6d59b7c23bc3 Mon Sep 17 00:00:00 2001 From: John Ed Quinn Date: Tue, 9 Jul 2024 10:24:46 -0700 Subject: [PATCH 1/3] Simplifies joins and fixes bugs --- .../java/org/partiql/eval/value/Datum.java | 2 - .../org/partiql/eval/value/DatumStruct.java | 42 ++++-- .../org/partiql/eval/internal/Compiler.kt | 10 +- .../eval/internal/helpers/TypesUtility.kt | 80 +++++++++++ .../eval/internal/helpers/ValueUtility.kt | 3 - .../internal/operator/rel/RelJoinInner.kt | 61 +++++++- .../eval/internal/operator/rel/RelJoinLeft.kt | 18 --- .../operator/rel/RelJoinNestedLoop.kt | 91 ------------ .../internal/operator/rel/RelJoinOuterFull.kt | 136 ++++++++++++------ .../internal/operator/rel/RelJoinOuterLeft.kt | 79 ++++++++++ .../operator/rel/RelJoinOuterRight.kt | 75 ++++++++++ .../internal/operator/rel/RelJoinRight.kt | 21 --- .../eval/internal/operator/rex/ExprPathKey.kt | 7 +- .../internal/operator/rex/ExprPathSymbol.kt | 7 +- .../eval/internal/PartiQLEngineDefaultTest.kt | 125 +++++++++++++++- .../src/main/resources/partiql_plan.ion | 2 + 16 files changed, 542 insertions(+), 217 deletions(-) create mode 100644 partiql-eval/src/main/kotlin/org/partiql/eval/internal/helpers/TypesUtility.kt delete mode 100644 partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinLeft.kt delete mode 100644 partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinNestedLoop.kt create mode 100644 partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterLeft.kt create mode 100644 partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterRight.kt delete mode 100644 partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinRight.kt diff --git a/partiql-eval/src/main/java/org/partiql/eval/value/Datum.java b/partiql-eval/src/main/java/org/partiql/eval/value/Datum.java index be3892c1c5..2b068b5f4e 100644 --- a/partiql-eval/src/main/java/org/partiql/eval/value/Datum.java +++ b/partiql-eval/src/main/java/org/partiql/eval/value/Datum.java @@ -317,7 +317,6 @@ default Iterator getFields() { * @throws NullPointerException if this instance also returns true on {@link #isNull()}; callers should check that * {@link #isNull()} returns false before attempting to invoke this method. */ - @NotNull default Datum get(@NotNull String name) { throw new UnsupportedOperationException(); } @@ -331,7 +330,6 @@ default Datum get(@NotNull String name) { * @throws NullPointerException if this instance also returns true on {@link #isNull()}; callers should check that * {@link #isNull()} returns false before attempting to invoke this method. */ - @NotNull default Datum getInsensitive(@NotNull String name) { throw new UnsupportedOperationException(); } diff --git a/partiql-eval/src/main/java/org/partiql/eval/value/DatumStruct.java b/partiql-eval/src/main/java/org/partiql/eval/value/DatumStruct.java index 07b18dcdba..1bc319954b 100644 --- a/partiql-eval/src/main/java/org/partiql/eval/value/DatumStruct.java +++ b/partiql-eval/src/main/java/org/partiql/eval/value/DatumStruct.java @@ -15,10 +15,10 @@ class DatumStruct implements Datum { @NotNull - private final Map> _delegate; + private final HashMap> _delegate; @NotNull - private final Map> _delegateNormalized; + private final HashMap> _delegateNormalized; private final static PType _type = PType.typeStruct(); @@ -50,24 +50,28 @@ public Iterator getFields() { ).iterator(); } - @NotNull @Override public Datum get(@NotNull String name) { - try { - return _delegate.get(name).get(0); - } catch (IndexOutOfBoundsException ex) { - throw new NullPointerException("Could not find struct key: " + name); + List values = _delegate.get(name); + if (values == null) { + return null; + } + if (values.isEmpty()) { + return null; } + return values.get(0); } - @NotNull @Override public Datum getInsensitive(@NotNull String name) { - try { - return _delegateNormalized.get(name).get(0); - } catch (IndexOutOfBoundsException ex) { - throw new NullPointerException("Could not find struct key: " + name); + List values = _delegateNormalized.get(name.toLowerCase()); + if (values == null) { + return null; + } + if (values.isEmpty()) { + return null; } + return values.get(0); } @NotNull @@ -75,4 +79,18 @@ public Datum getInsensitive(@NotNull String name) { public PType getType() { return _type; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("struct::{ "); + for (Map.Entry> entry : _delegate.entrySet()) { + sb.append(entry.getKey()); + sb.append(": "); + sb.append(entry.getValue().toString()); + sb.append(", "); + } + sb.append(" }"); + return sb.toString(); + } } diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/Compiler.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/Compiler.kt index 8c95fb0784..c450dffb52 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/Compiler.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/Compiler.kt @@ -11,9 +11,9 @@ import org.partiql.eval.internal.operator.rel.RelFilter import org.partiql.eval.internal.operator.rel.RelIntersectAll import org.partiql.eval.internal.operator.rel.RelIntersectDistinct import org.partiql.eval.internal.operator.rel.RelJoinInner -import org.partiql.eval.internal.operator.rel.RelJoinLeft import org.partiql.eval.internal.operator.rel.RelJoinOuterFull -import org.partiql.eval.internal.operator.rel.RelJoinRight +import org.partiql.eval.internal.operator.rel.RelJoinOuterLeft +import org.partiql.eval.internal.operator.rel.RelJoinOuterRight import org.partiql.eval.internal.operator.rel.RelLimit import org.partiql.eval.internal.operator.rel.RelOffset import org.partiql.eval.internal.operator.rel.RelProject @@ -364,9 +364,9 @@ internal class Compiler( val condition = visitRex(node.rex, ctx) return when (node.type) { Rel.Op.Join.Type.INNER -> RelJoinInner(lhs, rhs, condition) - Rel.Op.Join.Type.LEFT -> RelJoinLeft(lhs, rhs, condition) - Rel.Op.Join.Type.RIGHT -> RelJoinRight(lhs, rhs, condition) - Rel.Op.Join.Type.FULL -> RelJoinOuterFull(lhs, rhs, condition) + Rel.Op.Join.Type.LEFT -> RelJoinOuterLeft(lhs, rhs, condition, rhsType = node.rhs.type) + Rel.Op.Join.Type.RIGHT -> RelJoinOuterRight(lhs, rhs, condition, lhsType = node.lhs.type) + Rel.Op.Join.Type.FULL -> RelJoinOuterFull(lhs, rhs, condition, lhsType = node.lhs.type, rhsType = node.rhs.type) } } diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/helpers/TypesUtility.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/helpers/TypesUtility.kt new file mode 100644 index 0000000000..5857f2eeec --- /dev/null +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/helpers/TypesUtility.kt @@ -0,0 +1,80 @@ +package org.partiql.eval.internal.helpers + +import org.partiql.types.AnyOfType +import org.partiql.types.AnyType +import org.partiql.types.BagType +import org.partiql.types.BlobType +import org.partiql.types.BoolType +import org.partiql.types.ClobType +import org.partiql.types.DateType +import org.partiql.types.DecimalType +import org.partiql.types.FloatType +import org.partiql.types.GraphType +import org.partiql.types.IntType +import org.partiql.types.ListType +import org.partiql.types.MissingType +import org.partiql.types.NullType +import org.partiql.types.SexpType +import org.partiql.types.StaticType +import org.partiql.types.StringType +import org.partiql.types.StructType +import org.partiql.types.SymbolType +import org.partiql.types.TimeType +import org.partiql.types.TimestampType +import org.partiql.value.PartiQLValueExperimental +import org.partiql.value.PartiQLValueType + +internal object TypesUtility { + + @OptIn(PartiQLValueExperimental::class) + internal fun StaticType.toRuntimeType(): PartiQLValueType { + if (this is AnyOfType) { + // handle anyOf(null, T) cases + val t = types.filter { it !is NullType && it !is MissingType } + return if (t.size != 1) { + PartiQLValueType.ANY + } else { + t.first().asRuntimeType() + } + } + return this.asRuntimeType() + } + + @OptIn(PartiQLValueExperimental::class) + private fun StaticType.asRuntimeType(): PartiQLValueType = when (this) { + is AnyOfType -> PartiQLValueType.ANY + is AnyType -> PartiQLValueType.ANY + is BlobType -> PartiQLValueType.BLOB + is BoolType -> PartiQLValueType.BOOL + is ClobType -> PartiQLValueType.CLOB + is BagType -> PartiQLValueType.BAG + is ListType -> PartiQLValueType.LIST + is SexpType -> PartiQLValueType.SEXP + is DateType -> PartiQLValueType.DATE + // TODO: Run time decimal type does not model precision scale constraint yet + // despite that we match to Decimal vs Decimal_ARBITRARY (PVT) here + // but when mapping it back to Static Type, (i.e, mapping function return type to Value Type) + // we can only map to Unconstrained decimal (Static Type) + is DecimalType -> { + when (this.precisionScaleConstraint) { + is DecimalType.PrecisionScaleConstraint.Constrained -> PartiQLValueType.DECIMAL + DecimalType.PrecisionScaleConstraint.Unconstrained -> PartiQLValueType.DECIMAL_ARBITRARY + } + } + is FloatType -> PartiQLValueType.FLOAT64 + is GraphType -> error("Graph type missing from runtime types") + is IntType -> when (this.rangeConstraint) { + IntType.IntRangeConstraint.SHORT -> PartiQLValueType.INT16 + IntType.IntRangeConstraint.INT4 -> PartiQLValueType.INT32 + IntType.IntRangeConstraint.LONG -> PartiQLValueType.INT64 + IntType.IntRangeConstraint.UNCONSTRAINED -> PartiQLValueType.INT + } + MissingType -> PartiQLValueType.MISSING + is NullType -> PartiQLValueType.NULL + is StringType -> PartiQLValueType.STRING + is StructType -> PartiQLValueType.STRUCT + is SymbolType -> PartiQLValueType.SYMBOL + is TimeType -> PartiQLValueType.TIME + is TimestampType -> PartiQLValueType.TIMESTAMP + } +} diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/helpers/ValueUtility.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/helpers/ValueUtility.kt index 312f9a0a2e..9b4db288d3 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/helpers/ValueUtility.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/helpers/ValueUtility.kt @@ -49,7 +49,6 @@ internal object ValueUtility { * @throws NullPointerException if the value is null * @throws TypeCheckException if the value's type is not a text type (string, symbol, char) */ - @OptIn(PartiQLValueExperimental::class) fun Datum.getText(): String { return when (this.type.kind) { PType.Kind.STRING, PType.Kind.SYMBOL, PType.Kind.CHAR -> this.string @@ -67,7 +66,6 @@ internal object ValueUtility { * @throws NullPointerException if the value is null * @throws TypeCheckException if type is not an integer type */ - @OptIn(PartiQLValueExperimental::class) fun Datum.getBigIntCoerced(): BigInteger { return when (this.type.kind) { PType.Kind.TINYINT -> this.byte.toInt().toBigInteger() @@ -90,7 +88,6 @@ internal object ValueUtility { * @throws NullPointerException if the value is null * @throws TypeCheckException if type is not an integer type */ - @OptIn(PartiQLValueExperimental::class) fun Datum.getInt32Coerced(): Int { return when (this.type.kind) { PType.Kind.TINYINT -> this.byte.toInt() diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinInner.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinInner.kt index 265e78a549..64f8ff0891 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinInner.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinInner.kt @@ -1,17 +1,64 @@ package org.partiql.eval.internal.operator.rel +import org.partiql.eval.internal.Environment import org.partiql.eval.internal.Record +import org.partiql.eval.internal.helpers.ValueUtility.isTrue import org.partiql.eval.internal.operator.Operator +import org.partiql.value.PartiQLValueExperimental internal class RelJoinInner( - override val lhs: Operator.Relation, - override val rhs: Operator.Relation, - override val condition: Operator.Expr, -) : RelJoinNestedLoop() { - override fun join(condition: Boolean, lhs: Record, rhs: Record): Record? { - return when (condition) { - true -> lhs + rhs + private val lhs: Operator.Relation, + private val rhs: Operator.Relation, + private val condition: Operator.Expr, +) : RelPeeking() { + + private lateinit var env: Environment + private lateinit var iterator: Iterator + + override fun openPeeking(env: Environment) { + this.env = env + lhs.open(env) + iterator = implementation() + } + + override fun peek(): Record? { + return when (iterator.hasNext()) { + true -> iterator.next() false -> null } } + + override fun closePeeking() { + lhs.close() + rhs.close() + iterator = emptyList().iterator() + } + + /** + * INNER JOIN (LATERAL) + * + * Algorithm: + * ``` + * for lhsRecord in lhs: + * for rhsRecord in rhs(lhsRecord): + * if (condition matches): + * conditionMatched = true + * yield(lhsRecord + rhsRecord) + * ``` + * + * Development Note: The non-lateral version wouldn't need to push to the current environment. + */ + @OptIn(PartiQLValueExperimental::class) + private fun implementation() = iterator { + for (lhsRecord in lhs) { + rhs.open(env.push(lhsRecord)) + for (rhsRecord in rhs) { + val input = lhsRecord + rhsRecord + val result = condition.eval(env.push(input)) + if (result.isTrue()) { + yield(lhsRecord + rhsRecord) + } + } + } + } } diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinLeft.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinLeft.kt deleted file mode 100644 index eae51e9a31..0000000000 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinLeft.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.partiql.eval.internal.operator.rel - -import org.partiql.eval.internal.Record -import org.partiql.eval.internal.operator.Operator - -internal class RelJoinLeft( - override val lhs: Operator.Relation, - override val rhs: Operator.Relation, - override val condition: Operator.Expr, -) : RelJoinNestedLoop() { - - override fun join(condition: Boolean, lhs: Record, rhs: Record): Record { - if (condition.not()) { - rhs.padNull() - } - return lhs + rhs - } -} diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinNestedLoop.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinNestedLoop.kt deleted file mode 100644 index e52817db31..0000000000 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinNestedLoop.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.partiql.eval.internal.operator.rel - -import org.partiql.eval.internal.Environment -import org.partiql.eval.internal.Record -import org.partiql.eval.internal.helpers.IteratorSupplier -import org.partiql.eval.internal.helpers.ValueUtility.isTrue -import org.partiql.eval.internal.operator.Operator -import org.partiql.eval.value.Datum -import org.partiql.eval.value.Field -import org.partiql.types.PType - -internal abstract class RelJoinNestedLoop : RelPeeking() { - - abstract val lhs: Operator.Relation - abstract val rhs: Operator.Relation - abstract val condition: Operator.Expr - - private var lhsRecord: Record? = null - private lateinit var env: Environment - - override fun openPeeking(env: Environment) { - this.env = env - lhs.open(env) - if (lhs.hasNext().not()) { - return - } - lhsRecord = lhs.next() - rhs.open(env.push(lhsRecord!!)) - } - - abstract fun join(condition: Boolean, lhs: Record, rhs: Record): Record? - - override fun peek(): Record? { - if (lhsRecord == null) { - return null - } - var rhsRecord = when (rhs.hasNext()) { - true -> rhs.next() - false -> null - } - var toReturn: Record? = null - do { - // Acquire LHS and RHS Records - if (rhsRecord == null) { - rhs.close() - if (lhs.hasNext().not()) { - return null - } - lhsRecord = lhs.next() - rhs.open(env.push(lhsRecord!!)) - rhsRecord = when (rhs.hasNext()) { - true -> rhs.next() - false -> null - } - } - // Return Joined Record - if (rhsRecord != null && lhsRecord != null) { - val input = lhsRecord!! + rhsRecord - val result = condition.eval(env.push(input)) - toReturn = join(result.isTrue(), lhsRecord!!, rhsRecord) - } - // Move the pointer to the next row for the RHS - if (toReturn == null) rhsRecord = if (rhs.hasNext()) rhs.next() else null - } - while (toReturn == null) - return toReturn - } - - override fun closePeeking() { - lhs.close() - rhs.close() - } - - internal fun Record.padNull() { - this.values.indices.forEach { index -> - this.values[index] = values[index].padNull() - } - } - - private fun Datum.padNull(): Datum { - return when (this.type.kind) { - PType.Kind.STRUCT, PType.Kind.ROW -> { - val newFields = IteratorSupplier { this.fields }.map { - Field.of(it.name, Datum.nullValue()) - } - Datum.structValue(newFields) - } - else -> Datum.nullValue() - } - } -} diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterFull.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterFull.kt index 546c161715..6a5434efbf 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterFull.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterFull.kt @@ -1,61 +1,107 @@ package org.partiql.eval.internal.operator.rel +import org.partiql.eval.internal.Environment import org.partiql.eval.internal.Record +import org.partiql.eval.internal.helpers.ValueUtility.isTrue import org.partiql.eval.internal.operator.Operator +import org.partiql.eval.value.Datum +import org.partiql.plan.Rel -/** - * Here's a simple implementation of FULL OUTER JOIN. The idea is fairly straightforward: - * Iterate through LHS. For each iteration of the LHS, iterate through RHS. Now, check the condition. - * - If the condition passes, return the merged record (equivalent to result of INNER JOIN) - * - If the condition does not pass, we need a way to return two records (one where the LHS is padded with nulls, and - * one where the RHS is padded with nulls). How we do this: - * - We maintain the [previousLhs] and [previousRhs]. If they are null, we then compute the next LHS and RHS. We - * store their values in-memory. Then we return a merged Record where the LHS is padded and the RHS is not (equivalent - * to result of RIGHT OUTER JOIN). - * - If they aren't null, then we pad the RHS with NULLS (we assume we've already padded the LHS) and return (equivalent - * to result of LEFT OUTER JOIN). We also make sure [previousLhs] and [previousRhs] are now null. - * - * Performance Analysis: Assume that [lhs] has size M and [rhs] has size N. - * - Time: O(M * N) - * - Space: O(1) - */ internal class RelJoinOuterFull( - override val lhs: Operator.Relation, - override val rhs: Operator.Relation, - override val condition: Operator.Expr, -) : RelJoinNestedLoop() { - - private var previousLhs: Record? = null - private var previousRhs: Record? = null - - override fun next(): Record { - if (previousLhs != null && previousRhs != null) { - previousRhs!!.padNull() - val newRecord = previousLhs!! + previousRhs!! - previousLhs = null - previousRhs = null - return newRecord + private val lhs: Operator.Relation, + private val rhs: Operator.Relation, + private val condition: Operator.Expr, + lhsType: Rel.Type, + rhsType: Rel.Type +) : RelPeeking() { + + private val lhsPadded = Record( + Array(rhsType.schema.size) { Datum.nullValue(lhsType.schema[it].type) } + ) + + private val rhsPadded = Record( + Array(rhsType.schema.size) { Datum.nullValue(rhsType.schema[it].type) } + ) + + private lateinit var env: Environment + private lateinit var iterator: Iterator + + override fun openPeeking(env: Environment) { + this.env = env + lhs.open(env) + iterator = implementation() + } + + override fun peek(): Record? { + return when (iterator.hasNext()) { + true -> iterator.next() + false -> null } - return super.next() + } + + override fun closePeeking() { + lhs.close() + rhs.close() + iterator = emptyList().iterator() } /** - * Specifically, for FULL OUTER JOIN, when the JOIN Condition ([condition]) is TRUE, we need to return the - * rows merged (without modification). When the JOIN Condition ([condition]) is FALSE, we need to return - * the LHS padded (and merged with RHS not padded) and the RHS padded (merged with the LHS not padded). + * FULL OUTER JOIN (CANNOT BE LATERAL) + * + * Merge Join (special implementation for FULL OUTER). This is used because we don't have a sophisticated enough + * planner to perform the transformation specified by SQL Server: see section + * ["What about full outer joins?"](https://learn.microsoft.com/en-us/archive/blogs/craigfr/nested-loops-join). + * Furthermore, SQL Server allows for merge joins even without an equijoin predicate. See section + * ["Outer and semi-joins"](https://learn.microsoft.com/en-us/archive/blogs/craigfr/merge-join). + * + * + * Algorithm: + * ``` + * for lhsRecord, lhsIndex in lhs_sorted: + * for rhsRecord, rhsIndex in rhs_sorted: + * if (condition matches): + * lhsMatches[lhsIndex] = true + * rhsMatches[rhsIndex] = true + * yield(lhsRecord + rhsRecord) + * for lhsRecord, lhsIndex in lhs_sorted: + * if lhsMatches[lhsIndex] = false: + * yield(lhsRecord, null) + * for rhsRecord, rhsIndex in rhs_sorted: + * if rhsMatches[rhsIndex] = false: + * yield(null, rhsRecord) + * ``` + * + * TODO: Merge joins require that the LHS and RHS are sorted. */ - override fun join(condition: Boolean, lhs: Record, rhs: Record): Record { - when (condition) { - true -> { - previousLhs = null - previousRhs = null + private fun implementation() = iterator { + val lhsMatches = mutableSetOf() + val rhsMatches = mutableSetOf() + for ((lhsIndex, lhsRecord) in lhs.withIndex()) { + rhs.open(env) + for ((rhsIndex, rhsRecord) in rhs.withIndex()) { + val input = lhsRecord + rhsRecord + val result = condition.eval(env.push(input)) + if (result.isTrue()) { + lhsMatches.add(lhsIndex) + rhsMatches.add(rhsIndex) + yield(lhsRecord + rhsRecord) + } } - false -> { - previousLhs = lhs.copy() - previousRhs = rhs.copy() - lhs.padNull() + rhs.close() + } + lhs.close() + lhs.open(env) + for ((lhsIndex, lhsRecord) in lhs.withIndex()) { + if (!lhsMatches.contains(lhsIndex)) { + yield(lhsRecord + rhsPadded) + } + } + lhs.close() + rhs.open(env) + for ((rhsIndex, rhsRecord) in rhs.withIndex()) { + if (!rhsMatches.contains(rhsIndex)) { + yield(lhsPadded + rhsRecord) } } - return lhs + rhs } } diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterLeft.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterLeft.kt new file mode 100644 index 0000000000..3124220bc9 --- /dev/null +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterLeft.kt @@ -0,0 +1,79 @@ +package org.partiql.eval.internal.operator.rel + +import org.partiql.eval.internal.Environment +import org.partiql.eval.internal.Record +import org.partiql.eval.internal.helpers.ValueUtility.isTrue +import org.partiql.eval.internal.operator.Operator +import org.partiql.eval.value.Datum +import org.partiql.plan.Rel +import org.partiql.value.PartiQLValueExperimental + +internal class RelJoinOuterLeft( + private val lhs: Operator.Relation, + private val rhs: Operator.Relation, + private val condition: Operator.Expr, + rhsType: Rel.Type +) : RelPeeking() { + + private val rhsPadded = Record( + Array(rhsType.schema.size) { Datum.nullValue(rhsType.schema[it].type) } + ) + + private lateinit var env: Environment + private lateinit var iterator: Iterator + + override fun openPeeking(env: Environment) { + this.env = env + lhs.open(env) + iterator = implementation() + } + + override fun peek(): Record? { + return when (iterator.hasNext()) { + true -> iterator.next() + false -> null + } + } + + override fun closePeeking() { + lhs.close() + rhs.close() + iterator = emptyList().iterator() + } + + /** + * LEFT OUTER JOIN (LATERAL) + * + * Algorithm: + * ``` + * for lhsRecord in lhs: + * for rhsRecord in rhs(lhsRecord): + * if (condition matches): + * conditionMatched = true + * yield(lhsRecord + rhsRecord) + * if (!conditionMatched): + * yield(lhsRecord + NULL_RECORD) + * ``` + * + * Development Note: The non-lateral version wouldn't need to push to the current environment. + */ + @OptIn(PartiQLValueExperimental::class) + private fun implementation() = iterator { + for (lhsRecord in lhs) { + var lhsMatched = false + rhs.open(env.push(lhsRecord)) + for (rhsRecord in rhs) { + val input = lhsRecord + rhsRecord + val result = condition.eval(env.push(input)) + if (result.isTrue()) { + lhsMatched = true + yield(lhsRecord + rhsRecord) + } + } + rhs.close() + if (!lhsMatched) { + yield(lhsRecord + rhsPadded) + } + } + } +} diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterRight.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterRight.kt new file mode 100644 index 0000000000..a2913b0d43 --- /dev/null +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterRight.kt @@ -0,0 +1,75 @@ +package org.partiql.eval.internal.operator.rel + +import org.partiql.eval.internal.Environment +import org.partiql.eval.internal.Record +import org.partiql.eval.internal.helpers.ValueUtility.isTrue +import org.partiql.eval.internal.operator.Operator +import org.partiql.eval.value.Datum +import org.partiql.plan.Rel + +internal class RelJoinOuterRight( + private val lhs: Operator.Relation, + private val rhs: Operator.Relation, + private val condition: Operator.Expr, + lhsType: Rel.Type +) : RelPeeking() { + + private val lhsPadded = Record( + Array(lhsType.schema.size) { Datum.nullValue(lhsType.schema[it].type) } + ) + + private lateinit var env: Environment + private lateinit var iterator: Iterator + + override fun openPeeking(env: Environment) { + this.env = env + rhs.open(env) + iterator = implementation() + } + + override fun peek(): Record? { + return when (iterator.hasNext()) { + true -> iterator.next() + false -> null + } + } + + override fun closePeeking() { + lhs.close() + rhs.close() + iterator = emptyList().iterator() + } + + /** + * RIGHT OUTER JOIN (CANNOT BE LATERAL) + * + * Algorithm: + * ``` + * for rhsRecord in rhs: + * for lhsRecord in lhs(rhsRecord): + * if (condition matches): + * conditionMatched = true + * yield(lhsRecord + rhsRecord) + * if (!conditionMatched): + * yield(NULL_RECORD + rhsRecord) + * ``` + */ + private fun implementation() = iterator { + for (rhsRecord in rhs) { + var rhsMatched = false + lhs.open(env) + for (lhsRecord in lhs) { + val input = lhsRecord + rhsRecord + val result = condition.eval(env.push(input)) + if (result.isTrue()) { + rhsMatched = true + yield(lhsRecord + rhsRecord) + } + } + lhs.close() + if (!rhsMatched) { + yield(lhsPadded + rhsRecord) + } + } + } +} diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinRight.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinRight.kt deleted file mode 100644 index 4c020f93f8..0000000000 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinRight.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.partiql.eval.internal.operator.rel - -import org.partiql.eval.internal.Record -import org.partiql.eval.internal.operator.Operator - -internal class RelJoinRight( - lhs: Operator.Relation, - rhs: Operator.Relation, - override val condition: Operator.Expr, -) : RelJoinNestedLoop() { - - override val lhs: Operator.Relation = rhs - override val rhs: Operator.Relation = lhs - - override fun join(condition: Boolean, lhs: Record, rhs: Record): Record { - if (condition.not()) { - lhs.padNull() - } - return lhs + rhs - } -} diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprPathKey.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprPathKey.kt index 4a4aef9e67..39cc194136 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprPathKey.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprPathKey.kt @@ -21,11 +21,6 @@ internal class ExprPathKey( return Datum.nullValue() } val keyString = keyEvaluated.string - for (entry in rootEvaluated.fields) { - if (entry.name == keyString) { - return entry.value - } - } - throw TypeCheckException() + return rootEvaluated.get(keyString) ?: throw TypeCheckException() } } diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprPathSymbol.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprPathSymbol.kt index 1cdb7aca28..949828aaa3 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprPathSymbol.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rex/ExprPathSymbol.kt @@ -19,11 +19,6 @@ internal class ExprPathSymbol( if (struct.isNull) { return Datum.nullValue() } - for (entry in struct.fields) { - if (entry.name.equals(symbol, ignoreCase = true)) { - return entry.value - } - } - throw TypeCheckException("Couldn't find symbol '$symbol' in $struct.") + return struct.getInsensitive(symbol) ?: throw TypeCheckException("Couldn't find symbol '$symbol' in $struct.") } } 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 b5049d0aed..8255cd3066 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 @@ -65,6 +65,11 @@ class PartiQLEngineDefaultTest { @Execution(ExecutionMode.CONCURRENT) fun aggregationTests(tc: SuccessTestCase) = tc.assert() + @ParameterizedTest + @MethodSource("joinTestCases") + @Execution(ExecutionMode.CONCURRENT) + fun joinTests(tc: SuccessTestCase) = tc.assert() + @ParameterizedTest @MethodSource("globalsTestCases") @Execution(ExecutionMode.CONCURRENT) @@ -155,6 +160,123 @@ class PartiQLEngineDefaultTest { ), ) + @JvmStatic + fun joinTestCases() = listOf( + // LEFT OUTER JOIN -- Easy + SuccessTestCase( + input = """ + SELECT VALUE [lhs, rhs] + FROM << 0, 1, 2 >> lhs + LEFT OUTER JOIN << 0, 2, 3 >> rhs + ON lhs = rhs + """.trimIndent(), + expected = bagValue( + listValue(int32Value(0), int32Value(0)), + listValue(int32Value(1), int32Value(null)), + listValue(int32Value(2), int32Value(2)), + ) + ), + // LEFT OUTER JOIN -- RHS Empty + SuccessTestCase( + input = """ + SELECT VALUE [lhs, rhs] + FROM + << 0, 1, 2 >> lhs + LEFT OUTER JOIN ( + SELECT VALUE n + FROM << 0, 2, 3 >> AS n + WHERE n > 100 + ) rhs + ON lhs = rhs + """.trimIndent(), + expected = bagValue( + listValue(int32Value(0), int32Value(null)), + listValue(int32Value(1), int32Value(null)), + listValue(int32Value(2), int32Value(null)), + ) + ), + // LEFT OUTER JOIN -- LHS Empty + SuccessTestCase( + input = """ + SELECT VALUE [lhs, rhs] + FROM <<>> lhs + LEFT OUTER JOIN << 0, 2, 3>> rhs + ON lhs = rhs + """.trimIndent(), + expected = bagValue() + ), + // LEFT OUTER JOIN -- No Matches + SuccessTestCase( + input = """ + SELECT VALUE [lhs, rhs] + FROM << 0, 1, 2 >> lhs + LEFT OUTER JOIN << 3, 4, 5 >> rhs + ON lhs = rhs + """.trimIndent(), + expected = bagValue( + listValue(int32Value(0), int32Value(null)), + listValue(int32Value(1), int32Value(null)), + listValue(int32Value(2), int32Value(null)), + ) + ), + // RIGHT OUTER JOIN -- Easy + SuccessTestCase( + input = """ + SELECT VALUE [lhs, rhs] + FROM << 0, 1, 2 >> lhs + RIGHT OUTER JOIN << 0, 2, 3 >> rhs + ON lhs = rhs + """.trimIndent(), + expected = bagValue( + listValue(int32Value(0), int32Value(0)), + listValue(int32Value(2), int32Value(2)), + listValue(int32Value(null), int32Value(3)), + ) + ), + // RIGHT OUTER JOIN -- RHS Empty + SuccessTestCase( + input = """ + SELECT VALUE [lhs, rhs] + FROM << 0, 1, 2 >> lhs + RIGHT OUTER JOIN <<>> rhs + ON lhs = rhs + """.trimIndent(), + expected = bagValue() + ), + // RIGHT OUTER JOIN -- LHS Empty + SuccessTestCase( + input = """ + SELECT VALUE [lhs, rhs] + FROM ( + SELECT VALUE n + FROM << 0, 1, 2 >> AS n + WHERE n > 100 + ) lhs RIGHT OUTER JOIN + << 0, 2, 3>> rhs + ON lhs = rhs + """.trimIndent(), + expected = bagValue( + listValue(int32Value(null), int32Value(0)), + listValue(int32Value(null), int32Value(2)), + listValue(int32Value(null), int32Value(3)), + ) + ), + // RIGHT OUTER JOIN -- No Matches + SuccessTestCase( + input = """ + SELECT VALUE [lhs, rhs] + FROM << 0, 1, 2 >> lhs + RIGHT OUTER JOIN << 3, 4, 5 >> rhs + ON lhs = rhs + """.trimIndent(), + expected = bagValue( + listValue(int32Value(null), int32Value(3)), + listValue(int32Value(null), int32Value(4)), + listValue(int32Value(null), int32Value(5)), + ) + ), + ) + @JvmStatic fun subqueryTestCases() = listOf( SuccessTestCase( @@ -517,7 +639,8 @@ class PartiQLEngineDefaultTest { ), SuccessTestCase( input = "SELECT t.a, s.b FROM << { 'a': 1 } >> t LEFT JOIN << { 'b': 2 } >> s ON false;", - expected = bagValue(structValue("a" to int32Value(1), "b" to nullValue())) + expected = bagValue(structValue("a" to int32Value(1), "b" to nullValue())), + mode = PartiQLEngine.Mode.STRICT ), SuccessTestCase( input = "SELECT t.a, s.b FROM << { 'a': 1 } >> t FULL OUTER JOIN << { 'b': 2 } >> s ON false;", diff --git a/partiql-plan/src/main/resources/partiql_plan.ion b/partiql-plan/src/main/resources/partiql_plan.ion index 675d32de7d..801234b5b8 100644 --- a/partiql-plan/src/main/resources/partiql_plan.ion +++ b/partiql-plan/src/main/resources/partiql_plan.ion @@ -300,6 +300,8 @@ rel::{ projections: list::[rex], }, + // TODO: Specify that this is a LATERAL JOIN. Create a separate JOIN. Also, determine the allowable types of JOIN. + // For context: Oracle SQL doesn't allow ... FULL OUTER JOIN LATERAL ... or ... RIGHT OUTER JOIN LATERAL ... join::{ lhs: rel, rhs: rel, From 45979a9287e3611d70e59c570871f99727023750 Mon Sep 17 00:00:00 2001 From: John Ed Quinn Date: Fri, 12 Jul 2024 12:41:06 -0700 Subject: [PATCH 2/3] Updates KDocs and adds lateral tests --- .../internal/operator/rel/RelJoinInner.kt | 6 +++ .../internal/operator/rel/RelJoinOuterFull.kt | 7 +++ .../internal/operator/rel/RelJoinOuterLeft.kt | 7 +++ .../operator/rel/RelJoinOuterRight.kt | 6 +++ .../eval/internal/PartiQLEngineDefaultTest.kt | 29 +++++++++++- .../planner/internal/typer/PlanTyper.kt | 5 ++- .../internal/typer/PlanTyperTestsPorted.kt | 44 +++++++++++++++++++ test/partiql-tests | 2 +- 8 files changed, 103 insertions(+), 3 deletions(-) diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinInner.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinInner.kt index 64f8ff0891..cc8feae87f 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinInner.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinInner.kt @@ -6,6 +6,12 @@ import org.partiql.eval.internal.helpers.ValueUtility.isTrue import org.partiql.eval.internal.operator.Operator import org.partiql.value.PartiQLValueExperimental +/** + * Inner Join returns all joined records from the [lhs] and [rhs] when the [condition] evaluates to true. + * + * Note: This is currently the lateral version of the inner join. In the future, the two implementations + * (lateral vs non-lateral) may be separated for performance improvements. + */ internal class RelJoinInner( private val lhs: Operator.Relation, private val rhs: Operator.Relation, diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterFull.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterFull.kt index 6a5434efbf..a5b9c6a230 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterFull.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterFull.kt @@ -7,6 +7,13 @@ import org.partiql.eval.internal.operator.Operator import org.partiql.eval.value.Datum import org.partiql.plan.Rel +/** + * Full Outer Join returns all joined records from the [lhs] and [rhs] when the [condition] evaluates to true. For all + * records from the [lhs] that do not evaluate to true, these are also returned along with a NULL record from the [rhs]. + * For all records from the [rhs] that do not evaluate to true, these are also returned along with a NULL record from the [lhs]. + * + * Full Outer Join cannot be lateral according to PartiQL Specification Section 5.5. + */ internal class RelJoinOuterFull( private val lhs: Operator.Relation, private val rhs: Operator.Relation, diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterLeft.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterLeft.kt index 3124220bc9..6599b1d2d3 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterLeft.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterLeft.kt @@ -8,6 +8,13 @@ import org.partiql.eval.value.Datum import org.partiql.plan.Rel import org.partiql.value.PartiQLValueExperimental +/** + * Left Outer Join returns all joined records from the [lhs] and [rhs] when the [condition] evaluates to true. For all + * records from the [lhs] that do not evaluate to true, these are also returned along with a NULL record from the [rhs]. + * + * Note: This is currently the lateral version of the left outer join. In the future, the two implementations + * (lateral vs non-lateral) may be separated for performance improvements. + */ internal class RelJoinOuterLeft( private val lhs: Operator.Relation, private val rhs: Operator.Relation, diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterRight.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterRight.kt index a2913b0d43..71c667f3d0 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterRight.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/internal/operator/rel/RelJoinOuterRight.kt @@ -7,6 +7,12 @@ import org.partiql.eval.internal.operator.Operator import org.partiql.eval.value.Datum import org.partiql.plan.Rel +/** + * Right Outer Join returns all joined records from the [lhs] and [rhs] when the [condition] evaluates to true. For all + * records from the [rhs] that do not evaluate to true, these are also returned along with a NULL record from the [lhs]. + * + * Right Outer Join cannot be lateral according to PartiQL Specification Section 5.5. + */ internal class RelJoinOuterRight( private val lhs: Operator.Relation, private val rhs: Operator.Relation, 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 8255cd3066..48ad446478 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 @@ -9,7 +9,6 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.partiql.eval.PartiQLEngine import org.partiql.eval.PartiQLResult -import org.partiql.eval.internal.PartiQLEngineDefaultTest.SuccessTestCase.Global import org.partiql.parser.PartiQLParser import org.partiql.plan.PartiQLPlan import org.partiql.plan.debug.PlanPrinter @@ -275,6 +274,34 @@ class PartiQLEngineDefaultTest { listValue(int32Value(null), int32Value(5)), ) ), + // LEFT OUTER JOIN -- LATERAL + SuccessTestCase( + input = """ + SELECT VALUE rhs + FROM << [0, 1, 2], [10, 11, 12], [20, 21, 22] >> AS lhs + LEFT OUTER JOIN lhs AS rhs + ON lhs[2] = rhs + """.trimIndent(), + expected = bagValue( + int32Value(2), + int32Value(12), + int32Value(22), + ) + ), + // INNER JOIN -- LATERAL + SuccessTestCase( + input = """ + SELECT VALUE rhs + FROM << [0, 1, 2], [10, 11, 12], [20, 21, 22] >> AS lhs + INNER JOIN lhs AS rhs + ON lhs[2] = rhs + """.trimIndent(), + expected = bagValue( + int32Value(2), + int32Value(12), + int32Value(22), + ) + ), ) @JvmStatic 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 33383eeef3..50108818b5 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 @@ -436,7 +436,10 @@ internal class PlanTyper(private val env: Env) { override fun visitRelOpJoin(node: Rel.Op.Join, ctx: Rel.Type?): Rel { // Rewrite LHS and RHS val lhs = visitRel(node.lhs, ctx) - val stack = outer + listOf(TypeEnv(lhs.type.schema, outer)) + 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.FULL, Rel.Op.Join.Type.RIGHT -> outer + } val rhs = RelTyper(stack, Scope.GLOBAL).visitRel(node.rhs, ctx) // Calculate output schema given JOIN type diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/PlanTyperTestsPorted.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/PlanTyperTestsPorted.kt index 481a819cad..02889da30b 100644 --- a/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/PlanTyperTestsPorted.kt +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/internal/typer/PlanTyperTestsPorted.kt @@ -965,6 +965,50 @@ internal class PlanTyperTestsPorted { ProblemGenerator.undefinedVariable(insensitive("a"), setOf("t1", "t2")) ) ), + SuccessTestCase( + name = "LEFT JOIN (Lateral references)", + query = """ + SELECT VALUE rhs + FROM << [0, 1, 2], [10, 11, 12], [20, 21, 22] >> AS lhs + LEFT OUTER JOIN lhs AS rhs + ON lhs[2] = rhs + """.trimIndent(), + expected = BagType(INT4) + ), + SuccessTestCase( + name = "INNER JOIN (Lateral references)", + query = """ + SELECT VALUE rhs + FROM << [0, 1, 2], [10, 11, 12], [20, 21, 22] >> AS lhs + INNER JOIN lhs AS rhs + ON lhs[2] = rhs + """.trimIndent(), + expected = BagType(INT4) + ), + ErrorTestCase( + name = "RIGHT JOIN (Doesn't support lateral references)", + query = """ + SELECT VALUE rhs + FROM << [0, 1, 2], [10, 11, 12], [20, 21, 22] >> AS lhs + RIGHT OUTER JOIN lhs AS rhs + ON lhs[2] = rhs + """.trimIndent(), + problemHandler = assertProblemExists( + ProblemGenerator.undefinedVariable(insensitive("lhs"), setOf()) + ) + ), + ErrorTestCase( + name = "FULL JOIN (Doesn't support lateral references)", + query = """ + SELECT VALUE rhs + FROM << [0, 1, 2], [10, 11, 12], [20, 21, 22] >> AS lhs + FULL OUTER JOIN lhs AS rhs + ON lhs[2] = rhs + """.trimIndent(), + problemHandler = assertProblemExists( + ProblemGenerator.undefinedVariable(insensitive("lhs"), setOf()) + ) + ), ) @JvmStatic 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 1cd027290ce0a2a52f3e3b9588efceef71d5c0a5 Mon Sep 17 00:00:00 2001 From: John Ed Quinn Date: Fri, 12 Jul 2024 12:46:58 -0700 Subject: [PATCH 3/3] Utilizes the tests at head --- test/partiql-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/partiql-tests b/test/partiql-tests index c65b854e1d..be88ae732b 160000 --- a/test/partiql-tests +++ b/test/partiql-tests @@ -1 +1 @@ -Subproject commit c65b854e1dad88354af92fd018f306dca9a8a45a +Subproject commit be88ae732bec0388c88acab108a392f586094fc7