From 350834e826ad9adf650cb993d6e6e14cfe00edba Mon Sep 17 00:00:00 2001 From: John Ed Quinn Date: Fri, 3 Nov 2023 13:55:04 -0700 Subject: [PATCH] Disambiguates references to struct attributes --- .../org/partiql/planner/typer/PlanTyper.kt | 107 +++- .../partiql/planner/typer/PlanTyperTest.kt | 572 ++++++++++++++++++ .../resources/catalogs/default/b/b/d.ion | 2 +- .../pql/main/closed_duplicates_struct.ion | 20 + .../main/closed_ordered_duplicates_struct.ion | 21 + .../main/closed_union_duplicates_struct.ion | 43 ++ .../pql/main/open_duplicates_struct.ion | 17 + 7 files changed, 756 insertions(+), 26 deletions(-) create mode 100644 partiql-planner/src/test/kotlin/org/partiql/planner/typer/PlanTyperTest.kt create mode 100644 partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_duplicates_struct.ion create mode 100644 partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_ordered_duplicates_struct.ion create mode 100644 partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_union_duplicates_struct.ion create mode 100644 partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/open_duplicates_struct.ion diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/typer/PlanTyper.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/typer/PlanTyper.kt index be04079cc4..510cc25218 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/typer/PlanTyper.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/typer/PlanTyper.kt @@ -470,8 +470,14 @@ internal class PlanTyper( // } else it // } - // 3. Walk the steps to determine the path type. - val type = steps.fold(root.type) { curr, step -> inferPathStep(curr, step) } + // 3. Walk the steps, determine the path type, and replace each step with the disambiguated equivalent + // (AKA made sensitive, if possible) + var type = root.type + val newSteps = steps.map { step -> + val (stepType, replacementStep) = inferPathStep(type, step) + type = stepType + replacementStep + } // 4. Invalid path reference; always MISSING if (type == StaticType.MISSING) { @@ -480,7 +486,7 @@ internal class PlanTyper( } // 5. Non-missing, root is resolved - return rex(type, rexOpPath(root, steps)) + return rex(type, rexOpPath(root, newSteps)) } /** @@ -944,28 +950,42 @@ internal class PlanTyper( // Helpers - private fun inferPathStep(type: StaticType, step: Rex.Op.Path.Step): StaticType = when (type) { - is AnyType -> StaticType.ANY + /** + * @return a [Pair] where the [Pair.first] represents the type of the [step] and the [Pair.second] represents + * the disambiguated [step]. + */ + private fun inferPathStep(type: StaticType, step: Rex.Op.Path.Step): Pair = when (type) { + is AnyType -> StaticType.ANY to step is StructType -> inferPathStep(type, step) - is ListType, is SexpType -> inferPathStep(type as CollectionType, step) + is ListType, is SexpType -> inferPathStep(type as CollectionType, step) to step is AnyOfType -> { when (type.types.size) { 0 -> throw IllegalStateException("Cannot path on an empty StaticType union") else -> { val prevTypes = type.allTypes if (prevTypes.any { it is AnyType }) { - StaticType.ANY + StaticType.ANY to step } else { - val staticTypes = prevTypes.map { inferPathStep(it, step) } - AnyOfType(staticTypes.toSet()).flatten() + val results = prevTypes.map { inferPathStep(it, step) } + val types = results.map { it.first } + val firstResultStep = results.first().second + val replacementStep = when (results.map { it.second }.all { it == firstResultStep }) { + true -> firstResultStep + false -> step + } + AnyOfType(types.toSet()).flatten() to replacementStep } } } } - else -> StaticType.MISSING + else -> StaticType.MISSING to step } - private fun inferPathStep(struct: StructType, step: Rex.Op.Path.Step): StaticType = when (step) { + /** + * @return a [Pair] where the [Pair.first] represents the type of the [step] and the [Pair.second] represents + * the disambiguated [step]. + */ + private fun inferPathStep(struct: StructType, step: Rex.Op.Path.Step): Pair = when (step) { is Rex.Op.Path.Step.Index -> { if (step.key.type !is StringType) { error("Expected string but found: ${step.key.type}") @@ -974,17 +994,21 @@ internal class PlanTyper( val lit = (step.key.op as Rex.Op.Lit).value if (lit is TextValue<*> && !lit.isNull) { val id = identifierSymbol(lit.string!!, Identifier.CaseSensitivity.SENSITIVE) - inferStructLookup(struct, id) + val (type, replacementId) = inferStructLookup(struct, id) + type to rexOpPathStepSymbol(replacementId) } else { error("Expected text literal, but got $lit") } } else { // cannot infer type of non-literal path step because we don't know its value // we might improve upon this with some constant folding prior to typing - StaticType.ANY + StaticType.ANY to step } } - is Rex.Op.Path.Step.Symbol -> inferStructLookup(struct, step.identifier) + is Rex.Op.Path.Step.Symbol -> { + val (type, replacementId) = inferStructLookup(struct, step.identifier) + type to rexOpPathStepSymbol(replacementId) + } is Rex.Op.Path.Step.Unpivot -> error("Unpivot not supported") is Rex.Op.Path.Step.Wildcard -> error("Wildcard not supported") } @@ -999,22 +1023,55 @@ internal class PlanTyper( return collection.elementType } - private fun inferStructLookup(struct: StructType, key: Identifier.Symbol): StaticType { + /** + * Logic is as follows: + * 1. If [struct] is closed and ordered: + * - If no item is found, return [MissingType] + * - Else, grab first matching item and make sensitive. + * 2. If [struct] is closed + * - AND no item is found, return [MissingType] + * - AND only one item is present -> grab item and make sensitive. + * - AND more than one item is present, keep sensitivity and grab item. + * 3. If [struct] is open, return [AnyType] + * + * @return a [Pair] where the [Pair.first] represents the type of the [step] and the [Pair.second] represents + * the disambiguated [key]. + */ + private fun inferStructLookup(struct: StructType, key: Identifier.Symbol): Pair { val binding = key.toBindingName() - val type = when (struct.constraints.contains(TupleConstraint.Ordered)) { - true -> struct.fields.firstOrNull { entry -> binding.isEquivalentTo(entry.key) }?.value - false -> struct.fields.mapNotNull { entry -> - entry.value.takeIf { binding.isEquivalentTo(entry.key) } - }.let { valueTypes -> - StaticType.unionOf(valueTypes.toSet()).flatten().takeIf { valueTypes.isNotEmpty() } + val isClosed = struct.constraints.contains(TupleConstraint.Open(false)) + val isOrdered = struct.constraints.contains(TupleConstraint.Ordered) + val (name, type) = when { + // 1. Struct is closed and ordered + isClosed && isOrdered -> { + struct.fields.firstOrNull { entry -> binding.isEquivalentTo(entry.key) }?.let { + (sensitive(it.key) to it.value) + } ?: (key to StaticType.MISSING) } + // 2. Struct is closed + isClosed -> { + val matches = struct.fields.filter { entry -> binding.isEquivalentTo(entry.key) } + when (matches.size) { + 0 -> (key to StaticType.MISSING) + 1 -> matches.first().let { (sensitive(it.key) to it.value) } + else -> { + val firstKey = matches.first().key + val sharedKey = when (matches.all { it.key == firstKey }) { + true -> sensitive(firstKey) + false -> key + } + sharedKey to StaticType.unionOf(matches.map { it.value }.toSet()).flatten() + } + } + } + // 3. Struct is open + else -> (key to StaticType.ANY) } - return type ?: when (struct.contentClosed) { - true -> StaticType.MISSING - false -> StaticType.ANY - } + return type to name } + private fun sensitive(str: String): Identifier.Symbol = identifierSymbol(str, Identifier.CaseSensitivity.SENSITIVE) + /** * Resolution and typing of aggregation function calls. * diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/PlanTyperTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/PlanTyperTest.kt new file mode 100644 index 0000000000..6dce7d5f18 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/PlanTyperTest.kt @@ -0,0 +1,572 @@ +package org.partiql.planner.typer + +import com.amazon.ionelement.api.field +import com.amazon.ionelement.api.ionString +import com.amazon.ionelement.api.ionStructOf +import org.junit.jupiter.api.Test +import org.partiql.errors.Problem +import org.partiql.errors.ProblemCallback +import org.partiql.errors.ProblemHandler +import org.partiql.errors.ProblemSeverity +import org.partiql.plan.Identifier +import org.partiql.plan.Rex +import org.partiql.plan.identifierSymbol +import org.partiql.plan.rex +import org.partiql.plan.rexOpGlobal +import org.partiql.plan.rexOpLit +import org.partiql.plan.rexOpPath +import org.partiql.plan.rexOpPathStepSymbol +import org.partiql.plan.rexOpStruct +import org.partiql.plan.rexOpStructField +import org.partiql.plan.rexOpVarUnresolved +import org.partiql.plan.statementQuery +import org.partiql.planner.Env +import org.partiql.planner.PartiQLHeader +import org.partiql.planner.PartiQLPlanner +import org.partiql.plugins.local.LocalPlugin +import org.partiql.types.StaticType +import org.partiql.types.StructType +import org.partiql.types.TupleConstraint +import org.partiql.value.PartiQLValueExperimental +import org.partiql.value.int32Value +import org.partiql.value.stringValue +import java.util.Random +import kotlin.io.path.pathString +import kotlin.io.path.toPath +import kotlin.test.assertEquals + +class PlanTyperTest { + + companion object { + private val root = this::class.java.getResource("/catalogs/default")!!.toURI().toPath().pathString + + private val catalogConfig = mapOf( + "pql" to ionStructOf( + field("connector_name", ionString("local")), + field("root", ionString("$root/pql")), + ) + ) + + private val ORDERED_DUPLICATES_STRUCT = StructType( + fields = listOf( + StructType.Field("definition", StaticType.STRING), + StructType.Field("definition", StaticType.FLOAT), + StructType.Field("DEFINITION", StaticType.DECIMAL), + ), + contentClosed = true, + constraints = setOf( + TupleConstraint.Open(false), + TupleConstraint.Ordered + ) + ) + + private val DUPLICATES_STRUCT = StructType( + fields = listOf( + StructType.Field("definition", StaticType.STRING), + StructType.Field("definition", StaticType.FLOAT), + StructType.Field("DEFINITION", StaticType.DECIMAL), + ), + contentClosed = true, + constraints = setOf( + TupleConstraint.Open(false) + ) + ) + + private val CLOSED_UNION_DUPLICATES_STRUCT = StaticType.unionOf( + StructType( + fields = listOf( + StructType.Field("definition", StaticType.STRING), + StructType.Field("definition", StaticType.FLOAT), + StructType.Field("DEFINITION", StaticType.DECIMAL), + ), + contentClosed = true, + constraints = setOf( + TupleConstraint.Open(false) + ) + ), + StructType( + fields = listOf( + StructType.Field("definition", StaticType.INT2), + StructType.Field("definition", StaticType.INT4), + StructType.Field("DEFINITION", StaticType.INT8), + ), + contentClosed = true, + constraints = setOf( + TupleConstraint.Open(false), + TupleConstraint.Ordered + ) + ), + ) + + private val OPEN_DUPLICATES_STRUCT = StructType( + fields = listOf( + StructType.Field("definition", StaticType.STRING), + StructType.Field("definition", StaticType.FLOAT), + StructType.Field("DEFINITION", StaticType.DECIMAL), + ), + contentClosed = false + ) + + private fun getTyper(): PlanTyperWrapper { + val collector = ProblemCollector() + val env = Env( + listOf(PartiQLHeader), + listOf(LocalPlugin()), + PartiQLPlanner.Session( + queryId = Random().nextInt().toString(), + userId = "test-user", + currentCatalog = "pql", + currentDirectory = listOf("main"), + catalogConfig = catalogConfig + ) + ) + return PlanTyperWrapper(PlanTyper(env, collector), collector) + } + } + + private class PlanTyperWrapper( + internal val typer: PlanTyper, + internal val collector: ProblemCollector + ) + + /** + * This is a test to show that we convert: + * ``` + * { 'FiRsT_KeY': { 'sEcoNd_KEY': 5 } }.first_key."sEcoNd_KEY" + * ``` + * to + * ``` + * { 'FiRsT_KeY': { 'sEcoNd_KEY': 5 } }."FiRsT_KeY"."sEcoNd_KEY" + * ``` + * + * It also checks that we type it all correctly as well. + */ + @Test + @OptIn(PartiQLValueExperimental::class) + fun testReplacingStructs() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpStruct( + fields = listOf( + rexOpStructField( + k = rex(StaticType.STRING, rexOpLit(stringValue("FiRsT_KeY"))), + v = rex( + StaticType.ANY, + rexOpStruct( + fields = listOf( + rexOpStructField( + k = rex(StaticType.STRING, rexOpLit(stringValue("sEcoNd_KEY"))), + v = rex(StaticType.INT4, rexOpLit(int32Value(5))) + ) + ) + ) + ) + ) + ) + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("first_key", Identifier.CaseSensitivity.INSENSITIVE)), + rexOpPathStepSymbol(identifierSymbol("sEcoNd_KEY", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val firstKeyStruct = StructType( + fields = mapOf( + "sEcoNd_KEY" to StaticType.INT4 + ), + contentClosed = true, + constraints = setOf( + TupleConstraint.UniqueAttrs(true), + TupleConstraint.Open(false) + ) + ) + val topLevelStruct = StructType( + fields = mapOf( + "FiRsT_KeY" to firstKeyStruct + ), + contentClosed = true, + constraints = setOf( + TupleConstraint.UniqueAttrs(true), + TupleConstraint.Open(false) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.INT4, + op = rexOpPath( + root = rex( + type = topLevelStruct, + rexOpStruct( + fields = listOf( + rexOpStructField( + k = rex(StaticType.STRING, rexOpLit(stringValue("FiRsT_KeY"))), + v = rex( + type = firstKeyStruct, + rexOpStruct( + fields = listOf( + rexOpStructField( + k = rex(StaticType.STRING, rexOpLit(stringValue("sEcoNd_KEY"))), + v = rex(StaticType.INT4, rexOpLit(int32Value(5))) + ) + ) + ) + ) + ) + ) + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("FiRsT_KeY", Identifier.CaseSensitivity.SENSITIVE)), + rexOpPathStepSymbol(identifierSymbol("sEcoNd_KEY", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + @Test + fun testOrderedDuplicates() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpVarUnresolved( + identifierSymbol("closed_ordered_duplicates_struct", Identifier.CaseSensitivity.SENSITIVE), + Rex.Op.Var.Scope.DEFAULT + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("DEFINITION", Identifier.CaseSensitivity.INSENSITIVE)), + ) + ) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.STRING, + op = rexOpPath( + root = rex( + ORDERED_DUPLICATES_STRUCT, + rexOpGlobal(0) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + @Test + fun testOrderedDuplicatesWithSensitivity() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpVarUnresolved( + identifierSymbol("closed_ordered_duplicates_struct", Identifier.CaseSensitivity.SENSITIVE), + Rex.Op.Var.Scope.DEFAULT + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("DEFINITION", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.DECIMAL, + op = rexOpPath( + root = rex( + ORDERED_DUPLICATES_STRUCT, + rexOpGlobal(0) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("DEFINITION", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + @Test + fun testUnorderedDuplicates() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpVarUnresolved( + identifierSymbol("closed_duplicates_struct", Identifier.CaseSensitivity.SENSITIVE), + Rex.Op.Var.Scope.DEFAULT + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("DEFINITION", Identifier.CaseSensitivity.INSENSITIVE)), + ) + ) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.unionOf(StaticType.STRING, StaticType.FLOAT, StaticType.DECIMAL), + op = rexOpPath( + root = rex( + DUPLICATES_STRUCT, + rexOpGlobal(0) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("DEFINITION", Identifier.CaseSensitivity.INSENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + @Test + fun testUnorderedDuplicatesWithSensitivity() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpVarUnresolved( + identifierSymbol("closed_duplicates_struct", Identifier.CaseSensitivity.SENSITIVE), + Rex.Op.Var.Scope.DEFAULT + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("DEFINITION", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.DECIMAL, + op = rexOpPath( + root = rex( + DUPLICATES_STRUCT, + rexOpGlobal(0) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("DEFINITION", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + @Test + fun testUnorderedDuplicatesWithSensitivityAndDuplicateResults() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpVarUnresolved( + identifierSymbol("closed_duplicates_struct", Identifier.CaseSensitivity.SENSITIVE), + Rex.Op.Var.Scope.DEFAULT + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.unionOf(StaticType.STRING, StaticType.FLOAT), + op = rexOpPath( + root = rex( + DUPLICATES_STRUCT, + rexOpGlobal(0) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + @Test + fun testOpenDuplicates() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpVarUnresolved( + identifierSymbol("open_duplicates_struct", Identifier.CaseSensitivity.SENSITIVE), + Rex.Op.Var.Scope.DEFAULT + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + OPEN_DUPLICATES_STRUCT, + rexOpGlobal(0) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + @Test + fun testUnionClosedDuplicates() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpVarUnresolved( + identifierSymbol("closed_union_duplicates_struct", Identifier.CaseSensitivity.SENSITIVE), + Rex.Op.Var.Scope.DEFAULT + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.INSENSITIVE)), + ) + ) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.unionOf(StaticType.STRING, StaticType.FLOAT, StaticType.DECIMAL, StaticType.INT2), + op = rexOpPath( + root = rex( + CLOSED_UNION_DUPLICATES_STRUCT, + rexOpGlobal(0) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.INSENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + @Test + fun testUnionClosedDuplicatesWithSensitivity() { + val wrapper = getTyper() + val typer = wrapper.typer + val input = statementQuery( + root = rex( + type = StaticType.ANY, + op = rexOpPath( + root = rex( + StaticType.ANY, + rexOpVarUnresolved( + identifierSymbol("closed_union_duplicates_struct", Identifier.CaseSensitivity.SENSITIVE), + Rex.Op.Var.Scope.DEFAULT + ) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val expected = statementQuery( + root = rex( + type = StaticType.unionOf(StaticType.STRING, StaticType.FLOAT, StaticType.INT2), + op = rexOpPath( + root = rex( + CLOSED_UNION_DUPLICATES_STRUCT, + rexOpGlobal(0) + ), + steps = listOf( + rexOpPathStepSymbol(identifierSymbol("definition", Identifier.CaseSensitivity.SENSITIVE)), + ) + ) + ) + ) + val actual = typer.resolve(input) + assertEquals(expected, actual) + } + + /** + * A [ProblemHandler] that collects all the encountered [Problem]s without throwing. + * + * This is intended to be used when wanting to collect multiple problems that may be encountered (e.g. a static type + * inference pass that can result in multiple errors and/or warnings). This handler does not collect other exceptions + * that may be thrown. + */ + internal class ProblemCollector : ProblemCallback { + private val problemList = mutableListOf() + + val problems: List + get() = problemList + + val hasErrors: Boolean + get() = problemList.any { it.details.severity == ProblemSeverity.ERROR } + + val hasWarnings: Boolean + get() = problemList.any { it.details.severity == ProblemSeverity.WARNING } + + override fun invoke(problem: Problem) { + problemList.add(problem) + } + } +} diff --git a/partiql-planner/src/testFixtures/resources/catalogs/default/b/b/d.ion b/partiql-planner/src/testFixtures/resources/catalogs/default/b/b/d.ion index cfa121e146..46f56fef39 100644 --- a/partiql-planner/src/testFixtures/resources/catalogs/default/b/b/d.ion +++ b/partiql-planner/src/testFixtures/resources/catalogs/default/b/b/d.ion @@ -1,6 +1,6 @@ { type: "struct", - constraints: [ ordered ], + constraints: [ closed, ordered ], fields: [ { name: "e", diff --git a/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_duplicates_struct.ion b/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_duplicates_struct.ion new file mode 100644 index 0000000000..3344d2c07d --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_duplicates_struct.ion @@ -0,0 +1,20 @@ +{ + type: "struct", + constraints: [ + closed + ], + fields: [ + { + name: "definition", + type: "string", + }, + { + name: "definition", + type: "float32", + }, + { + name: "DEFINITION", + type: "decimal" + } + ] +} diff --git a/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_ordered_duplicates_struct.ion b/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_ordered_duplicates_struct.ion new file mode 100644 index 0000000000..f06fa335b8 --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_ordered_duplicates_struct.ion @@ -0,0 +1,21 @@ +{ + type: "struct", + constraints: [ + closed, + ordered + ], + fields: [ + { + name: "definition", + type: "string", + }, + { + name: "definition", + type: "float32", + }, + { + name: "DEFINITION", + type: "decimal" + } + ] +} diff --git a/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_union_duplicates_struct.ion b/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_union_duplicates_struct.ion new file mode 100644 index 0000000000..0c63d9b5bb --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/closed_union_duplicates_struct.ion @@ -0,0 +1,43 @@ +[ + { + type: "struct", + constraints: [ + closed + ], + fields: [ + { + name: "definition", + type: "string", + }, + { + name: "definition", + type: "float32", + }, + { + name: "DEFINITION", + type: "decimal" + } + ] + }, + { + type: "struct", + constraints: [ + closed, + ordered + ], + fields: [ + { + name: "definition", + type: "int16", + }, + { + name: "definition", + type: "int32", + }, + { + name: "DEFINITION", + type: "int64" + } + ] + }, +] diff --git a/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/open_duplicates_struct.ion b/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/open_duplicates_struct.ion new file mode 100644 index 0000000000..1335da6a10 --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/catalogs/default/pql/main/open_duplicates_struct.ion @@ -0,0 +1,17 @@ +{ + type: "struct", + fields: [ + { + name: "definition", + type: "string", + }, + { + name: "definition", + type: "float32", + }, + { + name: "DEFINITION", + type: "decimal" + } + ] +}