diff --git a/lang/src/org/partiql/lang/SqlException.kt b/lang/src/org/partiql/lang/SqlException.kt index 2b8100b51a..096e11c5eb 100644 --- a/lang/src/org/partiql/lang/SqlException.kt +++ b/lang/src/org/partiql/lang/SqlException.kt @@ -34,12 +34,10 @@ import org.partiql.lang.util.propertyValueMapOf * * @param message the message for this exception * @param errorCode the error code for this exception - * @param errorContextArg context for this error, contains details like line & column number among other attributes. + * @param errorContext context for this error, includes details like line & character offsets, among others. + * @param internal True to indicate that this exception a bug in ParitQL itself. False to indicate it was caused by + * incorrect usage of APIs or invalid end-user queries. * @param cause for this exception - * - * @constructor a custom error [message], the [errorCode], error context as a [propertyValueMap] and optional [cause] creates an - * [SqlException]. This is the constructor for the second configuration explained above. - * */ open class SqlException( override var message: String, @@ -83,7 +81,7 @@ open class SqlException( * * * ErrorCategory is one of `Lexer Error`, `Parser Error`, `Runtime Error` * * ErrorLocation is the line and column where the error occurred - * * Errormessatge is the **generated** error message + * * ErrorMessage is the **generated** error message * * * TODO: Prepend to the auto-generated message the file name. @@ -92,6 +90,10 @@ open class SqlException( fun generateMessage(): String = "${errorCategory(errorCode)}: ${errorLocation(errorContext)}: ${errorMessage(errorCode, errorContext)}" + /** Same as [generateMessage] but without the location. */ + fun generateMessageNoLocation(): String = + "${errorCategory(errorCode)}: ${errorMessage(errorCode, errorContext)}" + private fun errorMessage(errorCode: ErrorCode?, propertyValueMap: PropertyValueMap?): String = errorCode?.getErrorMessage(propertyValueMap) ?: UNKNOWN diff --git a/lang/src/org/partiql/lang/planner/EvaluatorOptions.kt b/lang/src/org/partiql/lang/planner/EvaluatorOptions.kt new file mode 100644 index 0000000000..546ed1a835 --- /dev/null +++ b/lang/src/org/partiql/lang/planner/EvaluatorOptions.kt @@ -0,0 +1,85 @@ +package org.partiql.lang.planner + +import org.partiql.lang.eval.ProjectionIterationBehavior +import org.partiql.lang.eval.ThunkOptions +import org.partiql.lang.eval.TypedOpBehavior +import org.partiql.lang.eval.TypingMode +import java.time.ZoneOffset + +/* + +Differences between CompilerOptions and EvaluatorOptions: + +- There is no EvaluatorOptions equivalent for CompileOptions.visitorTransformMode since the planner always runs some basic + normalization and variable resolution passes *before* the customer can inject their own transforms. +- There is no EvaluatorOptions equivalent for CompileOptions.thunkReturnTypeAssertions since PlannerPipeline does not +support the static type inferencer (yet). +- EvaluatorOptions.allowUndefinedVariables is new. +- EvaluatorOptions has no equivalent for CompileOptions.undefinedVariableBehavior -- this was added for backward +compatibility on behalf of a customer we don't have anymore. Internal bug number is IONSQL-134. + */ + +/** + * Specifies options that effect the behavior of the PartiQL physical plan evaluator. + * + * @param defaultTimezoneOffset Default timezone offset to be used when TIME WITH TIME ZONE does not explicitly + * specify the time zone. Defaults to [ZoneOffset.UTC]. + */ +@Suppress("DataClassPrivateConstructor") +data class EvaluatorOptions private constructor ( + val projectionIteration: ProjectionIterationBehavior = ProjectionIterationBehavior.FILTER_MISSING, + val thunkOptions: ThunkOptions = ThunkOptions.standard(), + val typingMode: TypingMode = TypingMode.LEGACY, + val typedOpBehavior: TypedOpBehavior = TypedOpBehavior.LEGACY, + val defaultTimezoneOffset: ZoneOffset = ZoneOffset.UTC +) { + companion object { + + /** + * Creates a java style builder that will choose the default values for any unspecified options. + */ + @JvmStatic + fun builder() = Builder() + + /** + * Creates a java style builder that will clone the [EvaluatorOptions] passed to the constructor. + */ + @JvmStatic + fun builder(options: EvaluatorOptions) = Builder(options) + + /** + * Kotlin style builder that will choose the default values for any unspecified options. + */ + fun build(block: Builder.() -> Unit) = Builder().apply(block).build() + + /** + * Kotlin style builder that will clone the [EvaluatorOptions] passed to the constructor. + */ + fun build(options: EvaluatorOptions, block: Builder.() -> Unit) = Builder(options).apply(block).build() + + /** + * Creates a [EvaluatorOptions] instance with the standard values for use by the legacy AST compiler. + */ + @JvmStatic + fun standard() = Builder().build() + } + + /** + * Builds a [EvaluatorOptions] instance. + */ + class Builder(private var options: EvaluatorOptions = EvaluatorOptions()) { + + fun projectionIteration(value: ProjectionIterationBehavior) = set { copy(projectionIteration = value) } + fun typingMode(value: TypingMode) = set { copy(typingMode = value) } + fun typedOpBehavior(value: TypedOpBehavior) = set { copy(typedOpBehavior = value) } + fun thunkOptions(value: ThunkOptions) = set { copy(thunkOptions = value) } + fun defaultTimezoneOffset(value: ZoneOffset) = set { copy(defaultTimezoneOffset = value) } + + private inline fun set(block: EvaluatorOptions.() -> EvaluatorOptions): Builder { + options = block(options) + return this + } + + fun build() = options + } +} diff --git a/lang/src/org/partiql/lang/planner/UniqueIdResolver.kt b/lang/src/org/partiql/lang/planner/MetadataResolver.kt similarity index 65% rename from lang/src/org/partiql/lang/planner/UniqueIdResolver.kt rename to lang/src/org/partiql/lang/planner/MetadataResolver.kt index cf581e8999..2f677b279a 100644 --- a/lang/src/org/partiql/lang/planner/UniqueIdResolver.kt +++ b/lang/src/org/partiql/lang/planner/MetadataResolver.kt @@ -8,6 +8,9 @@ sealed class ResolutionResult { /** * A success case, indicates the [uniqueId] of the match to the [BindingName] in the global scope. * Typically, this is defined by the storage layer. + * + * In the future, this will likely contain much more than just a unique id. It might include detailed schema + * information about global variables. */ data class GlobalVariable(val uniqueId: String) : ResolutionResult() @@ -22,9 +25,19 @@ sealed class ResolutionResult { object Undefined : ResolutionResult() } -fun interface UniqueIdResolver { +/** + * Supplies the query planner with metadata about the current database. Meant to be implemented by the application + * embedding PartiQL. + * + * Metadata is associated with global variables. Global variables can be tables or (less commonly) any other + * application specific global variable. + * + * In the future, new methods could be added which expose information about other types of database metadata such as + * available indexes and table statistics. + */ +interface MetadataResolver { /** - * Implementations try to resolve a global variable which is typically a database table to a unique identifier + * Implementations try to resolve a variable which is typically a database table to a schema * using [bindingName]. [bindingName] includes both the name as specified by the query author and a [BindingCase] * which indicates if query author included double quotes (") which mean the lookup should be case-sensitive. * @@ -41,10 +54,12 @@ fun interface UniqueIdResolver { * Note that while [ResolutionResult.LocalVariable] exists, it is intentionally marked `internal` and cannot * be used by outside this project. */ - fun resolve(bindingName: BindingName): ResolutionResult + fun resolveVariable(bindingName: BindingName): ResolutionResult } -private val EMPTY = UniqueIdResolver { ResolutionResult.Undefined } +private val EMPTY: MetadataResolver = object : MetadataResolver { + override fun resolveVariable(bindingName: BindingName): ResolutionResult = ResolutionResult.Undefined +} -/** Convenience function for obtaining an instance of [UniqueIdResolver] with no defined variables. */ -fun emptyUniqueIdResolver(): UniqueIdResolver = EMPTY +/** Convenience function for obtaining an instance of [MetadataResolver] with no defined variables. */ +fun emptyMetadataResolver(): MetadataResolver = EMPTY diff --git a/lang/src/org/partiql/lang/planner/PlannerPipeline.kt b/lang/src/org/partiql/lang/planner/PlannerPipeline.kt new file mode 100644 index 0000000000..ba9ed67e24 --- /dev/null +++ b/lang/src/org/partiql/lang/planner/PlannerPipeline.kt @@ -0,0 +1,355 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at: + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + */ + +package org.partiql.lang.planner + +import com.amazon.ion.IonSystem +import org.partiql.lang.SqlException +import org.partiql.lang.ast.SourceLocationMeta +import org.partiql.lang.domains.PartiqlAst +import org.partiql.lang.domains.PartiqlPhysical +import org.partiql.lang.errors.Problem +import org.partiql.lang.errors.ProblemCollector +import org.partiql.lang.errors.Property +import org.partiql.lang.eval.ExprFunction +import org.partiql.lang.eval.ExprValueFactory +import org.partiql.lang.eval.Expression +import org.partiql.lang.eval.ThunkReturnTypeAssertions +import org.partiql.lang.eval.builtins.createBuiltinFunctions +import org.partiql.lang.planner.transforms.PlanningProblemDetails +import org.partiql.lang.planner.transforms.normalize +import org.partiql.lang.planner.transforms.toDefaultPhysicalPlan +import org.partiql.lang.planner.transforms.toLogicalPlan +import org.partiql.lang.planner.transforms.toResolvedPlan +import org.partiql.lang.syntax.Parser +import org.partiql.lang.syntax.SqlParser +import org.partiql.lang.syntax.SyntaxException + +/** + * [PlannerPipeline] is the main interface for planning and compiling PartiQL queries into instances of [Expression] + * which can be executed. + * + * This class was originally derived from [org.partiql.lang.CompilerPipeline], which is the main compiler entry point + * for the legacy AST compiler. The main difference is that the logical and physical plans have taken the place of + * PartiQL's AST, and that after parsing several passes over the AST are performed: + * + * - It is transformed into a logical plan + * - Variables are resolved. + * - It is converted into a physical plan. + * + * In the future additional passes will exist to include optimizations like predicate & projection push-down, and + * will be extensible by customers who can include their own optimizations. + * + * Two basic scenarios for using this interface: + * + * 1. You want to plan and compile a query once, but don't care about re-using the plan across process instances. In + * this scenario, simply use the [planAndCompile] function to obtain an instance of [Expression] that can be + * invoked directly to evaluate a query. + * 2. You want to plan a query once and share a planned query across process boundaries. In this scenario, use + * [plan] to perform query planning and obtain an instance of [PartiqlPhysical.Statement] which can be serialized to + * Ion text or binary format. On the other side of the process boundary, use [compile] to turn the + * [PartiqlPhysical.Statement] query plan into an [Expression]. Compilation itself should be relatively fast. + * + * The provided builder companion creates an instance of [PlannerPipeline] that is NOT thread safe and should NOT be + * used to compile queries concurrently. If used in a multithreaded application, use one instance of [PlannerPipeline] + * per thread. + */ +interface PlannerPipeline { + val valueFactory: ExprValueFactory + + /** + * Plans a query only but does not compile it. + * + * - Parses the specified SQL string, producing an AST. + * - Converts the AST to a logical plan. + * - Resolves all global and local variables in the logical plan, assigning unique indexes to local variables + * and calling [MetadataResolver.resolveVariable] to obtain unique identifiers global values such as tables that + * are specific to the application embedding PartiQL, and optionally converts undefined variables to dynamic + * lookups. + * - Converts the logical plan to a physical plan with `(impl default)` operators. + * + * @param query The text of the SQL statement or expression to be planned. + * @return [PassResult.Success] containing an instance of [PartiqlPhysical.Statement] and any applicable warnings + * if planning was successful or [PassResult.Error] if not. + */ + fun plan(query: String): PassResult + + /** + * Compiles the previously planned [PartiqlPhysical.Statement] instance. + * + * @param physcialPlan The physical query plan. + * @return [PassResult.Success] containing an instance of [PartiqlPhysical.Statement] and any applicable warnings + * if compilation was successful or [PassResult.Error] if not. + */ + fun compile(physcialPlan: PartiqlPhysical.Plan): PassResult + + /** + * Plans and compiles a query. + * + * @param query The text of the SQL statement or expression to be planned and compiled. + * @return [PassResult.Success] containing an instance of [PartiqlPhysical.Statement] and any applicable warnings + * if compiling and planning was successful or [PassResult.Error] if not. + */ + fun planAndCompile(query: String): PassResult = + when (val planResult = plan(query)) { + is PassResult.Error -> PassResult.Error(planResult.errors) + is PassResult.Success -> { + when (val compileResult = compile(planResult.result)) { + is PassResult.Error -> compileResult + is PassResult.Success -> PassResult.Success( + compileResult.result, + // Need to include any warnings that may have been discovered during planning. + planResult.warnings + compileResult.warnings + ) + } + } + } + + @Suppress("DeprecatedCallableAddReplaceWith", "DEPRECATION") + companion object { + private const val WARNING = "WARNING: PlannerPipeline is EXPERIMENTAL and has incomplete language support! " + + "For production use, see org.partiql.lang.CompilerPipeline which is stable and supports all PartiQL " + + "features." + + /** Kotlin style builder for [PlannerPipeline]. If calling from Java instead use [builder]. */ + @Deprecated(WARNING) + fun build(ion: IonSystem, block: Builder.() -> Unit) = build(ExprValueFactory.standard(ion), block) + + /** Kotlin style builder for [PlannerPipeline]. If calling from Java instead use [builder]. */ + @Deprecated(WARNING) + fun build(valueFactory: ExprValueFactory, block: Builder.() -> Unit) = Builder(valueFactory).apply(block).build() + + /** Fluent style builder. If calling from Kotlin instead use the [build] method. */ + @JvmStatic + @Deprecated(WARNING) + fun builder(ion: IonSystem): Builder = builder(ExprValueFactory.standard(ion)) + + /** Fluent style builder. If calling from Kotlin instead use the [build] method. */ + @JvmStatic + @Deprecated(WARNING) + fun builder(valueFactory: ExprValueFactory): Builder = Builder(valueFactory) + + /** Returns an implementation of [PlannerPipeline] with all properties set to their defaults. */ + @JvmStatic + @Deprecated(WARNING) + fun standard(ion: IonSystem): PlannerPipeline = standard(ExprValueFactory.standard(ion)) + + /** Returns an implementation of [PlannerPipeline] with all properties set to their defaults. */ + @JvmStatic + @Deprecated(WARNING) + fun standard(valueFactory: ExprValueFactory): PlannerPipeline = builder(valueFactory).build() + } + + /** + * An implementation of the builder pattern for instances of [PlannerPipeline]. The created instance of + * [PlannerPipeline] is NOT thread safe and should NOT be used to compile queries concurrently. If used in a + * multithreaded application, use one instance of [PlannerPipeline] per thread. + */ + class Builder(val valueFactory: ExprValueFactory) { + private var parser: Parser? = null + private var evaluatorOptions: EvaluatorOptions? = null + private var metadataResolver: MetadataResolver = emptyMetadataResolver() + private var allowUndefinedVariables: Boolean = false + private var enableLegacyExceptionHandling: Boolean = false + + /** + * Specifies the [Parser] to be used to turn an PartiQL query into an instance of [PartiqlAst]. + * The default is [SqlParser]. + */ + fun sqlParser(p: Parser): Builder = this.apply { + parser = p + } + + /** + * Options affecting evaluation-time behavior. The default is [EvaluatorOptions.standard]. + */ + fun evaluatorOptions(options: EvaluatorOptions): Builder = this.apply { + evaluatorOptions = options + } + + /** + * A nested builder for compilation options. The default is [EvaluatorOptions.standard]. + * + * Avoid the use of this overload if calling from Java and instead use the overload accepting an instance + * of [EvaluatorOptions]. + * + * There is no need to call [Builder.build] when using this method. + */ + fun evaluatorOptions(block: EvaluatorOptions.Builder.() -> Unit): Builder = + evaluatorOptions(EvaluatorOptions.build(block)) + + /** + * Adds the [MetadataResolver] for global variables. + * + * [metadataResolver] is queried during query planning to fetch metadata information such as table schemas. + */ + fun metadataResolver(bindings: MetadataResolver): Builder = this.apply { + this.metadataResolver = bindings + } + + /** + * Sets a flag indicating if undefined variables are allowed. + * + * When allowed, undefined variables are rewritten to dynamic lookups. This is intended to provide a migration + * path for legacy PartiQL customers who depend on dynamic lookup of undefined variables to use the query + * planner & phys. algebra. New customers should not enable this. + */ + fun allowUndefinedVariables(allow: Boolean = true): Builder = this.apply { + this.allowUndefinedVariables = allow + } + + /** + * Prevents [SqlException] that occur during compilation from being converted into [Problem]s. + * + * This is for compatibility with the legacy unit test suite, which hasn't been updated to handle + * [Problem]s yet. + */ + internal fun enableLegacyExceptionHandling(): Builder = this.apply { + enableLegacyExceptionHandling = true + } + + /** Builds the actual implementation of [PlannerPipeline]. */ + fun build(): PlannerPipeline { + val compileOptionsToUse = evaluatorOptions ?: EvaluatorOptions.standard() + + when (compileOptionsToUse.thunkOptions.thunkReturnTypeAssertions) { + ThunkReturnTypeAssertions.DISABLED -> { /* take no action */ } + ThunkReturnTypeAssertions.ENABLED -> error( + "TODO: Support ThunkReturnTypeAssertions.ENABLED " + + "need a static type pass first)" + ) + } + + val builtinFunctions = createBuiltinFunctions(valueFactory) + // TODO: uncomment when DynamicLookupExprFunction exists +// val builtinFunctions = createBuiltinFunctions(valueFactory) + DynamicLookupExprFunction() + val builtinFunctionsMap = builtinFunctions.associateBy { + it.signature.name + } + + return PlannerPipelineImpl( + valueFactory = valueFactory, + parser = parser ?: SqlParser(valueFactory.ion), + evaluatorOptions = compileOptionsToUse, + functions = builtinFunctionsMap, + metadataResolver = metadataResolver, + allowUndefinedVariables = allowUndefinedVariables, + enableLegacyExceptionHandling = enableLegacyExceptionHandling + ) + } + } +} + +internal class PlannerPipelineImpl( + override val valueFactory: ExprValueFactory, + private val parser: Parser, + val evaluatorOptions: EvaluatorOptions, + val functions: Map, + val metadataResolver: MetadataResolver, + val allowUndefinedVariables: Boolean, + val enableLegacyExceptionHandling: Boolean +) : PlannerPipeline { + + init { + when (evaluatorOptions.thunkOptions.thunkReturnTypeAssertions) { + ThunkReturnTypeAssertions.DISABLED -> { + /** intentionally blank. */ + } + ThunkReturnTypeAssertions.ENABLED -> + // Need a type inferencer pass on resolved logical algebra to support this. + TODO("Support for EvaluatorOptions.thunkReturnTypeAsserts == ThunkReturnTypeAssertions.ENABLED") + } + } + + override fun plan(query: String): PassResult { + val ast = try { + parser.parseAstStatement(query) + } catch (ex: SyntaxException) { + val problem = Problem( + SourceLocationMeta( + ex.errorContext[Property.LINE_NUMBER]?.longValue() ?: -1, + ex.errorContext[Property.COLUMN_NUMBER]?.longValue() ?: -1 + ), + PlanningProblemDetails.ParseError(ex.generateMessageNoLocation()) + ) + return PassResult.Error(listOf(problem)) + } + // Now run the AST thru each pass until we arrive at the physical algebra. + + // Normalization--synthesizes any unspecified `AS` aliases, converts `SELECT *` to `SELECT f.*[, ...]` ... + val normalizedAst = ast.normalize() + + // ast -> logical plan + val logicalPlan = normalizedAst.toLogicalPlan() + + // logical plan -> resolved logical plan + val problemHandler = ProblemCollector() + val resolvedLogicalPlan = logicalPlan.toResolvedPlan(problemHandler, metadataResolver, allowUndefinedVariables) + // If there are unresolved variables after attempting to resolve variables, then we can't proceed. + if (problemHandler.hasErrors) { + return PassResult.Error(problemHandler.problems) + } + + // Possible future passes: + // - type checking and inferencing? + // - constant folding + // - common sub-expression removal + // - push down predicates & projections on top of their scan nodes. + // - customer supplied rewrites of resolved logical plan. + + // resolved logical plan -> physical plan. + // this will give all relational operators `(impl default)`. + val physicalPlan = resolvedLogicalPlan.toDefaultPhysicalPlan() + + // Future work: invoke passes to choose relational operator implementations other than `(impl default)`. + // Future work: fully push down predicates and projections into their physical read operators. + // Future work: customer supplied rewrites of phsyical plan + + // If we reach this far, we're successful. If there were any problems at all, they were just warnings. + return PassResult.Success(physicalPlan, problemHandler.problems) + } + + override fun compile(physcialPlan: PartiqlPhysical.Plan): PassResult { + TODO("uncomment the code below in the PR introducing the plan evaluator") +// val compiler = PhysicalExprToThunkConverterImpl( +// valueFactory = valueFactory, +// functions = functions, +// customTypedOpParameters = customTypedOpParameters, +// procedures = procedures, +// evaluatorOptions = evaluatorOptions +// ) +// +// val expression = when { +// enableLegacyExceptionHandling -> compiler.compile(physcialPlan) +// else -> { +// // Legacy exception handling is disabled, convert any [SqlException] into a Problem and return +// // PassResult.Error. +// try { +// compiler.compile(physcialPlan) +// } catch (e: SqlException) { +// val problem = Problem( +// SourceLocationMeta( +// e.errorContext[Property.LINE_NUMBER]?.longValue() ?: -1, +// e.errorContext[Property.COLUMN_NUMBER]?.longValue() ?: -1 +// ), +// PlanningProblemDetails.CompileError(e.generateMessageNoLocation()) +// ) +// return PassResult.Error(listOf(problem)) +// } +// } +// } +// +// return PassResult.Success(expression, listOf()) + } +} diff --git a/lang/src/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransform.kt b/lang/src/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransform.kt index c2416c4b60..3f4f2ccfa1 100644 --- a/lang/src/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransform.kt +++ b/lang/src/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransform.kt @@ -10,15 +10,15 @@ import org.partiql.lang.errors.Problem import org.partiql.lang.errors.ProblemHandler import org.partiql.lang.eval.BindingName import org.partiql.lang.eval.builtins.DYNAMIC_LOOKUP_FUNCTION_NAME +import org.partiql.lang.planner.MetadataResolver import org.partiql.lang.planner.ResolutionResult -import org.partiql.lang.planner.UniqueIdResolver import org.partiql.pig.runtime.asPrimitive /** * Resolves all variables by rewriting `(id )` to * `(id )`) or `(global_id )`, or a `$__dynamic_lookup__` call site (if enabled). * - * Local variables are resolved independently within this pass, but we rely on [globals] to resolve global variables. + * Local variables are resolved independently within this pass, but we rely on [resolver] to resolve global variables. * * There are actually two passes here: * 1. All [PartiqlLogical.VarDecl] nodes are allocated unique indexes (which is stored in a meta). This pass is @@ -71,14 +71,14 @@ import org.partiql.pig.runtime.asPrimitive */ internal fun PartiqlLogical.Plan.toResolvedPlan( problemHandler: ProblemHandler, - globals: UniqueIdResolver, + resolver: MetadataResolver, allowUndefinedVariables: Boolean = false ): PartiqlLogicalResolved.Plan { // Allocate a unique id for each `VarDecl` val (planWithAllocatedVariables, allLocals) = this.allocateVariableIds() // Transform to `partiql_logical_resolved` while resolving variables. - val resolvedSt = LogicalToLogicalResolvedVisitorTransform(allowUndefinedVariables, problemHandler, globals) + val resolvedSt = LogicalToLogicalResolvedVisitorTransform(allowUndefinedVariables, problemHandler, resolver) .transformPlan(planWithAllocatedVariables) .copy(locals = allLocals) @@ -119,7 +119,7 @@ private data class LogicalToLogicalResolvedVisitorTransform( /** Where to send error reports. */ private val problemHandler: ProblemHandler, /** If a variable is not found using [inputScope], we will attempt to locate the binding here instead. */ - private val globals: UniqueIdResolver, + private val globals: MetadataResolver, ) : PartiqlLogicalToPartiqlLogicalResolvedVisitorTransform() { /** The current [LocalScope]. */ @@ -253,14 +253,14 @@ private data class LogicalToLogicalResolvedVisitorTransform( node.qualifier is PartiqlLogical.ScopeQualifier.Unqualified ) { // look up variable in globals first, then locals - when (val globalResolutionResult = globals.resolve(bindingName)) { + when (val globalResolutionResult = globals.resolveVariable(bindingName)) { ResolutionResult.Undefined -> lookupLocalVariable(bindingName) else -> globalResolutionResult } } else { // look up variable in locals first, then globals. when (val localResolutionResult = lookupLocalVariable(bindingName)) { - ResolutionResult.Undefined -> globals.resolve(bindingName) + ResolutionResult.Undefined -> globals.resolveVariable(bindingName) else -> localResolutionResult } } diff --git a/lang/test/org/partiql/lang/planner/PlannerPipelineSmokeTests.kt b/lang/test/org/partiql/lang/planner/PlannerPipelineSmokeTests.kt new file mode 100644 index 0000000000..eb7a726802 --- /dev/null +++ b/lang/test/org/partiql/lang/planner/PlannerPipelineSmokeTests.kt @@ -0,0 +1,81 @@ +package org.partiql.lang.planner + +import com.amazon.ion.system.IonSystemBuilder +import com.amazon.ionelement.api.ionInt +import com.amazon.ionelement.api.ionString +import com.amazon.ionelement.api.toIonValue +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.partiql.lang.ION +import org.partiql.lang.domains.PartiqlPhysical +import org.partiql.lang.planner.transforms.PLAN_VERSION_NUMBER +import org.partiql.lang.planner.transforms.PlanningProblemDetails +import org.partiql.lang.util.SexpAstPrettyPrinter + +/** + * Query planning primarily consists of AST traversals and rewrites. Each of those are thoroughly tested separately, + * but it is still good to have a simple "smoke test" for the planner pipeline. + */ +class PlannerPipelineSmokeTests { + private val ion = IonSystemBuilder.standard().build() + + @Suppress("DEPRECATION") + private fun createPlannerPipelineForTest(allowUndefinedVariables: Boolean) = PlannerPipeline.build(ion) { + allowUndefinedVariables(allowUndefinedVariables) + metadataResolver(createFakeMetadataResolver("Customer" to "fake_uid_for_Customer")) + } + + @Test + fun `happy path`() { + val pipeline = createPlannerPipelineForTest(allowUndefinedVariables = true) + val result = pipeline.plan("SELECT c.* FROM Customer AS c WHERE c.primaryKey = 42") + + result as PassResult.Success + println(SexpAstPrettyPrinter.format(result.result.toIonElement().asAnyElement().toIonValue(ION))) + + assertEquals( + result, + PassResult.Success( + result = PartiqlPhysical.build { + plan( + stmt = query( + bindingsToValues( + exp = struct(structFields(localId(0))), + query = filter( + i = impl("default"), + predicate = eq( + operands0 = path( + localId(0), + pathExpr(lit(ionString("primaryKey")), caseInsensitive()) + ), + operands1 = lit(ionInt(42)) + ), + source = scan( + i = impl("default"), + expr = globalId("Customer", "fake_uid_for_Customer"), + asDecl = varDecl(0) + ) + ) + ) + ), + locals = listOf(localVariable("c", 0)), + version = PLAN_VERSION_NUMBER + ) + }, + warnings = emptyList() + ) + ) + } + + @Test + fun `undefined variable`() { + val qp = createPlannerPipelineForTest(allowUndefinedVariables = false) + val result = qp.plan("SELECT undefined.* FROM Customer AS c") + assertEquals( + PassResult.Error( + listOf(problem(1, 8, PlanningProblemDetails.UndefinedVariable("undefined", caseSensitive = false))) + ), + result + ) + } +} diff --git a/lang/test/org/partiql/lang/planner/Util.kt b/lang/test/org/partiql/lang/planner/Util.kt index f2e5381ef5..b4635b1e16 100644 --- a/lang/test/org/partiql/lang/planner/Util.kt +++ b/lang/test/org/partiql/lang/planner/Util.kt @@ -3,18 +3,21 @@ package org.partiql.lang.planner import org.partiql.lang.ast.SourceLocationMeta import org.partiql.lang.errors.Problem import org.partiql.lang.errors.ProblemDetails +import org.partiql.lang.eval.BindingName /** - * Creates a fake implementation of [UniqueIdResolver] with the specified [globalVariableNames]. + * Creates a fake implementation of [MetadataResolver] with the specified [globalVariableNames]. * * The fake unique identifier of bound variables is computed to be `fake_uid_for_${globalVariableName}`. */ -fun createFakeGlobalBindings(vararg globalVariableNames: Pair) = - UniqueIdResolver { bindingName -> - val matches = globalVariableNames.filter { bindingName.isEquivalentTo(it.first) } - when (matches.size) { - 0 -> ResolutionResult.Undefined - else -> ResolutionResult.GlobalVariable(matches.first().second) +fun createFakeMetadataResolver(vararg globalVariableNames: Pair) = + object : MetadataResolver { + override fun resolveVariable(bindingName: BindingName): ResolutionResult { + val matches = globalVariableNames.filter { bindingName.isEquivalentTo(it.first) } + return when (matches.size) { + 0 -> ResolutionResult.Undefined + else -> ResolutionResult.GlobalVariable(matches.first().second) + } } } diff --git a/lang/test/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransformTests.kt b/lang/test/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransformTests.kt index b985bc0046..eccfd4c4b3 100644 --- a/lang/test/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransformTests.kt +++ b/lang/test/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransformTests.kt @@ -14,7 +14,7 @@ import org.partiql.lang.errors.ProblemCollector import org.partiql.lang.eval.BindingCase import org.partiql.lang.eval.builtins.DYNAMIC_LOOKUP_FUNCTION_NAME import org.partiql.lang.eval.sourceLocationMeta -import org.partiql.lang.planner.createFakeGlobalBindings +import org.partiql.lang.planner.createFakeMetadataResolver import org.partiql.lang.planner.problem import org.partiql.lang.syntax.SqlParser import org.partiql.lang.util.ArgumentsProviderBase @@ -91,7 +91,7 @@ class LogicalToLogicalResolvedVisitorTransformTests { } /** Mock table resolver. That can resolve f, foo, or UPPERCASE_FOO, while respecting case-sensitivity. */ - private val globalBindings = createFakeGlobalBindings( + private val globalBindings = createFakeMetadataResolver( *listOf( "shadow", "foo",