diff --git a/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt b/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt index 22a2bdd334..337f98cf49 100644 --- a/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt +++ b/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt @@ -2052,6 +2052,7 @@ internal class PartiQLParserDefault : PartiQLParser { val n = ctx.arg0?.text?.toInt() when (ctx.datatype.type) { GeneratedParser.FLOAT -> when (n) { + null -> typeFloat64() 32 -> typeFloat32() 64 -> typeFloat64() else -> throw error(ctx.datatype, "Invalid FLOAT precision. Expected 32 or 64") diff --git a/partiql-planner/build.gradle.kts b/partiql-planner/build.gradle.kts index ddf58d2f93..27c29b9080 100644 --- a/partiql-planner/build.gradle.kts +++ b/partiql-planner/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { // Test testImplementation(project(":partiql-parser")) testImplementation(project(":plugins:partiql-local")) + testImplementation(project(":plugins:partiql-memory")) // Test Fixtures testFixturesImplementation(project(":partiql-spi")) } diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/PartiQLHeader.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/PartiQLHeader.kt index 5cc103cf1e..41132fbf13 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/PartiQLHeader.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/PartiQLHeader.kt @@ -233,28 +233,31 @@ object PartiQLHeader : Header() { // SPECIAL FORMS - private fun like(): List = listOf( - FunctionSignature.Scalar( - name = "like", - returns = BOOL, - parameters = listOf( - FunctionParameter("value", STRING), - FunctionParameter("pattern", STRING), + private fun like(): List = types.text.flatMap { t -> + listOf( + FunctionSignature.Scalar( + name = "like", + returns = BOOL, + parameters = listOf( + FunctionParameter("value", t), + FunctionParameter("pattern", t), + ), + isNullCall = true, + isNullable = false, ), - isNullCall = true, - isNullable = false, - ), - FunctionSignature.Scalar( - name = "like_escape", - returns = BOOL, - parameters = listOf( - FunctionParameter("value", STRING), - FunctionParameter("pattern", STRING), - FunctionParameter("escape", STRING), + FunctionSignature.Scalar( + name = "like_escape", + returns = BOOL, + parameters = listOf( + FunctionParameter("value", t), + FunctionParameter("pattern", t), + FunctionParameter("escape", t), + ), + isNullCall = true, + isNullable = false, ), - isNullCall = true, - isNullable = false, - ), + ) + } + listOf( FunctionSignature.Scalar( name = "like", returns = BOOL, diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/typer/FnResolver.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/typer/FnResolver.kt index b73d8afb0f..0bd0986e2e 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/typer/FnResolver.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/typer/FnResolver.kt @@ -224,8 +224,14 @@ internal class FnResolver(private val headers: List
) { } } } - // we made a match - return mapping + // if all elements requires casting, then no match + // because there must be another function definition that requires no casting + return if (mapping.contains(null)) { + // we made a match + mapping + } else { + null + } } /** 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 9962fa5712..24007c8a30 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 @@ -84,6 +84,7 @@ import org.partiql.types.TupleConstraint import org.partiql.types.function.FunctionSignature import org.partiql.value.BoolValue import org.partiql.value.PartiQLValueExperimental +import org.partiql.value.PartiQLValueType import org.partiql.value.TextValue import org.partiql.value.boolValue @@ -510,6 +511,7 @@ internal class PlanTyper( // Type the arguments val fn = node.fn as Fn.Unresolved val isEq = fn.isEq() + val isTypeAssertion = fn.isTypeAssertion() var missingArg = false val args = node.args.map { val arg = visitRex(it, null) @@ -518,7 +520,7 @@ internal class PlanTyper( } // 7.1 All functions return MISSING when one of their inputs is MISSING (except `=`) - if (missingArg && !isEq) { + if (missingArg && !isEq && !isTypeAssertion) { handleAlwaysMissing() return rex(StaticType.MISSING, rexOpCall(fn, args)) } @@ -532,6 +534,13 @@ internal class PlanTyper( val newArgs = rewriteFnArgs(match.mapping, args) val returns = newFn.signature.returns + // dynamic function resolution should only be called upon the arg type is any type or any of type + if (newFn.signature.parameters.all { it.type == PartiQLValueType.ANY } && newFn.signature.isMissable) { + if (!newArgs.map { it.type }.any { it is AnyType || it is AnyOfType }) { + handleAlwaysMissing() + return rex(StaticType.MISSING, rexOpCall(fn, args)) + } + } // Determine the nullability of the return type var isNull = false // True iff NULL CALL and has a NULL arg var isNullable = false // True iff NULL CALL and has a NULLABLE arg; or is a NULLABLE operator @@ -557,7 +566,7 @@ internal class PlanTyper( } // Some operators can return MISSING during runtime - if (match.isMissable && !isEq) { + if (match.isMissable && !isEq && !isTypeAssertion) { type = StaticType.unionOf(type, StaticType.MISSING) } @@ -1348,6 +1357,10 @@ internal class PlanTyper( return (identifier is Identifier.Symbol && (identifier as Identifier.Symbol).symbol == "eq") } + private fun Fn.Unresolved.isTypeAssertion(): Boolean { + return (identifier is Identifier.Symbol && (identifier as Identifier.Symbol).symbol.startsWith("is")) + } + /** * This will make all binding values nullables. If the value is a struct, each field will be nullable. * diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/typer/TypeLattice.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/typer/TypeLattice.kt index 63af484732..da7b6f3c45 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/typer/TypeLattice.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/typer/TypeLattice.kt @@ -181,17 +181,17 @@ internal class TypeLattice private constructor( ) graph[BOOL] = relationships( BOOL to coercion(), - INT8 to coercion(), - INT16 to coercion(), - INT32 to coercion(), - INT64 to coercion(), - INT to coercion(), - DECIMAL to coercion(), - FLOAT32 to coercion(), - FLOAT64 to coercion(), - CHAR to coercion(), - STRING to coercion(), - SYMBOL to coercion(), + INT8 to explicit(), + INT16 to explicit(), + INT32 to explicit(), + INT64 to explicit(), + INT to explicit(), + DECIMAL to explicit(), + FLOAT32 to explicit(), + FLOAT64 to explicit(), + CHAR to explicit(), + STRING to explicit(), + SYMBOL to explicit(), ) graph[INT8] = relationships( BOOL to explicit(), diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/HeaderTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/HeaderTest.kt index 3dad436cae..4fd0d79ef1 100644 --- a/partiql-planner/src/test/kotlin/org/partiql/planner/HeaderTest.kt +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/HeaderTest.kt @@ -1,13 +1,12 @@ package org.partiql.planner -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test class HeaderTest { @Test - @Disabled +// @Disabled fun print() { - println(PartiQLHeader) + println(PartiQLHeader.toString()) } } diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/PartiQLTyperTestBase.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/PartiQLTyperTestBase.kt new file mode 100644 index 0000000000..51efc83f0f --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/PartiQLTyperTestBase.kt @@ -0,0 +1,124 @@ +package org.partiql.planner.typer + +import com.amazon.ionelement.api.ionString +import com.amazon.ionelement.api.ionStructOf +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.DynamicTest +import org.partiql.errors.Problem +import org.partiql.errors.ProblemCallback +import org.partiql.errors.ProblemSeverity +import org.partiql.parser.PartiQLParserBuilder +import org.partiql.plan.Statement +import org.partiql.planner.PartiQLPlanner +import org.partiql.planner.PartiQLPlannerBuilder +import org.partiql.planner.test.PartiQLTest +import org.partiql.planner.test.PartiQLTestProvider +import org.partiql.plugins.memory.MemoryCatalog +import org.partiql.plugins.memory.MemoryPlugin +import org.partiql.types.StaticType +import java.util.Random +import java.util.stream.Stream + +abstract class PartiQLTyperTestBase { + sealed class TestResult { + data class Success(val expectedType: StaticType) : TestResult() { + override fun toString(): String = "Success_$expectedType" + } + + object Failure : TestResult() { + override fun toString(): String = "Failure" + } + } + + 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) + } + } + + companion object { + internal val session: ((String) -> PartiQLPlanner.Session) = { catalog -> + PartiQLPlanner.Session( + queryId = Random().nextInt().toString(), + userId = "test-user", + currentCatalog = catalog, + catalogConfig = mapOf( + catalog to ionStructOf( + "connector_name" to ionString("memory") + ) + ) + ) + } + } + + val inputs = PartiQLTestProvider().apply { load() } + + val testingPipeline: ((String, String, MemoryCatalog.Provider, ProblemCallback) -> PartiQLPlanner.Result) = { query, catalog, catalogProvider, collector -> + val ast = PartiQLParserBuilder.standard().build().parse(query).root + val planner = PartiQLPlannerBuilder().plugins(listOf(MemoryPlugin(catalogProvider))).build() + planner.plan(ast, session(catalog), collector) + } + + fun testGen( + testCategory: String, + tests: List, + argsMap: Map>>, + ): Stream { + val catalogProvider = MemoryCatalog.Provider() + + return tests.map { test -> + val group = test.statement + val children = argsMap.flatMap { (key, value) -> + value.mapIndexed { index: Int, types: List -> + val testName = "${testCategory}_${key}_$index" + catalogProvider[testName] = MemoryCatalog.of( + *( + types.mapIndexed { i, t -> + "t${i + 1}" to t + }.toTypedArray() + ) + ) + val displayName = "$group | $testName | $types" + val statement = test.statement + // Assert + DynamicTest.dynamicTest(displayName) { + val pc = ProblemCollector() + if (key is TestResult.Success) { + val result = testingPipeline(statement, testName, catalogProvider, pc) + val root = (result.plan.statement as Statement.Query).root + val actualType = root.type + assert(actualType == key.expectedType) { + """ + expected Type is : ${key.expectedType} + actual Type is : $actualType + """.trimIndent() + } + } else { + val result = testingPipeline(statement, testName, catalogProvider, pc) + val root = (result.plan.statement as Statement.Query).root + val actualType = root.type + assert(actualType == StaticType.MISSING) { + """ + expected Type is : missing + actual Type is : $actualType + """.trimIndent() + } + } + } + } + } + DynamicContainer.dynamicContainer(group, children) + }.stream() + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpArithmeticTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpArithmeticTest.kt new file mode 100644 index 0000000000..22f6c6b133 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpArithmeticTest.kt @@ -0,0 +1,67 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.CastType +import org.partiql.planner.util.allNumberType +import org.partiql.planner.util.allSupportedType +import org.partiql.planner.util.cartesianProduct +import org.partiql.planner.util.castTable +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpArithmeticTest : PartiQLTyperTestBase() { + @TestFactory + fun arithmetic(): Stream { + val tests = listOf( + "expr-61", + "expr-62", + "expr-63", + "expr-64", + "expr-65", + ).map { inputs.get("basics", it)!! } + + val argsMap: Map>> = buildMap { + val successArgs = (allNumberType + listOf(StaticType.NULL, StaticType.MISSING)) + .let { cartesianProduct(it, it) } + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + val arg0 = args.first() + val arg1 = args[1] + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else if (arg0 == arg1) { + (this[TestResult.Success(arg1)] ?: setOf(args)).let { + put(TestResult.Success(arg1), it + setOf(args)) + } + } else if (castTable(arg1, arg0) == CastType.COERCION) { + (this[TestResult.Success(arg0)] ?: setOf(args)).let { + put(TestResult.Success(arg0), it + setOf(args)) + } + } else { + (this[TestResult.Success(arg1)] ?: setOf(args)).let { + put(TestResult.Success(arg1), it + setOf(args)) + } + } + Unit + } + + put(TestResult.Failure, failureArgs) + } + + return super.testGen("arithmetic", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpBetweenTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpBetweenTest.kt new file mode 100644 index 0000000000..1355347682 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpBetweenTest.kt @@ -0,0 +1,80 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.CastType +import org.partiql.planner.util.allNumberType +import org.partiql.planner.util.allSupportedType +import org.partiql.planner.util.cartesianProduct +import org.partiql.planner.util.castTable +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpBetweenTest : PartiQLTyperTestBase() { + @TestFactory + fun between(): Stream { + val tests = listOf( + "expr-57", + "expr-58", + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = + cartesianProduct( + allNumberType + listOf(StaticType.NULL, StaticType.MISSING), + allNumberType + listOf(StaticType.NULL, StaticType.MISSING), + allNumberType + listOf(StaticType.NULL, StaticType.MISSING), + ) + // TODO: Support comparing Datetime value using between +// + cartesianProduct( +// allDateTimeType + listOf(StaticType.NULL, StaticType.MISSING), +// allDateTimeType + listOf(StaticType.NULL, StaticType.MISSING), +// allDateTimeType + listOf(StaticType.NULL, StaticType.MISSING), +// ) + + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType, + allSupportedType + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + val arg0 = args.first() + val arg1 = args[1] + val arg2 = args[2] + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else if (arg0 == arg1 && arg0 == arg2) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else if (castTable(arg1, arg0) == CastType.COERCION && castTable(arg2, arg0) == CastType.COERCION) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else if (castTable(arg0, arg1) == CastType.COERCION && castTable(arg2, arg1) == CastType.COERCION) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, failureArgs) + } + + return super.testGen("between", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpBitwiseAndTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpBitwiseAndTest.kt new file mode 100644 index 0000000000..e788aa19e8 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpBitwiseAndTest.kt @@ -0,0 +1,62 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.CastType +import org.partiql.planner.util.allIntType +import org.partiql.planner.util.allSupportedType +import org.partiql.planner.util.cartesianProduct +import org.partiql.planner.util.castTable +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpBitwiseAndTest : PartiQLTyperTestBase() { + @TestFactory + fun bitwiseAnd(): Stream { + val tests = listOf( + "expr-60" + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = (allIntType + listOf(StaticType.NULL, StaticType.MISSING)) + .let { cartesianProduct(it, it) } + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + val arg0 = args.first() + val arg1 = args[1] + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else if (arg0 == arg1) { + (this[TestResult.Success(arg1)] ?: setOf(args)).let { + put(TestResult.Success(arg1), it + setOf(args)) + } + } else if (castTable(arg1, arg0) == CastType.COERCION) { + (this[TestResult.Success(arg0)] ?: setOf(args)).let { + put(TestResult.Success(arg0), it + setOf(args)) + } + } else { + (this[TestResult.Success(arg1)] ?: setOf(args)).let { + put(TestResult.Success(arg1), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, failureArgs) + } + + return super.testGen("bitwise_and", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpComparisonTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpComparisonTest.kt new file mode 100644 index 0000000000..80ea07bc79 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpComparisonTest.kt @@ -0,0 +1,78 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.CastType +import org.partiql.planner.util.allSupportedType +import org.partiql.planner.util.cartesianProduct +import org.partiql.planner.util.castTable +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpComparisonTest : PartiQLTyperTestBase() { + + @TestFactory + fun comparison(): Stream { + val tests = listOf( + "expr-03", // Less than TODO: Less than currently only support numeric type + "expr-04", // Less than or equal TODO: Less than or equal currently only support numeric type + "expr-05", // Bigger than TODO: Bigger than currently only support numeric type + "expr-06", // Bigger than or equal TODO: Bigger than or equal currently only support numeric type + "expr-07", // Equal + "expr-08", // Not Equal != + "expr-09", // Not Equal <> + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = + // t? op t? always success + allSupportedType.flatMap { t -> + cartesianProduct( + listOf(t, StaticType.NULL, StaticType.MISSING), + listOf(t, StaticType.NULL, StaticType.MISSING) + ) + }.toSet() + + cartesianProduct( + StaticType.NUMERIC.allTypes, + StaticType.NUMERIC.allTypes + StaticType.NULL_OR_MISSING.allTypes + ) + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType, + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + val arg0 = args.first() + val arg1 = args[1] + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else if (arg0 == arg1) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else if (castTable(arg1, arg0) == CastType.COERCION) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, failureArgs) + } + + return super.testGen("comparison", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpConcatTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpConcatTest.kt new file mode 100644 index 0000000000..54407fb395 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpConcatTest.kt @@ -0,0 +1,62 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.CastType +import org.partiql.planner.util.allSupportedType +import org.partiql.planner.util.allTextType +import org.partiql.planner.util.cartesianProduct +import org.partiql.planner.util.castTable +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpConcatTest : PartiQLTyperTestBase() { + @TestFactory + fun concat(): Stream { + val tests = listOf( + "expr-59" + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = (allTextType + listOf(StaticType.NULL, StaticType.MISSING)) + .let { cartesianProduct(it, it) } + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + val arg0 = args.first() + val arg1 = args[1] + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else if (arg0 == arg1) { + (this[TestResult.Success(arg1)] ?: setOf(args)).let { + put(TestResult.Success(arg1), it + setOf(args)) + } + } else if (castTable(arg1, arg0) == CastType.COERCION) { + (this[TestResult.Success(arg0)] ?: setOf(args)).let { + put(TestResult.Success(arg0), it + setOf(args)) + } + } else { + (this[TestResult.Success(arg1)] ?: setOf(args)).let { + put(TestResult.Success(arg1), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, failureArgs) + } + + return super.testGen("concat", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpInTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpInTest.kt new file mode 100644 index 0000000000..e297d17484 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpInTest.kt @@ -0,0 +1,89 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.allCollectionType +import org.partiql.planner.util.allSupportedType +import org.partiql.planner.util.cartesianProduct +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpInTest : PartiQLTyperTestBase() { + + @TestFactory + fun inSingleArg(): Stream { + val tests = listOf( + "expr-50", // IN ( true ) + "expr-51", // NOT IN ( true ) + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = + allSupportedType.map { t -> + listOf(t) + }.toSet() + + successArgs.forEach { args: List -> + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, emptySet>()) + } + + return super.testGen("in", tests, argsMap) + } + + @TestFactory + fun inDoubleArg(): Stream { + val tests = listOf( + "expr-52", // t1 IN t2 + "expr-53", // t1 NOT IN t2 + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = cartesianProduct( + allSupportedType, + (allCollectionType + listOf(StaticType.NULL, StaticType.MISSING)) + ) + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType, + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, failureArgs) + } + + return super.testGen("in", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpIsTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpIsTest.kt new file mode 100644 index 0000000000..c6cdb7dad6 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpIsTest.kt @@ -0,0 +1,29 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.allSupportedType +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpIsTest : PartiQLTyperTestBase() { + @TestFactory + fun isAssertion(): Stream { + val tests = buildList { + (10..49).forEach { + this.add("expr-$it") + } + }.map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = allSupportedType.flatMap { t -> + setOf(listOf(t)) + }.toSet() + put(TestResult.Success(StaticType.BOOL), successArgs) + put(TestResult.Failure, emptySet>()) + } + + return super.testGen("is", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpLikeTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpLikeTest.kt new file mode 100644 index 0000000000..c1a8df0d7d --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpLikeTest.kt @@ -0,0 +1,117 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.CastType +import org.partiql.planner.util.allSupportedType +import org.partiql.planner.util.allTextType +import org.partiql.planner.util.cartesianProduct +import org.partiql.planner.util.castTable +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpLikeTest : PartiQLTyperTestBase() { + @TestFactory + fun likeDoubleArg(): Stream { + val tests = listOf( + "expr-54", // t1 LIKE t2 + "expr-55", // t1 NOT LIKE t2 + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = (allTextType + listOf(StaticType.NULL, StaticType.MISSING)) + .let { cartesianProduct(it, it) } + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + val arg0 = args.first() + val arg1 = args[1] + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else if (arg0 == arg1) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else if (castTable(arg1, arg0) == CastType.COERCION) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, failureArgs) + } + + return super.testGen("like", tests, argsMap) + } + + @TestFactory + fun likeTripleArg(): Stream { + val tests = listOf( + "expr-56", // t1 NOT LIKE t2 ESCAPE t3 + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = (allTextType + listOf(StaticType.NULL, StaticType.MISSING)) + .let { cartesianProduct(it, it, it) } + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType, + allSupportedType, + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + val arg0 = args.first() + val arg1 = args[1] + val arg2 = args[2] + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else if (arg0 == arg1 && arg0 == arg2) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else if (castTable(arg1, arg0) == CastType.COERCION && castTable(arg2, arg0) == CastType.COERCION) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else if (castTable(arg0, arg1) == CastType.COERCION && castTable(arg2, arg1) == CastType.COERCION) { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } else { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, failureArgs) + } + + return super.testGen("like", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpLogicalTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpLogicalTest.kt new file mode 100644 index 0000000000..770374dfa7 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpLogicalTest.kt @@ -0,0 +1,68 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.CastType +import org.partiql.planner.util.allSupportedType +import org.partiql.planner.util.cartesianProduct +import org.partiql.planner.util.castTable +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpLogicalTest : PartiQLTyperTestBase() { + + @TestFactory + fun logical(): Stream { + val supportedType = listOf( + StaticType.BOOL, + StaticType.NULL, + StaticType.MISSING + ) + + val tests = listOf( + "expr-00", // OR + "expr-01", // AND + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = cartesianProduct(supportedType, supportedType) + val failureArgs = cartesianProduct( + allSupportedType, + allSupportedType + ).filterNot { + successArgs.contains(it) + }.toSet() + + successArgs.forEach { args: List -> + val arg0 = args.first() + val arg1 = args[1] + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else if (arg0 == arg1) { + (this[TestResult.Success(arg1)] ?: setOf(args)).let { + put(TestResult.Success(arg1), it + setOf(args)) + } + } else if (castTable(arg1, arg0) == CastType.COERCION) { + (this[TestResult.Success(arg0)] ?: setOf(args)).let { + put(TestResult.Success(arg0), it + setOf(args)) + } + } else { + (this[TestResult.Success(arg1)] ?: setOf(args)).let { + put(TestResult.Success(arg1), it + setOf(args)) + } + } + Unit + } + put(TestResult.Failure, failureArgs) + } + + return super.testGen("logical", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpNegateTest.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpNegateTest.kt new file mode 100644 index 0000000000..793aa185e4 --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/typer/operator/OpNegateTest.kt @@ -0,0 +1,51 @@ +package org.partiql.planner.typer.operator + +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.partiql.planner.typer.PartiQLTyperTestBase +import org.partiql.planner.util.allSupportedType +import org.partiql.types.StaticType +import java.util.stream.Stream + +class OpNegateTest : PartiQLTyperTestBase() { + @TestFactory + fun negate(): Stream { + val supportedType = listOf( + StaticType.BOOL, + StaticType.NULL, + StaticType.MISSING, + ) + + val unsupportedType = allSupportedType.filterNot { + supportedType.contains(it) + } + + val tests = listOf( + "expr-02", // Not + ).map { inputs.get("basics", it)!! } + + val argsMap = buildMap { + val successArgs = supportedType.map { t -> listOf(t) }.toSet() + successArgs.forEach { args: List -> + if (args.contains(StaticType.MISSING)) { + (this[TestResult.Success(StaticType.MISSING)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.MISSING), it + setOf(args)) + } + } else if (args.contains(StaticType.NULL)) { + (this[TestResult.Success(StaticType.NULL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.NULL), it + setOf(args)) + } + } else { + (this[TestResult.Success(StaticType.BOOL)] ?: setOf(args)).let { + put(TestResult.Success(StaticType.BOOL), it + setOf(args)) + } + } + Unit + } + + put(TestResult.Failure, unsupportedType.map { t -> listOf(t) }.toSet()) + } + + return super.testGen("negate", tests, argsMap) + } +} diff --git a/partiql-planner/src/test/kotlin/org/partiql/planner/util/Utils.kt b/partiql-planner/src/test/kotlin/org/partiql/planner/util/Utils.kt new file mode 100644 index 0000000000..a01d35c17e --- /dev/null +++ b/partiql-planner/src/test/kotlin/org/partiql/planner/util/Utils.kt @@ -0,0 +1,181 @@ +package org.partiql.planner.util + +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 + +fun cartesianProduct(a: List, b: List, vararg lists: List): Set> = + (listOf(a, b).plus(lists)) + .fold(listOf(listOf())) { acc, set -> + acc.flatMap { list -> set.map { element -> list + element } } + }.toSet() + +val allSupportedType = StaticType.ALL_TYPES.filterNot { it == StaticType.GRAPH } + +val allSupportedTypeNotUnknown = allSupportedType.filterNot { it == StaticType.MISSING || it == StaticType.NULL } + +val allCollectionType = listOf(StaticType.LIST, StaticType.BAG, StaticType.SEXP) + +val allTextType = listOf(StaticType.SYMBOL, StaticType.STRING, StaticType.CLOB) + +val allDateTimeType = listOf(StaticType.TIME, StaticType.TIMESTAMP, StaticType.DATE) + +val allNumberType = StaticType.NUMERIC.allTypes + +val allIntType = listOf(StaticType.INT2, StaticType.INT4, StaticType.INT8, StaticType.INT) + +enum class CastType { + COERCION, // lossless + EXPLICIT, // lossy + UNSAFE // fail +} + +val castTable: ((StaticType, StaticType) -> CastType) = { from, to -> + when (from) { + is AnyOfType -> CastType.UNSAFE + is AnyType -> + when (to) { + is AnyType -> CastType.COERCION + else -> CastType.UNSAFE + } + is BlobType -> + when (to) { + is BlobType -> CastType.COERCION + else -> CastType.UNSAFE + } + is BoolType -> + when (to) { + is BoolType, is DecimalType, is FloatType, is IntType -> CastType.COERCION + is StringType, is SymbolType -> CastType.COERCION + else -> CastType.UNSAFE + } + is ClobType -> + when (to) { + is ClobType -> CastType.COERCION + else -> CastType.UNSAFE + } + is BagType -> when (to) { + is BagType -> CastType.COERCION + else -> CastType.UNSAFE + } + is ListType -> when (to) { + is BagType -> CastType.COERCION + else -> CastType.UNSAFE + } + is SexpType -> when (to) { + is BagType -> CastType.COERCION + else -> CastType.UNSAFE + } + is DateType -> when (to) { + is BagType -> CastType.COERCION + else -> CastType.UNSAFE + } + is DecimalType -> when (to) { + is DecimalType -> CastType.COERCION + else -> CastType.UNSAFE + } + is FloatType -> when (to) { + is DecimalType -> CastType.COERCION + is FloatType -> CastType.COERCION + else -> CastType.UNSAFE + } + is GraphType -> when (to) { + is GraphType -> CastType.COERCION + else -> CastType.UNSAFE + } + is IntType -> { + when (to) { + is IntType -> { + when (from.rangeConstraint) { + IntType.IntRangeConstraint.SHORT -> { + when (to.rangeConstraint) { + IntType.IntRangeConstraint.SHORT -> CastType.COERCION + IntType.IntRangeConstraint.INT4 -> CastType.COERCION + IntType.IntRangeConstraint.LONG -> CastType.COERCION + IntType.IntRangeConstraint.UNCONSTRAINED -> CastType.COERCION + } + } + IntType.IntRangeConstraint.INT4 -> { + when (to.rangeConstraint) { + IntType.IntRangeConstraint.SHORT -> CastType.UNSAFE + IntType.IntRangeConstraint.INT4 -> CastType.COERCION + IntType.IntRangeConstraint.LONG -> CastType.COERCION + IntType.IntRangeConstraint.UNCONSTRAINED -> CastType.COERCION + } + } + IntType.IntRangeConstraint.LONG -> { + when (to.rangeConstraint) { + IntType.IntRangeConstraint.SHORT -> CastType.UNSAFE + IntType.IntRangeConstraint.INT4 -> CastType.UNSAFE + IntType.IntRangeConstraint.LONG -> CastType.COERCION + IntType.IntRangeConstraint.UNCONSTRAINED -> CastType.COERCION + } + } + IntType.IntRangeConstraint.UNCONSTRAINED -> { + when (to.rangeConstraint) { + IntType.IntRangeConstraint.SHORT -> CastType.UNSAFE + IntType.IntRangeConstraint.INT4 -> CastType.UNSAFE + IntType.IntRangeConstraint.LONG -> CastType.UNSAFE + IntType.IntRangeConstraint.UNCONSTRAINED -> CastType.COERCION + } + } + } + } + is FloatType -> CastType.COERCION + is DecimalType -> CastType.COERCION + else -> CastType.UNSAFE + } + } + MissingType -> when (to) { + is MissingType -> CastType.COERCION + else -> CastType.UNSAFE + } + is NullType -> when (to) { + is NullType -> CastType.COERCION + else -> CastType.UNSAFE + } + is StringType -> + when (to) { + is StringType -> CastType.COERCION + is SymbolType -> CastType.COERCION + is ClobType -> CastType.COERCION + else -> CastType.UNSAFE + } + is StructType -> when (to) { + is StructType -> CastType.COERCION + else -> CastType.UNSAFE + } + is SymbolType -> when (to) { + is SymbolType -> CastType.COERCION + is StringType -> CastType.COERCION + is ClobType -> CastType.COERCION + else -> CastType.UNSAFE + } + is TimeType -> when (to) { + is TimeType -> CastType.COERCION + else -> CastType.UNSAFE + } + is TimestampType -> when (to) { + is TimestampType -> CastType.COERCION + else -> CastType.UNSAFE + } + } +} diff --git a/partiql-planner/src/testFixtures/resources/inputs/basics/case.sql b/partiql-planner/src/testFixtures/resources/inputs/basics/case.sql new file mode 100644 index 0000000000..f7099d53cc --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/inputs/basics/case.sql @@ -0,0 +1,85 @@ +--#[case-00] +CASE + WHEN FALSE THEN 0 + WHEN TRUE THEN 1 + ELSE 2 +END; + +--#[case-01] +CASE + WHEN 1 = 2 THEN 0 + WHEN 2 = 3 THEN 1 + ELSE 3 +END; + +--#[case-02] +CASE 1 + WHEN 1 THEN 'MATCH!' + ELSE 'NO MATCH!' +END; + +--#[case-03] +CASE 'Hello World' + WHEN 'Hello World' THEN TRUE + ELSE FALSE +END; + +--#[case-04] +SELECT + CASE a + WHEN TRUE THEN 'a IS TRUE' + ELSE 'a MUST BE FALSE' + END AS result +FROM T; + +--#[case-05] +SELECT + CASE + WHEN a = TRUE THEN 'a IS TRUE' + ELSE 'a MUST BE FALSE' + END AS result +FROM T; + +--#[case-06] +SELECT + CASE b + WHEN 10 THEN 'b IS 10' + ELSE 'b IS NOT 10' + END AS result +FROM T; + +--#[case-07] +-- TODO: This is currently failing as we seemingly cannot search for a nested attribute of a global. +SELECT + CASE d.e + WHEN 'WATER' THEN 'd.e IS WATER' + ELSE 'd.e IS NOT WATER' + END AS result +FROM T; + +--#[case-08] +SELECT + CASE x + WHEN 'WATER' THEN 'x IS WATER' + WHEN 5 THEN 'x IS 5' + ELSE 'x IS SOMETHING ELSE' + END AS result +FROM T; + +--#[case-09] +-- TODO: When using `x IS STRING` or `x IS DECIMAL`, I found that there are issues with the SqlCalls not receiving +-- the length/precision/scale parameters. This doesn't have to do with CASE_WHEN, but it needs to be addressed. +SELECT + CASE + WHEN x IS INT THEN 'x IS INT' + WHEN x IS STRUCT THEN 'x IS STRUCT' + ELSE 'x IS SOMETHING ELSE' + END AS result +FROM T; + +--#[case-10] +CASE + WHEN FALSE THEN 0 + WHEN FALSE THEN 1 + ELSE 2 +END; diff --git a/partiql-planner/src/testFixtures/resources/inputs/basics/paths.sql b/partiql-planner/src/testFixtures/resources/inputs/basics/paths.sql new file mode 100644 index 0000000000..4837ace0b6 --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/inputs/basics/paths.sql @@ -0,0 +1,135 @@ +-- ---------------------------------------- +-- PartiQL Path Navigation +-- ---------------------------------------- + +--#[paths-00] +-- tuple navigation +x.y; + +--#[paths-01] +-- array navigation with literal +x[0]; + +--#[paths-02] +-- tuple navigation with array notation +x['y']; + +--#[paths-03] +-- tuple navigation (2) +x."y"; + +--#[paths-04] +-- tuple navigation with explicit cast as string +x[CAST(z AS STRING)]; + +-- ---------------------------------------- +-- Composition of Navigation (5 choose 3) +-- ---------------------------------------- + +--#[paths-05] +x.y[0]['y']; + +--#[paths-06] +x.y[0]."y"; + +--#[paths-07] +x.y[0][CAST(z AS STRING)]; + +--#[paths-08] +x.y['y']."y"; + +--#[paths-09] +x.y['y'][CAST(z AS STRING)]; + +--#[paths-10] +x.y."y"[CAST(z AS STRING)]; + +--#[paths-11] +x[0]['y']."y"; + +--#[paths-12] +x[0]['y'][CAST(z AS STRING)]; + +--#[paths-13] +x[0]."y"[CAST(z AS STRING)]; + +--#[paths-14] +x['y']."y"[CAST(z AS STRING)]; + +-- ---------------------------------------- +-- Array Navigation with Expressions +-- ---------------------------------------- + +--#[paths-15] +x[0+1]; + +--#[paths-16] +x[ABS(1)]; + +-- ---------------------------------------- +-- PartiQL Path Navigation (+SFW) +-- ---------------------------------------- + +--#[paths-sfw-00] +-- tuple navigation +SELECT t.x.y AS v FROM t; + +--#[paths-sfw-01] +-- array navigation with literal +SELECT t.x[0] AS v FROM t; + +--#[paths-sfw-02] +-- tuple navigation with array notation (1) +SELECT t.x['y'] AS v FROM t; + +--#[paths-sfw-03] +-- tuple navigation with array notation (2) +SELECT t.x."y" AS v FROM t; + +--#[paths-sfw-04] +-- tuple navigation with explicit cast as string +SELECT t.x[CAST(t.z AS STRING)] AS v FROM t; + +-- ---------------------------------------- +-- Composition of Navigation (5 choose 3) +-- ---------------------------------------- + +--#[paths-sfw-05] +SELECT t.x.y[0]['y'] AS v FROM t; + +--#[paths-sfw-06] +SELECT t.x.y[0]."y" AS v FROM t; + +--#[paths-sfw-07] +SELECT t.x.y[0][CAST(t.z AS STRING)] AS v FROM t; + +--#[paths-sfw-08] +SELECT t.x.y['y']."y" AS v FROM t; + +--#[paths-sfw-09] +SELECT t.x.y['y'][CAST(t.z AS STRING)] AS v FROM t; + +--#[paths-sfw-10] +SELECT t.x.y."y"[CAST(t.z AS STRING)] AS v FROM t; + +--#[paths-sfw-11] +SELECT t.x[0]['y']."y" AS v FROM t; + +--#[paths-sfw-12] +SELECT t.x[0]['y'][CAST(t.z AS STRING)] AS v FROM t; + +--#[paths-sfw-13] +SELECT t.x[0]."y"[CAST(t.z AS STRING)] AS v FROM t; + +--#[paths-sfw-14] +SELECT t.x['y']."y"[CAST(t.z AS STRING)] AS v FROM t; + +-- ---------------------------------------- +-- Array Navigation with Expressions +-- ---------------------------------------- + +--#[paths-sfw-15] +SELECT t.x[0 + 1] AS v FROM t; + +--#[paths-sfw-16] +SELECT t.x[ABS(1)] AS v FROM t; diff --git a/partiql-planner/src/testFixtures/resources/inputs/basics/select.sql b/partiql-planner/src/testFixtures/resources/inputs/basics/select.sql new file mode 100644 index 0000000000..da7911c59e --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/inputs/basics/select.sql @@ -0,0 +1,50 @@ +--#[select-00] +SELECT a, b, c FROM T; + +--#[select-01] +SELECT * FROM T; + +--#[select-02] +SELECT VALUE { 'a': a, 'b': b, 'c': c } FROM T; + +--#[select-03] +SELECT VALUE a FROM T; + +--#[select-04] +SELECT * FROM T AS t1, T AS t2; + +--#[select-05] +SELECT t.d.* FROM T; + +--#[select-06] +SELECT t, t.d.* FROM T; + +--#[select-07] +SELECT t.d.*, t.d.* FROM T; + +--#[select-08] +SELECT d.* FROM T; + +--#[select-09] +SELECT t.* FROM T; + +--#[select-10] +SELECT t.c || CURRENT_USER FROM T; + +--#[select-11] +SELECT CURRENT_USER FROM T; + +--#[select-12] +SELECT CURRENT_DATE FROM T; + +--#[select-13] +SELECT DATE_DIFF(DAY, CURRENT_DATE, CURRENT_DATE) FROM T; + +--#[select-14] +SELECT DATE_ADD(DAY, 5, CURRENT_DATE) FROM T; + +--#[select-15] +SELECT DATE_ADD(DAY, -5, CURRENT_DATE) FROM T; + +--#[select-16] +SELECT a FROM t; diff --git a/partiql-planner/src/testFixtures/resources/inputs/basics/simple.sql b/partiql-planner/src/testFixtures/resources/inputs/basics/simple.sql new file mode 100644 index 0000000000..827bfab992 --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/inputs/basics/simple.sql @@ -0,0 +1,244 @@ +-- ------------------ +-- Globals +-- ------------------ + +--#[global-00] +my_global; + +-- ------------------ +-- Literals +-- ------------------ + +--#[sanity-lit-00] +true; + +--#[sanity-lit-01] +1; + +--#[sanity-lit-02] +1.0; + +--#[sanity-lit-03] +'hello'; + +--#[sanity-lit-04] +[ 'a', 'b', 'c' ]; + +--#[sanity-lit-05] +<< 'a', 'b', 'c' >>; + +--#[sanity-lit-06] +{ 'a': 1, 'b': 2, 'c': 3 }; + +-- ------------------ +-- Basic Expressions +-- ------------------ + +--#[expr-00] +t1 OR t2; + +--#[expr-01] +t1 AND t2; + +--#[expr-02] +NOT t1; + +--#[expr-03] +t1 < t2; + +--#[expr-04] +t1 <= t2; + +--#[expr-05] +t1 > t2; + +--#[expr-06] +t1 >= t2; + +--#[expr-07] +t1 = t2; + +--#[expr-08] +t1 != t2; + +--#[expr-09] +t1 <> t2; + +--#[expr-10] +t1 IS NULL; + +--#[expr-11] +t1 IS NOT NULL; + +--#[expr-12] +t1 IS MISSING; + +--#[expr-13] +t1 IS NOT MISSING; + +--#[expr-14] +t1 IS INT2; + +--#[expr-15] +t1 IS NOT INT2; + +--#[expr-16] +t1 IS INT4; + +--#[expr-17] +t1 IS NOT INT4; + +--#[expr-18] +t1 IS INT8; + +--#[expr-19] +t1 IS NOT INT8; + +--#[expr-20] +t1 IS INT; + +--#[expr-21] +t1 IS NOT INT; + +--#[expr-22] +t1 IS DECIMAL; + +--#[expr-23] +t1 IS NOT DECIMAL; + +--#[expr-24] +t1 IS FLOAT; + +--#[expr-25] +t1 IS NOT FLOAT; + +--#[expr-26] +t1 IS BOOL; + +--#[expr-27] +t1 IS NOT BOOL; + +--#[expr-28] +t1 IS SYMBOL; + +--#[expr-29] +t1 IS NOT SYMBOL; + +--#[expr-30] +t1 IS DATE; + +--#[expr-31] +t1 IS NOT DATE; + +--#[expr-32] +t1 IS TIME; + +--#[expr-33] +t1 IS NOT TIME; + +--#[expr-34] +t1 IS TIMESTAMP; + +--#[expr-35] +t1 IS NOT TIMESTAMP; + +--#[expr-36] +t1 IS STRING; + +--#[expr-37] +t1 IS NOT STRING; + +--#[expr-38] +t1 IS CLOB; + +--#[expr-39] +t1 IS NOT CLOB; + +--#[expr-40] +t1 IS BLOB; + +--#[expr-41] +t1 IS NOT BLOB; + +--#[expr-42] +t1 IS LIST; + +--#[expr-43] +t1 IS NOT LIST; + +--#[expr-44] +t1 IS SEXP; + +--#[expr-45] +t1 IS NOT SEXP; + +--#[expr-46] +t1 IS STRUCT; + +--#[expr-47] +t1 IS NOT STRUCT; + +--#[expr-48] +t1 IS BAG; + +--#[expr-49] +t1 IS NOT BAG; + +--#[expr-50] +t1 IN ( true ); + +--#[expr-51] +t1 NOT IN ( false ); + +--#[expr-52] +t1 IN t2; + +--#[expr-53] +t1 NOT IN t2; + +--#[expr-54] +t1 LIKE t2; + +--#[expr-55] +t1 NOT LIKE t2; + +--#[expr-56] +t1 LIKE t2 ESCAPE t3; + +--#[expr-57] +t1 BETWEEN t2 AND t3; + +--#[expr-58] +t1 NOT BETWEEN t2 AND t3; + +--#[expr-59] +t1 || t2; + +--#[expr-60] +t1 & t2; + +--#[expr-61] +t1 + t2; + +--#[expr-62] +t1 - t2; + +--#[expr-63] +t1 % t2; + +--#[expr-64] +t1 * t2; + +--#[expr-65] +t1 / t2; + +--#[expr-66] +CURRENT_USER; + +--#[expr-67] +CURRENT_DATE; + +--#[expr-68] +'My name is ' || CURRENT_USER; + +-- TO BE CONTINUED .... diff --git a/partiql-planner/src/testFixtures/resources/inputs/basics/subquery.sql b/partiql-planner/src/testFixtures/resources/inputs/basics/subquery.sql new file mode 100644 index 0000000000..4f3fa8e6e1 --- /dev/null +++ b/partiql-planner/src/testFixtures/resources/inputs/basics/subquery.sql @@ -0,0 +1,17 @@ +-- Scalar subquery coercion +--#[subquery-00] +1 = (SELECT b FROM T); + +-- Row value subquery coercion +--#[subquery-01] +(false, 1) = (SELECT a, b FROM T); + +-- IN collection subquery +--#[subquery-02] +SELECT UPPER(v) FROM T +WHERE b IN (SELECT b FROM T WHERE a); + +-- Scalar subquery coercion with aggregation +--#[subquery-03] +-- 100 = (SELECT MAX(t.b) FROM T as t) +100 = (SELECT COUNT(*) FROM T); diff --git a/partiql-types/src/main/kotlin/org/partiql/types/function/FunctionSignature.kt b/partiql-types/src/main/kotlin/org/partiql/types/function/FunctionSignature.kt index c2abeaa0ac..cbb5470b66 100644 --- a/partiql-types/src/main/kotlin/org/partiql/types/function/FunctionSignature.kt +++ b/partiql-types/src/main/kotlin/org/partiql/types/function/FunctionSignature.kt @@ -93,6 +93,46 @@ public sealed class FunctionSignature( result = 31 * result + (description?.hashCode() ?: 0) return result } + + // Logic for writing a [FunctionSignature] using SQL `CREATE FUNCTION` syntax. + + /** + * SQL-99 p.542 + */ + private val deterministicCharacteristic = when (isDeterministic) { + true -> "DETERMINISTIC" + else -> "NOT DETERMINISTIC" + } + + /** + * SQL-99 p.543 + */ + private val nullCallClause = when (isNullCall) { + true -> "RETURNS NULL ON NULL INPUT" + else -> "CALLED ON NULL INPUT" + } + + public fun sql(): String = buildString { + val fn = name.uppercase() + val indent = " " + append("CREATE FUNCTION \"$fn\" (") + if (parameters.isNotEmpty()) { + val extent = parameters.maxOf { it.name.length } + for (i in parameters.indices) { + val p = parameters[i] + val ws = (extent - p.name.length) + 1 + appendLine() + append(indent).append(p.name.uppercase()).append(" ".repeat(ws)).append(p.type.name) + if (i != parameters.size - 1) append(",") + } + } + appendLine(" )") + append(indent).appendLine("RETURNS $returns") + append(indent).appendLine("SPECIFIC $specific") + append(indent).appendLine(deterministicCharacteristic) + append(indent).appendLine(nullCallClause) + append(indent).appendLine("RETURN $fn ( ${parameters.joinToString { it.name.uppercase() }} ) ;") + } } /** @@ -140,44 +180,4 @@ public sealed class FunctionSignature( return result } } - - // // Logic for writing a [FunctionSignature] using SQL `CREATE FUNCTION` syntax. - // - // /** - // * SQL-99 p.542 - // */ - // private val deterministicCharacteristic = when (isDeterministic) { - // true -> "DETERMINISTIC" - // else -> "NOT DETERMINISTIC" - // } - // - // /** - // * SQL-99 p.543 - // */ - // private val nullCallClause = when (isNullCall) { - // true -> "RETURNS NULL ON NULL INPUT" - // else -> "CALLED ON NULL INPUT" - // } - // - // private fun sql(): String = buildString { - // val fn = name.uppercase() - // val indent = " " - // append("CREATE FUNCTION \"$fn\" (") - // if (parameters.isNotEmpty()) { - // val extent = parameters.maxOf { it.name.length } - // for (i in parameters.indices) { - // val p = parameters[i] - // val ws = (extent - p.name.length) + 1 - // appendLine() - // append(indent).append(p.name.uppercase()).append(" ".repeat(ws)).append(p.type.name) - // if (i != parameters.size - 1) append(",") - // } - // } - // appendLine(" )") - // append(indent).appendLine("RETURNS $returns") - // append(indent).appendLine("SPECIFIC $specific") - // append(indent).appendLine(deterministicCharacteristic) - // append(indent).appendLine(nullCallClause) - // append(indent).appendLine("RETURN $fn ( ${parameters.joinToString { it.name.uppercase() }} ) ;") - // } }