org.partiql.lang.eval.EvaluatingCompiler.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of partiql-lang-kotlin Show documentation
Show all versions of partiql-lang-kotlin Show documentation
An implementation of PartiQL for the JVM written in Kotlin.
/*
* 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.eval
import com.amazon.ion.IonString
import com.amazon.ion.IonValue
import com.amazon.ion.Timestamp
import com.amazon.ionelement.api.MetaContainer
import com.amazon.ionelement.api.ionBool
import com.amazon.ionelement.api.toIonValue
import org.partiql.lang.ast.AggregateCallSiteListMeta
import org.partiql.lang.ast.AggregateRegisterIdMeta
import org.partiql.lang.ast.IsCountStarMeta
import org.partiql.lang.ast.SourceLocationMeta
import org.partiql.lang.ast.UniqueNameMeta
import org.partiql.lang.ast.find
import org.partiql.lang.ast.sourceLocation
import org.partiql.lang.domains.PartiqlAst
import org.partiql.lang.domains.PartiqlPhysical
import org.partiql.lang.domains.staticType
import org.partiql.lang.domains.toBindingCase
import org.partiql.lang.errors.ErrorCode
import org.partiql.lang.errors.Property
import org.partiql.lang.errors.PropertyValueMap
import org.partiql.lang.errors.UNBOUND_QUOTED_IDENTIFIER_HINT
import org.partiql.lang.eval.binding.Alias
import org.partiql.lang.eval.binding.localsBinder
import org.partiql.lang.eval.builtins.storedprocedure.StoredProcedure
import org.partiql.lang.eval.like.parsePattern
import org.partiql.lang.eval.time.Time
import org.partiql.lang.eval.visitors.PartiqlAstSanityValidator
import org.partiql.lang.types.AnyOfType
import org.partiql.lang.types.AnyType
import org.partiql.lang.types.FunctionSignature
import org.partiql.lang.types.IntType
import org.partiql.lang.types.SingleType
import org.partiql.lang.types.StaticType
import org.partiql.lang.types.TypedOpParameter
import org.partiql.lang.types.UnknownArguments
import org.partiql.lang.types.UnsupportedTypeCheckException
import org.partiql.lang.types.toTypedOpParameter
import org.partiql.lang.util.bigDecimalOf
import org.partiql.lang.util.checkThreadInterrupted
import org.partiql.lang.util.codePointSequence
import org.partiql.lang.util.div
import org.partiql.lang.util.drop
import org.partiql.lang.util.foldLeftProduct
import org.partiql.lang.util.interruptibleFold
import org.partiql.lang.util.isZero
import org.partiql.lang.util.minus
import org.partiql.lang.util.plus
import org.partiql.lang.util.rem
import org.partiql.lang.util.stringValue
import org.partiql.lang.util.take
import org.partiql.lang.util.times
import org.partiql.lang.util.totalMinutes
import org.partiql.lang.util.unaryMinus
import org.partiql.pig.runtime.SymbolPrimitive
import java.math.BigDecimal
import java.util.LinkedList
import java.util.Stack
import java.util.TreeSet
import java.util.regex.Pattern
/**
* A thunk with no parameters other than the current environment.
*
* See https://en.wikipedia.org/wiki/Thunk
*
* This name was chosen because it is a thunk that accepts an instance of `Environment`.
*/
private typealias ThunkEnv = Thunk
/**
* A thunk taking a single [T] argument and the current environment.
*
* See https://en.wikipedia.org/wiki/Thunk
*
* This name was chosen because it is a thunk that accepts an instance of `Environment` and an [ExprValue] as
* its arguments.
*/
private typealias ThunkEnvValue = ThunkValue
/**
* A basic compiler that converts an instance of [PartiqlAst] to an [Expression].
*
* This implementation produces a "compiled" form consisting of context-threaded
* code in the form of a tree of [ThunkEnv]s. An overview of this technique can be found
* [here][1].
*
* **Note:** *threaded* in this context is used in how the code gets *threaded* together for
* interpretation and **not** the concurrency primitive. That is to say this code is NOT thread
* safe.
*
* [1]: https://www.complang.tuwien.ac.at/anton/lvas/sem06w/fest.pdf
*
* Note that this is not implemented in the pattern of a typical visitor pattern. The visitor pattern isn't a good
* match for all scenarios. It's great for simple needs such as the types of checks performed or simple transformations
* such as partial evaluation and transforming variable references to De Bruijn indices, however a compiler needs
* much finer grain of control over exactly how and when each node is walked, visited, and transformed.
*
* @param functions A map of functions keyed by function name that will be available during compilation.
* @param compileOptions Various options that effect how the source code is compiled.
*/
internal class EvaluatingCompiler(
private val valueFactory: ExprValueFactory,
private val functions: Map,
private val customTypedOpParameters: Map,
private val procedures: Map,
private val compileOptions: CompileOptions = CompileOptions.standard()
) {
private val errorSignaler = compileOptions.typingMode.createErrorSignaler(valueFactory)
private val thunkFactory = compileOptions.typingMode.createThunkFactory(compileOptions.thunkOptions, valueFactory)
private val compilationContextStack = Stack()
private val currentCompilationContext: CompilationContext
get() = compilationContextStack.peek() ?: errNoContext(
"compilationContextStack was empty.", ErrorCode.EVALUATOR_UNEXPECTED_VALUE, internal = true
)
// Note: please don't make this inline -- it messes up [EvaluationException] stack traces and
// isn't a huge benefit because this is only used at SQL-compile time anyway.
private fun nestCompilationContext(
expressionContext: ExpressionContext,
fromSourceNames: Set,
block: () -> R
): R {
compilationContextStack.push(
when {
compilationContextStack.empty() -> CompilationContext(expressionContext, fromSourceNames)
else -> compilationContextStack.peek().createNested(
expressionContext,
fromSourceNames
)
}
)
try {
return block()
} finally {
compilationContextStack.pop()
}
}
private fun Number.exprValue(): ExprValue = when (this) {
is Int -> valueFactory.newInt(this)
is Long -> valueFactory.newInt(this)
is Double -> valueFactory.newFloat(this)
is BigDecimal -> valueFactory.newDecimal(this)
else -> errNoContext(
"Cannot convert number to expression value: $this",
errorCode = ErrorCode.EVALUATOR_INVALID_CONVERSION,
internal = true
)
}
private fun Boolean.exprValue(): ExprValue = valueFactory.newBoolean(this)
private fun String.exprValue(): ExprValue = valueFactory.newString(this)
/** Represents an instance of a compiled `GROUP BY` expression and alias. */
private class CompiledGroupByItem(val alias: ExprValue, val uniqueId: String?, val thunk: ThunkEnv)
/**
* Represents a memoized binding [BindingName] and an [ExprValue] of the same name.
* Used during evaluation og `GROUP BY`.
*/
private data class FromSourceBindingNamePair(val bindingName: BindingName, val nameExprValue: ExprValue)
/** Represents an instance of a compiled `ORDER BY` expression, orderingSpec and nulls type. */
private class CompiledOrderByItem(val comparator: NaturalExprValueComparators, val thunk: ThunkEnv)
/**
* Base class for [ExprAggregator] instances which accumulate values and perform a final computation.
*/
private inner class Accumulator(
var current: ExprValue?,
val nextFunc: (ExprValue?, ExprValue) -> ExprValue,
val valueFilter: (ExprValue) -> Boolean = { _ -> true }
) : ExprAggregator {
override fun next(value: ExprValue) {
// skip the accumulation function if the value is unknown or if the value is filtered out
if (value.isNotUnknown() && valueFilter.invoke(value)) {
current = nextFunc(current, value)
}
}
override fun compute() = current ?: valueFactory.nullValue
}
private fun comparisonAccumulator(comparator: NaturalExprValueComparators): (ExprValue?, ExprValue) -> ExprValue =
{ left, right ->
when {
left == null || comparator.compare(left, right) > 0 -> right
else -> left
}
}
/** Dispatch table for built-in aggregate functions. */
private val builtinAggregates: Map, ExprAggregatorFactory> =
run {
fun checkIsNumberType(funcName: String, value: ExprValue) {
if (!value.type.isNumber) {
errNoContext(
message = "Aggregate function $funcName expects arguments of NUMBER type but the following value was provided: $value, with type of ${value.type}",
errorCode = ErrorCode.EVALUATOR_INVALID_ARGUMENTS_FOR_AGG_FUNCTION,
internal = false
)
}
}
val countAccFunc: (ExprValue?, ExprValue) -> ExprValue = { accumulated, _ -> (accumulated!!.longValue() + 1L).exprValue() }
val sumAccFunc: (ExprValue?, ExprValue) -> ExprValue = { accumulated, nextItem ->
checkIsNumberType("SUM", nextItem)
accumulated?.let { (it.numberValue() + nextItem.numberValue()).exprValue() } ?: nextItem
}
val minAccFunc = comparisonAccumulator(NaturalExprValueComparators.NULLS_LAST_ASC)
val maxAccFunc = comparisonAccumulator(NaturalExprValueComparators.NULLS_LAST_DESC)
val avgAggregateGenerator = { filter: (ExprValue) -> Boolean ->
object : ExprAggregator {
var sum: Number? = null
var count = 0L
override fun next(value: ExprValue) {
if (value.isNotUnknown() && filter.invoke(value)) {
checkIsNumberType("AVG", value)
sum = sum?.let { it + value.numberValue() } ?: value.numberValue()
count++
}
}
override fun compute() =
sum?.let { (it / bigDecimalOf(count)).exprValue() }
?: [email protected]
}
}
val allFilter: (ExprValue) -> Boolean = { _ -> true }
// each distinct ExprAggregator must get its own createUniqueExprValueFilter()
mapOf(
Pair("count", PartiqlAst.SetQuantifier.All()) to ExprAggregatorFactory.over {
Accumulator((0L).exprValue(), countAccFunc, allFilter)
},
Pair("count", PartiqlAst.SetQuantifier.Distinct()) to ExprAggregatorFactory.over {
Accumulator((0L).exprValue(), countAccFunc, createUniqueExprValueFilter())
},
Pair("sum", PartiqlAst.SetQuantifier.All()) to ExprAggregatorFactory.over {
Accumulator(null, sumAccFunc, allFilter)
},
Pair("sum", PartiqlAst.SetQuantifier.Distinct()) to ExprAggregatorFactory.over {
Accumulator(null, sumAccFunc, createUniqueExprValueFilter())
},
Pair("avg", PartiqlAst.SetQuantifier.All()) to ExprAggregatorFactory.over {
avgAggregateGenerator(allFilter)
},
Pair("avg", PartiqlAst.SetQuantifier.Distinct()) to ExprAggregatorFactory.over {
avgAggregateGenerator(createUniqueExprValueFilter())
},
Pair("max", PartiqlAst.SetQuantifier.All()) to ExprAggregatorFactory.over {
Accumulator(null, maxAccFunc, allFilter)
},
Pair("max", PartiqlAst.SetQuantifier.Distinct()) to ExprAggregatorFactory.over {
Accumulator(null, maxAccFunc, createUniqueExprValueFilter())
},
Pair("min", PartiqlAst.SetQuantifier.All()) to ExprAggregatorFactory.over {
Accumulator(null, minAccFunc, allFilter)
},
Pair("min", PartiqlAst.SetQuantifier.Distinct()) to ExprAggregatorFactory.over {
Accumulator(null, minAccFunc, createUniqueExprValueFilter())
}
)
}
/**
* Compiles a [PartiqlAst.Statement] tree to an [Expression].
*
* Checks [Thread.interrupted] before every expression and sub-expression is compiled
* and throws [InterruptedException] if [Thread.interrupted] it has been set in the
* hope that long-running compilations may be aborted by the caller.
*/
fun compile(originalAst: PartiqlAst.Statement): Expression {
val visitorTransform = compileOptions.visitorTransformMode.createVisitorTransform()
val transformedAst = visitorTransform.transformStatement(originalAst)
val partiqlAstSanityValidator = PartiqlAstSanityValidator()
partiqlAstSanityValidator.validate(transformedAst, compileOptions)
val thunk = nestCompilationContext(ExpressionContext.NORMAL, emptySet()) {
compileAstStatement(transformedAst)
}
return object : Expression {
override fun eval(session: EvaluationSession): ExprValue {
val env = Environment(
session = session,
locals = session.globals,
current = session.globals
)
return thunk(env)
}
}
}
/**
* Evaluates an instance of [PartiqlAst.Statement] against a global set of bindings.
*/
fun eval(ast: PartiqlAst.Statement, session: EvaluationSession): ExprValue = compile(ast).eval(session)
/**
* Compiles the specified [PartiqlAst.Statement] into a [ThunkEnv].
*
* This function will [InterruptedException] if [Thread.interrupted] has been set.
*/
private fun compileAstStatement(ast: PartiqlAst.Statement): ThunkEnv {
checkThreadInterrupted()
return when (ast) {
is PartiqlAst.Statement.Query -> compileAstExpr(ast.expr)
is PartiqlAst.Statement.Ddl -> compileDdl(ast)
is PartiqlAst.Statement.Dml -> compileDml(ast)
is PartiqlAst.Statement.Exec -> compileExec(ast)
is PartiqlAst.Statement.Explain -> throw EvaluationException(
"EXPLAIN is not supported in the Evaluating Compiler",
ErrorCode.UNIMPLEMENTED_FEATURE,
internal = false
)
}
}
private fun compileAstExpr(expr: PartiqlAst.Expr): ThunkEnv {
val metas = expr.metas
return when (expr) {
is PartiqlAst.Expr.Lit -> compileLit(expr, metas)
is PartiqlAst.Expr.Missing -> compileMissing(metas)
is PartiqlAst.Expr.Id -> compileId(expr, metas)
is PartiqlAst.Expr.SimpleCase -> compileSimpleCase(expr, metas)
is PartiqlAst.Expr.SearchedCase -> compileSearchedCase(expr, metas)
is PartiqlAst.Expr.Path -> compilePath(expr, metas)
is PartiqlAst.Expr.Struct -> compileStruct(expr, metas)
is PartiqlAst.Expr.Select -> compileSelect(expr, metas)
is PartiqlAst.Expr.CallAgg -> compileCallAgg(expr, metas)
is PartiqlAst.Expr.Parameter -> compileParameter(expr, metas)
is PartiqlAst.Expr.Date -> compileDate(expr, metas)
is PartiqlAst.Expr.LitTime -> compileLitTime(expr, metas)
// arithmetic operations
is PartiqlAst.Expr.Plus -> compilePlus(expr, metas)
is PartiqlAst.Expr.Times -> compileTimes(expr, metas)
is PartiqlAst.Expr.Minus -> compileMinus(expr, metas)
is PartiqlAst.Expr.Divide -> compileDivide(expr, metas)
is PartiqlAst.Expr.Modulo -> compileModulo(expr, metas)
// comparison operators
is PartiqlAst.Expr.And -> compileAnd(expr, metas)
is PartiqlAst.Expr.Between -> compileBetween(expr, metas)
is PartiqlAst.Expr.Eq -> compileEq(expr, metas)
is PartiqlAst.Expr.Gt -> compileGt(expr, metas)
is PartiqlAst.Expr.Gte -> compileGte(expr, metas)
is PartiqlAst.Expr.Lt -> compileLt(expr, metas)
is PartiqlAst.Expr.Lte -> compileLte(expr, metas)
is PartiqlAst.Expr.Like -> compileLike(expr, metas)
is PartiqlAst.Expr.InCollection -> compileIn(expr, metas)
// logical operators
is PartiqlAst.Expr.Ne -> compileNe(expr, metas)
is PartiqlAst.Expr.Or -> compileOr(expr, metas)
// unary
is PartiqlAst.Expr.Not -> compileNot(expr, metas)
is PartiqlAst.Expr.Pos -> compilePos(expr, metas)
is PartiqlAst.Expr.Neg -> compileNeg(expr, metas)
// other operators
is PartiqlAst.Expr.Concat -> compileConcat(expr, metas)
is PartiqlAst.Expr.Call -> compileCall(expr, metas)
is PartiqlAst.Expr.NullIf -> compileNullIf(expr, metas)
is PartiqlAst.Expr.Coalesce -> compileCoalesce(expr, metas)
// "typed" operators (RHS is a data type and not an expression)
is PartiqlAst.Expr.Cast -> compileCast(expr, metas)
is PartiqlAst.Expr.IsType -> compileIs(expr, metas)
is PartiqlAst.Expr.CanCast -> compileCanCast(expr, metas)
is PartiqlAst.Expr.CanLosslessCast -> compileCanLosslessCast(expr, metas)
// sequence constructors
is PartiqlAst.Expr.List -> compileSeq(ExprValueType.LIST, expr.values, metas)
is PartiqlAst.Expr.Sexp -> compileSeq(ExprValueType.SEXP, expr.values, metas)
is PartiqlAst.Expr.Bag -> compileSeq(ExprValueType.BAG, expr.values, metas)
// bag operators
is PartiqlAst.Expr.BagOp -> compileBagOp(expr, metas)
is PartiqlAst.Expr.GraphMatch -> TODO("Compilation of GraphMatch expression")
is PartiqlAst.Expr.CallWindow -> TODO("Evaluating Compiler doesn't support window function")
}
}
private fun compileAstExprs(args: List) = args.map { compileAstExpr(it) }
private fun compileNullIf(expr: PartiqlAst.Expr.NullIf, metas: MetaContainer): ThunkEnv {
val expr1Thunk = compileAstExpr(expr.expr1)
val expr2Thunk = compileAstExpr(expr.expr2)
// Note: NULLIF does not propagate the unknown values and .exprEquals provides the correct semantics.
return thunkFactory.thunkEnv(metas) { env ->
val expr1Value = expr1Thunk(env)
val expr2Value = expr2Thunk(env)
when {
expr1Value.exprEquals(expr2Value) -> valueFactory.nullValue
else -> expr1Value
}
}
}
private fun compileCoalesce(expr: PartiqlAst.Expr.Coalesce, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.args)
return thunkFactory.thunkEnv(metas) { env ->
var nullFound = false
var knownValue: ExprValue? = null
for (thunk in argThunks) {
val argValue = thunk(env)
if (argValue.isNotUnknown()) {
knownValue = argValue
// No need to execute remaining thunks to save computation as first non-unknown value is found
break
}
if (argValue.type == ExprValueType.NULL) {
nullFound = true
}
}
when (knownValue) {
null -> when {
compileOptions.typingMode == TypingMode.PERMISSIVE && !nullFound -> valueFactory.missingValue
else -> valueFactory.nullValue
}
else -> knownValue
}
}
}
/**
* Returns a function that accepts an [ExprValue] as an argument and returns true it is `NULL`, `MISSING`, or
* within the range specified by [range].
*/
private fun integerValueValidator(
range: LongRange
): (ExprValue) -> Boolean = { value ->
when (value.type) {
ExprValueType.NULL, ExprValueType.MISSING -> true
ExprValueType.INT -> {
val longValue: Long = value.scalar.numberValue()?.toLong()
?: error(
"ExprValue.numberValue() must not be `NULL` when its type is INT." +
"This indicates that the ExprValue instance has a bug."
)
// PRO-TIP: make sure to use the `Long` primitive type here with `.contains` otherwise
// Kotlin will use the version of `.contains` that treats [range] as a collection, and it will
// be very slow!
range.contains(longValue)
}
else -> error(
"IntegerValueValidator can only accept ExprValue with type INT, NULL, and MISSING."
)
}
}
/**
* For operators which could return integer type, check integer overflow in case of [TypingMode.PERMISSIVE].
*/
private fun checkIntegerOverflow(computeThunk: ThunkEnv, metas: MetaContainer): ThunkEnv =
when (val staticTypes = metas.staticType?.type?.getTypes()) {
// No staticType, can't validate integer size.
null -> computeThunk
else -> {
when (compileOptions.typingMode) {
TypingMode.LEGACY -> {
// integer size constraints have not been tested under [TypingMode.LEGACY] because the
// [StaticTypeInferenceVisitorTransform] doesn't support being used with legacy mode yet.
// throw an exception in case we encounter this untested scenario. This might work fine, but I
// wouldn't bet on it.
val hasConstrainedInteger = staticTypes.any {
it is IntType && it.rangeConstraint != IntType.IntRangeConstraint.UNCONSTRAINED
}
if (hasConstrainedInteger) {
TODO("Legacy mode doesn't support integer size constraints yet.")
} else {
computeThunk
}
}
TypingMode.PERMISSIVE -> {
val biggestIntegerType = staticTypes.filterIsInstance().maxBy {
it.rangeConstraint.numBytes
}
when (biggestIntegerType) {
// static type contains one or more IntType
is IntType -> {
val validator = integerValueValidator(biggestIntegerType.rangeConstraint.validRange)
thunkFactory.thunkEnv(metas) { env ->
val naryResult = computeThunk(env)
// validation shall only happen when the result is INT/MISSING/NULL
// this is important as StaticType may contain a mixture of multiple types
when (val type = naryResult.type) {
ExprValueType.INT, ExprValueType.MISSING, ExprValueType.NULL -> errorSignaler.errorIf(
!validator(naryResult),
ErrorCode.EVALUATOR_INTEGER_OVERFLOW,
{ ErrorDetails(metas, "Integer overflow", errorContextFrom(metas)) },
{ naryResult }
)
else -> {
if (staticTypes.all { it is IntType }) {
error(
"The expression's static type was supposed to be INT but instead it was $type" +
"This may indicate the presence of a bug in the type inferencer."
)
} else {
naryResult
}
}
}
}
}
// If there is no IntType StaticType, can't validate the integer size either.
null -> computeThunk
else -> computeThunk
}
}
}
}
}
private fun compilePlus(expr: PartiqlAst.Expr.Plus, metas: MetaContainer): ThunkEnv {
if (expr.operands.size < 2) {
error("Internal Error: PartiqlAst.Expr.Plus must have at least 2 arguments")
}
val argThunks = compileAstExprs(expr.operands)
val computeThunk = thunkFactory.thunkFold(metas, argThunks) { lValue, rValue ->
(lValue.numberValue() + rValue.numberValue()).exprValue()
}
return checkIntegerOverflow(computeThunk, metas)
}
private fun compileMinus(expr: PartiqlAst.Expr.Minus, metas: MetaContainer): ThunkEnv {
if (expr.operands.size < 2) {
error("Internal Error: PartiqlAst.Expr.Minus must have at least 2 arguments")
}
val argThunks = compileAstExprs(expr.operands)
val computeThunk = thunkFactory.thunkFold(metas, argThunks) { lValue, rValue ->
(lValue.numberValue() - rValue.numberValue()).exprValue()
}
return checkIntegerOverflow(computeThunk, metas)
}
private fun compilePos(expr: PartiqlAst.Expr.Pos, metas: MetaContainer): ThunkEnv {
val exprThunk = compileAstExpr(expr.expr)
val computeThunk = thunkFactory.thunkEnvOperands(metas, exprThunk) { _, value ->
// Invoking .numberValue() here makes this essentially just a type check
value.numberValue()
// Original value is returned unmodified.
value
}
return checkIntegerOverflow(computeThunk, metas)
}
private fun compileNeg(expr: PartiqlAst.Expr.Neg, metas: MetaContainer): ThunkEnv {
val exprThunk = compileAstExpr(expr.expr)
val computeThunk = thunkFactory.thunkEnvOperands(metas, exprThunk) { _, value ->
(-value.numberValue()).exprValue()
}
return checkIntegerOverflow(computeThunk, metas)
}
private fun compileTimes(expr: PartiqlAst.Expr.Times, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
val computeThunk = thunkFactory.thunkFold(metas, argThunks) { lValue, rValue ->
(lValue.numberValue() * rValue.numberValue()).exprValue()
}
return checkIntegerOverflow(computeThunk, metas)
}
private fun compileDivide(expr: PartiqlAst.Expr.Divide, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
val computeThunk = thunkFactory.thunkFold(metas, argThunks) { lValue, rValue ->
val denominator = rValue.numberValue()
errorSignaler.errorIf(
denominator.isZero(),
ErrorCode.EVALUATOR_DIVIDE_BY_ZERO,
{ ErrorDetails(metas, "/ by zero") }
) {
try {
(lValue.numberValue() / denominator).exprValue()
} catch (e: ArithmeticException) {
// Setting the internal flag as true as it is not clear what
// ArithmeticException may be thrown by the above
throw EvaluationException(
cause = e,
errorCode = ErrorCode.EVALUATOR_ARITHMETIC_EXCEPTION,
errorContext = errorContextFrom(metas),
internal = true
)
}
}
}
return checkIntegerOverflow(computeThunk, metas)
}
private fun compileModulo(expr: PartiqlAst.Expr.Modulo, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
val computeThunk = thunkFactory.thunkFold(metas, argThunks) { lValue, rValue ->
val denominator = rValue.numberValue()
if (denominator.isZero()) {
err("% by zero", ErrorCode.EVALUATOR_MODULO_BY_ZERO, errorContextFrom(metas), false)
}
(lValue.numberValue() % denominator).exprValue()
}
return checkIntegerOverflow(computeThunk, metas)
}
private fun compileEq(expr: PartiqlAst.Expr.Eq, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
return thunkFactory.thunkAndMap(metas, argThunks) { lValue, rValue ->
(lValue.exprEquals(rValue))
}
}
private fun compileNe(expr: PartiqlAst.Expr.Ne, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
return thunkFactory.thunkFold(metas, argThunks) { lValue, rValue ->
((!lValue.exprEquals(rValue)).exprValue())
}
}
private fun compileLt(expr: PartiqlAst.Expr.Lt, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
return thunkFactory.thunkAndMap(metas, argThunks) { lValue, rValue -> lValue < rValue }
}
private fun compileLte(expr: PartiqlAst.Expr.Lte, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
return thunkFactory.thunkAndMap(metas, argThunks) { lValue, rValue -> lValue <= rValue }
}
private fun compileGt(expr: PartiqlAst.Expr.Gt, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
return thunkFactory.thunkAndMap(metas, argThunks) { lValue, rValue -> lValue > rValue }
}
private fun compileGte(expr: PartiqlAst.Expr.Gte, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
return thunkFactory.thunkAndMap(metas, argThunks) { lValue, rValue -> lValue >= rValue }
}
private fun compileBetween(expr: PartiqlAst.Expr.Between, metas: MetaContainer): ThunkEnv {
val valueThunk = compileAstExpr(expr.value)
val fromThunk = compileAstExpr(expr.from)
val toThunk = compileAstExpr(expr.to)
return thunkFactory.thunkEnvOperands(metas, valueThunk, fromThunk, toThunk) { _, v, f, t ->
(v >= f && v <= t).exprValue()
}
}
/**
* `IN` can *almost* be thought of has being syntactic sugar for the `OR` operator.
*
* `a IN (b, c, d)` is equivalent to `a = b OR a = c OR a = d`. On deep inspection, there
* are important implications to this regarding propagation of unknown values. Specifically, the
* presence of any unknown in `b`, `c`, or `d` will result in unknown propagation iif `a` does not
* equal `b`, `c`, or `d`. i.e.:
*
* - `1 in (null, 2, 3)` -> `null`
* - `2 in (null, 2, 3)` -> `true`
* - `2 in (1, 2, 3)` -> `true`
* - `0 in (1, 2, 4)` -> `false`
*
* `IN` is varies from the `OR` operator in that this behavior holds true when other types of expressions are
* used on the right side of `IN` such as sub-queries and variables whose value is that of a list or bag.
*/
private fun compileIn(expr: PartiqlAst.Expr.InCollection, metas: MetaContainer): ThunkEnv {
val args = expr.operands
val leftThunk = compileAstExpr(args[0])
val rightOp = args[1]
fun isOptimizedCase(values: List): Boolean =
values.all { it is PartiqlAst.Expr.Lit && !it.value.isNull }
fun optimizedCase(values: List): ThunkEnv {
// Put all the literals in the sequence into a pre-computed map to be checked later by the thunk.
// If the left-hand value is one of these we can short-circuit with a result of TRUE.
// This is the fastest possible case and allows for hundreds of literal values (or more) in the
// sequence without a huge performance penalty.
// NOTE: we cannot use a [HashSet<>] here because [ExprValue] does not implement [Object.hashCode] or
// [Object.equals].
val precomputedLiteralsMap = values
.filterIsInstance()
.mapTo(TreeSet(DEFAULT_COMPARATOR)) {
valueFactory.newFromIonValue(
it.value.toIonValue(
valueFactory.ion
)
)
}
// the compiled thunk simply checks if the left side is contained on the right side.
// thunkEnvOperands takes care of unknown propagation for the left side; for the right,
// this unknown propagation does not apply since we've eliminated the possibility of unknowns above.
return thunkFactory.thunkEnvOperands(metas, leftThunk) { _, leftValue ->
precomputedLiteralsMap.contains(leftValue).exprValue()
}
}
return when {
// We can significantly optimize this if rightArg is a sequence constructor which is composed of entirely
// of non-null literal values.
rightOp is PartiqlAst.Expr.List && isOptimizedCase(rightOp.values) -> optimizedCase(rightOp.values)
rightOp is PartiqlAst.Expr.Bag && isOptimizedCase(rightOp.values) -> optimizedCase(rightOp.values)
rightOp is PartiqlAst.Expr.Sexp && isOptimizedCase(rightOp.values) -> optimizedCase(rightOp.values)
// The unoptimized case...
else -> {
val rightThunk = compileAstExpr(rightOp)
// Legacy mode:
// Returns FALSE when the right side of IN is not a sequence
// Returns NULL if the right side is MISSING or any value on the right side is MISSING
// Permissive mode:
// Returns MISSING when the right side of IN is not a sequence
// Returns MISSING if the right side is MISSING or any value on the right side is MISSING
val (propagateMissingAs, propagateNotASeqAs) = with(valueFactory) {
when (compileOptions.typingMode) {
TypingMode.LEGACY -> nullValue to newBoolean(false)
TypingMode.PERMISSIVE -> missingValue to missingValue
}
}
// Note that standard unknown propagation applies to the left and right operands. Both [TypingMode]s
// are handled by [ThunkFactory.thunkEnvOperands] and that additional rules for unknown propagation are
// implemented within the thunk for the values within the sequence on the right side of IN.
thunkFactory.thunkEnvOperands(metas, leftThunk, rightThunk) { _, leftValue, rightValue ->
var nullSeen = false
var missingSeen = false
when {
rightValue.type == ExprValueType.MISSING -> propagateMissingAs
!rightValue.type.isSequence -> propagateNotASeqAs
else -> {
rightValue.forEach {
when (it.type) {
ExprValueType.NULL -> nullSeen = true
ExprValueType.MISSING -> missingSeen = true
// short-circuit to TRUE on the first matching value
else -> if (it.exprEquals(leftValue)) {
return@thunkEnvOperands valueFactory.newBoolean(true)
}
}
}
// If we make it here then there was no match. Propagate MISSING, NULL or return false.
// Note that if both MISSING and NULL was encountered, MISSING takes precedence.
when {
missingSeen -> propagateMissingAs
nullSeen -> valueFactory.nullValue
else -> valueFactory.newBoolean(false)
}
}
}
}
}
}
}
private fun compileNot(expr: PartiqlAst.Expr.Not, metas: MetaContainer): ThunkEnv {
val argThunk = compileAstExpr(expr.expr)
return thunkFactory.thunkEnvOperands(metas, argThunk) { _, value ->
(!value.booleanValue()).exprValue()
}
}
private fun compileAnd(expr: PartiqlAst.Expr.And, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
// can't use the null propagation supplied by [ThunkFactory.thunkEnv] here because AND short-circuits on
// false values and *NOT* on NULL or MISSING
return when (compileOptions.typingMode) {
TypingMode.LEGACY -> thunkFactory.thunkEnv(metas) thunk@{ env ->
var hasUnknowns = false
argThunks.forEach { currThunk ->
val currValue = currThunk(env)
when {
currValue.isUnknown() -> hasUnknowns = true
// Short circuit only if we encounter a known false value.
!currValue.booleanValue() -> return@thunk valueFactory.newBoolean(false)
}
}
when (hasUnknowns) {
true -> valueFactory.nullValue
false -> valueFactory.newBoolean(true)
}
}
TypingMode.PERMISSIVE -> thunkFactory.thunkEnv(metas) thunk@{ env ->
var hasNull = false
var hasMissing = false
argThunks.forEach { currThunk ->
val currValue = currThunk(env)
when (currValue.type) {
// Short circuit only if we encounter a known false value.
ExprValueType.BOOL -> if (!currValue.booleanValue()) return@thunk valueFactory.newBoolean(false)
ExprValueType.NULL -> hasNull = true
// type mismatch, return missing
else -> hasMissing = true
}
}
when {
hasMissing -> valueFactory.missingValue
hasNull -> valueFactory.nullValue
else -> valueFactory.newBoolean(true)
}
}
}
}
private fun compileOr(expr: PartiqlAst.Expr.Or, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
// can't use the null propagation supplied by [ThunkFactory.thunkEnv] here because OR short-circuits on
// true values and *NOT* on NULL or MISSING
return when (compileOptions.typingMode) {
TypingMode.LEGACY ->
thunkFactory.thunkEnv(metas) thunk@{ env ->
var hasUnknowns = false
argThunks.forEach { currThunk ->
val currValue = currThunk(env)
// How null-propagation works for OR is rather weird according to the SQL-92 spec.
// Nulls are propagated like other expressions only when none of the terms are TRUE.
// If any one of them is TRUE, then the entire expression evaluates to TRUE, i.e.:
// NULL OR TRUE -> TRUE
// NULL OR FALSE -> NULL
// (strange but true)
when {
currValue.isUnknown() -> hasUnknowns = true
currValue.booleanValue() -> return@thunk valueFactory.newBoolean(true)
}
}
when (hasUnknowns) {
true -> valueFactory.nullValue
false -> valueFactory.newBoolean(false)
}
}
TypingMode.PERMISSIVE -> thunkFactory.thunkEnv(metas) thunk@{ env ->
var hasNull = false
var hasMissing = false
argThunks.forEach { currThunk ->
val currValue = currThunk(env)
when (currValue.type) {
// Short circuit only if we encounter a known true value.
ExprValueType.BOOL -> if (currValue.booleanValue()) return@thunk valueFactory.newBoolean(true)
ExprValueType.NULL -> hasNull = true
else -> hasMissing = true // type mismatch, return missing.
}
}
when {
hasMissing -> valueFactory.missingValue
hasNull -> valueFactory.nullValue
else -> valueFactory.newBoolean(false)
}
}
}
}
private fun compileConcat(expr: PartiqlAst.Expr.Concat, metas: MetaContainer): ThunkEnv {
val argThunks = compileAstExprs(expr.operands)
return thunkFactory.thunkFold(metas, argThunks) { lValue, rValue ->
val lType = lValue.type
val rType = rValue.type
if (lType.isText && rType.isText) {
// null/missing propagation is handled before getting here
(lValue.stringValue() + rValue.stringValue()).exprValue()
} else {
err(
"Wrong argument type for ||",
ErrorCode.EVALUATOR_CONCAT_FAILED_DUE_TO_INCOMPATIBLE_TYPE,
errorContextFrom(metas).also {
it[Property.ACTUAL_ARGUMENT_TYPES] = listOf(lType, rType).toString()
},
internal = false
)
}
}
}
private fun compileCall(expr: PartiqlAst.Expr.Call, metas: MetaContainer): ThunkEnv {
val funcArgThunks = compileAstExprs(expr.args)
val func = functions[expr.funcName.text] ?: err(
"No such function: ${expr.funcName.text}",
ErrorCode.EVALUATOR_NO_SUCH_FUNCTION,
errorContextFrom(metas).also {
it[Property.FUNCTION_NAME] = expr.funcName.text
},
internal = false
)
// Check arity
if (funcArgThunks.size !in func.signature.arity) {
val errorContext = errorContextFrom(metas).also {
it[Property.FUNCTION_NAME] = func.signature.name
it[Property.EXPECTED_ARITY_MIN] = func.signature.arity.first
it[Property.EXPECTED_ARITY_MAX] = func.signature.arity.last
it[Property.ACTUAL_ARITY] = funcArgThunks.size
}
val message = when {
func.signature.arity.first == 1 && func.signature.arity.last == 1 ->
"${func.signature.name} takes a single argument, received: ${funcArgThunks.size}"
func.signature.arity.first == func.signature.arity.last ->
"${func.signature.name} takes exactly ${func.signature.arity.first} arguments, received: ${funcArgThunks.size}"
else ->
"${func.signature.name} takes between ${func.signature.arity.first} and " +
"${func.signature.arity.last} arguments, received: ${funcArgThunks.size}"
}
err(
message,
ErrorCode.EVALUATOR_INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNC_CALL,
errorContext,
internal = false
)
}
fun checkArgumentTypes(signature: FunctionSignature, args: List): Arguments {
fun checkArgumentType(formalStaticType: StaticType, actualArg: ExprValue, position: Int) {
val formalExprValueTypeDomain = formalStaticType.typeDomain
val actualExprValueType = actualArg.type
val actualStaticType = StaticType.fromExprValue(actualArg)
if (!actualStaticType.isSubTypeOf(formalStaticType)) {
errInvalidArgumentType(
signature = signature,
position = position,
expectedTypes = formalExprValueTypeDomain.toList(),
actualType = actualExprValueType
)
}
}
val required = args.take(signature.requiredParameters.size)
val rest = args.drop(signature.requiredParameters.size)
signature.requiredParameters.zip(required).forEachIndexed { idx, (expected, actual) ->
checkArgumentType(expected, actual, idx + 1)
}
return if (signature.optionalParameter != null && rest.isNotEmpty()) {
val opt = rest.last()
checkArgumentType(signature.optionalParameter, opt, required.size + 1)
RequiredWithOptional(required, opt)
} else if (signature.variadicParameter != null) {
rest.forEachIndexed { idx, arg ->
checkArgumentType(signature.variadicParameter.type, arg, required.size + 1 + idx)
}
RequiredWithVariadic(required, rest)
} else {
RequiredArgs(required)
}
}
val computeThunk = when (func.signature.unknownArguments) {
UnknownArguments.PROPAGATE -> thunkFactory.thunkEnvOperands(metas, funcArgThunks) { env, values ->
val checkedArgs = checkArgumentTypes(func.signature, values)
func.call(env.session, checkedArgs)
}
UnknownArguments.PASS_THRU -> thunkFactory.thunkEnv(metas) { env ->
val funcArgValues = funcArgThunks.map { it(env) }
val checkedArgs = checkArgumentTypes(func.signature, funcArgValues)
func.call(env.session, checkedArgs)
}
}
return checkIntegerOverflow(computeThunk, metas)
}
private fun compileLit(expr: PartiqlAst.Expr.Lit, metas: MetaContainer): ThunkEnv {
val value = valueFactory.newFromIonValue(expr.value.toIonValue(valueFactory.ion))
return thunkFactory.thunkEnv(metas) { value }
}
private fun compileMissing(metas: MetaContainer): ThunkEnv =
thunkFactory.thunkEnv(metas) { valueFactory.missingValue }
private fun compileId(expr: PartiqlAst.Expr.Id, metas: MetaContainer): ThunkEnv {
val uniqueNameMeta = metas[UniqueNameMeta.TAG] as? UniqueNameMeta
val fromSourceNames = currentCompilationContext.fromSourceNames
return when (uniqueNameMeta) {
null -> {
val bindingName = BindingName(expr.name.text, expr.case.toBindingCase())
val evalVariableReference = when (compileOptions.undefinedVariable) {
UndefinedVariableBehavior.ERROR ->
thunkFactory.thunkEnv(metas) { env ->
when (val value = env.current[bindingName]) {
null -> {
if (fromSourceNames.any { bindingName.isEquivalentTo(it) }) {
err(
"Variable not in GROUP BY or aggregation function: ${bindingName.name}",
ErrorCode.EVALUATOR_VARIABLE_NOT_INCLUDED_IN_GROUP_BY,
errorContextFrom(metas).also {
it[Property.BINDING_NAME] = bindingName.name
},
internal = false
)
} else {
val (errorCode, hint) = when (expr.case) {
is PartiqlAst.CaseSensitivity.CaseSensitive ->
Pair(
ErrorCode.EVALUATOR_QUOTED_BINDING_DOES_NOT_EXIST,
" $UNBOUND_QUOTED_IDENTIFIER_HINT"
)
is PartiqlAst.CaseSensitivity.CaseInsensitive ->
Pair(ErrorCode.EVALUATOR_BINDING_DOES_NOT_EXIST, "")
}
err(
"No such binding: ${bindingName.name}.$hint",
errorCode,
errorContextFrom(metas).also {
it[Property.BINDING_NAME] = bindingName.name
},
internal = false
)
}
}
else -> value
}
}
UndefinedVariableBehavior.MISSING ->
thunkFactory.thunkEnv(metas) { env ->
env.current[bindingName] ?: valueFactory.missingValue
}
}
when (expr.qualifier) {
is PartiqlAst.ScopeQualifier.Unqualified -> evalVariableReference
is PartiqlAst.ScopeQualifier.LocalsFirst -> thunkFactory.thunkEnv(metas) { env ->
evalVariableReference(env.flipToLocals())
}
}
}
else -> {
val bindingName = BindingName(uniqueNameMeta.uniqueName, BindingCase.SENSITIVE)
thunkFactory.thunkEnv(metas) { env ->
// Unique identifiers are generated by the compiler and should always resolve. If they
// don't for some reason we have a bug.
env.current[bindingName] ?: err(
"Uniquely named binding \"${bindingName.name}\" does not exist for some reason",
ErrorCode.INTERNAL_ERROR,
errorContextFrom(metas),
internal = true
)
}
}
}
}
private fun compileParameter(expr: PartiqlAst.Expr.Parameter, metas: MetaContainer): ThunkEnv {
val ordinal = expr.index.value.toInt()
val index = ordinal - 1
return { env ->
val params = env.session.parameters
if (params.size <= index) {
err(
"Unbound parameter for ordinal: $ordinal",
ErrorCode.EVALUATOR_UNBOUND_PARAMETER,
errorContextFrom(metas).also {
it[Property.EXPECTED_PARAMETER_ORDINAL] = ordinal
it[Property.BOUND_PARAMETER_COUNT] = params.size
},
internal = false
)
}
params[index]
}
}
/**
* Returns a lambda that implements the `IS` operator type check according to the current
* [TypedOpBehavior].
*/
private fun makeIsCheck(
staticType: SingleType,
typedOpParameter: TypedOpParameter,
metas: MetaContainer
): (ExprValue) -> Boolean {
val exprValueType = staticType.runtimeType
// The "simple" type match function only looks at the [ExprValueType] of the [ExprValue]
// and invokes the custom [validationThunk] if one exists.
val simpleTypeMatchFunc = { expValue: ExprValue ->
val isTypeMatch = when (exprValueType) {
// MISSING IS NULL and NULL IS MISSING
ExprValueType.NULL -> expValue.type.isUnknown
else -> expValue.type == exprValueType
}
(isTypeMatch && typedOpParameter.validationThunk?.let { it(expValue) } != false)
}
return when (compileOptions.typedOpBehavior) {
TypedOpBehavior.LEGACY -> simpleTypeMatchFunc
TypedOpBehavior.HONOR_PARAMETERS -> { expValue: ExprValue ->
staticType.allTypes.any {
val matchesStaticType = try {
it.isInstance(expValue)
} catch (e: UnsupportedTypeCheckException) {
err(
e.message!!,
ErrorCode.UNIMPLEMENTED_FEATURE,
errorContextFrom(metas),
internal = true
)
}
when {
!matchesStaticType -> false
else -> when (val validator = typedOpParameter.validationThunk) {
null -> true
else -> validator(expValue)
}
}
}
}
}
}
private fun compileIs(expr: PartiqlAst.Expr.IsType, metas: MetaContainer): ThunkEnv {
val expThunk = compileAstExpr(expr.value)
val typedOpParameter = expr.type.toTypedOpParameter()
if (typedOpParameter.staticType is AnyType) {
return thunkFactory.thunkEnv(metas) { valueFactory.newBoolean(true) }
}
if (compileOptions.typedOpBehavior == TypedOpBehavior.HONOR_PARAMETERS && expr.type is PartiqlAst.Type.FloatType && expr.type.precision != null) {
err(
"FLOAT precision parameter is unsupported",
ErrorCode.SEMANTIC_FLOAT_PRECISION_UNSUPPORTED,
errorContextFrom(expr.type.metas),
internal = false
)
}
val typeMatchFunc = when (val staticType = typedOpParameter.staticType) {
is SingleType -> makeIsCheck(staticType, typedOpParameter, metas)
is AnyOfType -> staticType.types.map { childType ->
when (childType) {
is SingleType -> makeIsCheck(childType, typedOpParameter, metas)
else -> err(
"Union type cannot have ANY or nested AnyOf type for IS",
ErrorCode.SEMANTIC_UNION_TYPE_INVALID,
errorContextFrom(metas),
internal = true
)
}
}.let { typeMatchFuncs ->
{ expValue: ExprValue -> typeMatchFuncs.any { func -> func(expValue) } }
}
is AnyType -> throw IllegalStateException("Unexpected ANY type in IS compilation")
}
return thunkFactory.thunkEnv(metas) { env ->
val expValue = expThunk(env)
typeMatchFunc(expValue).exprValue()
}
}
private fun compileCastHelper(value: PartiqlAst.Expr, asType: PartiqlAst.Type, metas: MetaContainer): ThunkEnv {
val expThunk = compileAstExpr(value)
val typedOpParameter = asType.toTypedOpParameter()
if (typedOpParameter.staticType is AnyType) {
return expThunk
}
if (compileOptions.typedOpBehavior == TypedOpBehavior.HONOR_PARAMETERS && asType is PartiqlAst.Type.FloatType && asType.precision != null) {
err(
"FLOAT precision parameter is unsupported",
ErrorCode.SEMANTIC_FLOAT_PRECISION_UNSUPPORTED,
errorContextFrom(asType.metas),
internal = false
)
}
fun typeOpValidate(
value: ExprValue,
castOutput: ExprValue,
typeName: String,
locationMeta: SourceLocationMeta?
) {
if (typedOpParameter.validationThunk?.let { it(castOutput) } == false) {
val errorContext = PropertyValueMap().also {
it[Property.CAST_FROM] = value.type.toString()
it[Property.CAST_TO] = typeName
}
locationMeta?.let { fillErrorContext(errorContext, it) }
err(
"Validation failure for $asType",
ErrorCode.EVALUATOR_CAST_FAILED,
errorContext,
internal = false
)
}
}
fun singleTypeCastFunc(singleType: SingleType): CastFunc {
val locationMeta = metas.sourceLocationMeta
return { value ->
val castOutput = value.cast(
singleType,
valueFactory,
compileOptions.typedOpBehavior,
locationMeta,
compileOptions.defaultTimezoneOffset
)
typeOpValidate(value, castOutput, singleType.runtimeType.toString(), locationMeta)
castOutput
}
}
fun compileSingleTypeCast(singleType: SingleType): ThunkEnv {
val castFunc = singleTypeCastFunc(singleType)
// We do not use thunkFactory here because we want to explicitly avoid
// the optional evaluation-time type check for CAN_CAST below.
// Can cast needs that returns false if an
// exception is thrown during a normal cast operation.
return { env ->
val valueToCast = expThunk(env)
castFunc(valueToCast)
}
}
fun compileCast(type: StaticType): ThunkEnv = when (type) {
is SingleType -> compileSingleTypeCast(type)
is AnyOfType -> {
val locationMeta = metas.sourceLocationMeta
val castTable = AnyOfCastTable(type, metas, valueFactory, ::singleTypeCastFunc);
// We do not use thunkFactory here because we want to explicitly avoid
// the optional evaluation-time type check for CAN_CAST below.
// note that this would interfere with the error handling for can_cast that returns false if an
// exception is thrown during a normal cast operation.
{ env ->
val sourceValue = expThunk(env)
castTable.cast(sourceValue).also {
// TODO put the right type name here
typeOpValidate(sourceValue, it, "", locationMeta)
}
}
}
is AnyType -> throw IllegalStateException("Unreachable code")
}
return compileCast(typedOpParameter.staticType)
}
private fun compileCast(expr: PartiqlAst.Expr.Cast, metas: MetaContainer): ThunkEnv =
thunkFactory.thunkEnv(metas, compileCastHelper(expr.value, expr.asType, metas))
private fun compileCanCast(expr: PartiqlAst.Expr.CanCast, metas: MetaContainer): ThunkEnv {
val typedOpParameter = expr.asType.toTypedOpParameter()
if (typedOpParameter.staticType is AnyType) {
return thunkFactory.thunkEnv(metas) { valueFactory.newBoolean(true) }
}
val expThunk = compileAstExpr(expr.value)
// TODO consider making this more efficient by not directly delegating to CAST
// TODO consider also making the operand not double evaluated (e.g. having expThunk memoize)
val castThunkEnv = compileCastHelper(expr.value, expr.asType, expr.metas)
return thunkFactory.thunkEnv(metas) { env ->
val sourceValue = expThunk(env)
try {
when {
// NULL/MISSING can cast to anything as themselves
sourceValue.isUnknown() -> valueFactory.newBoolean(true)
else -> {
val castedValue = castThunkEnv(env)
when {
// NULL/MISSING from cast is a permissive way to signal failure
castedValue.isUnknown() -> valueFactory.newBoolean(false)
else -> valueFactory.newBoolean(true)
}
}
}
} catch (e: EvaluationException) {
if (e.internal) {
throw e
}
valueFactory.newBoolean(false)
}
}
}
private fun compileCanLosslessCast(expr: PartiqlAst.Expr.CanLosslessCast, metas: MetaContainer): ThunkEnv {
val typedOpParameter = expr.asType.toTypedOpParameter()
if (typedOpParameter.staticType is AnyType) {
return thunkFactory.thunkEnv(metas) { valueFactory.newBoolean(true) }
}
val expThunk = compileAstExpr(expr.value)
// TODO consider making this more efficient by not directly delegating to CAST
val castThunkEnv = compileCastHelper(expr.value, expr.asType, expr.metas)
return thunkFactory.thunkEnv(metas) { env ->
val sourceValue = expThunk(env)
val sourceType = StaticType.fromExprValue(sourceValue)
fun roundTrip(): ExprValue {
val castedValue = castThunkEnv(env)
val locationMeta = metas.sourceLocationMeta
fun castFunc(singleType: SingleType) =
{ value: ExprValue ->
value.cast(
singleType,
valueFactory,
compileOptions.typedOpBehavior,
locationMeta,
compileOptions.defaultTimezoneOffset
)
}
val roundTripped = when (sourceType) {
is SingleType -> castFunc(sourceType)(castedValue)
is AnyOfType -> {
val castTable = AnyOfCastTable(sourceType, metas, valueFactory, ::castFunc)
castTable.cast(sourceValue)
}
// Should not be possible
is AnyType -> throw IllegalStateException("ANY type is not configured correctly in compiler")
}
val lossless = sourceValue.exprEquals(roundTripped)
return valueFactory.newBoolean(lossless)
}
try {
when (sourceValue.type) {
// NULL can cast to anything as itself
ExprValueType.NULL -> valueFactory.newBoolean(true)
// Short-circuit timestamp -> date roundtrip if precision isn't [Timestamp.Precision.DAY] or
// [Timestamp.Precision.MONTH] or [Timestamp.Precision.YEAR]
ExprValueType.TIMESTAMP -> when (typedOpParameter.staticType) {
StaticType.DATE -> when (sourceValue.timestampValue().precision) {
Timestamp.Precision.DAY, Timestamp.Precision.MONTH, Timestamp.Precision.YEAR -> roundTrip()
else -> valueFactory.newBoolean(false)
}
StaticType.TIME -> valueFactory.newBoolean(false)
else -> roundTrip()
}
// For all other cases, attempt a round-trip of the value through the source and dest types
else -> roundTrip()
}
} catch (e: EvaluationException) {
if (e.internal) {
throw e
}
valueFactory.newBoolean(false)
}
}
}
private fun compileSimpleCase(expr: PartiqlAst.Expr.SimpleCase, metas: MetaContainer): ThunkEnv {
val valueThunk = compileAstExpr(expr.expr)
val branchThunks = expr.cases.pairs.map { Pair(compileAstExpr(it.first), compileAstExpr(it.second)) }
val elseThunk = when (expr.default) {
null -> thunkFactory.thunkEnv(metas) { valueFactory.nullValue }
else -> compileAstExpr(expr.default)
}
return thunkFactory.thunkEnv(metas) thunk@{ env ->
val caseValue = valueThunk(env)
// if the case value is unknown then we can short-circuit to the elseThunk directly
when {
caseValue.isUnknown() -> elseThunk(env)
else -> {
branchThunks.forEach { bt ->
val branchValue = bt.first(env)
// Just skip any branch values that are unknown, which we consider the same as false here.
when {
branchValue.isUnknown() -> { /* intentionally blank */
}
else -> {
if (caseValue.exprEquals(branchValue)) {
return@thunk bt.second(env)
}
}
}
}
}
}
elseThunk(env)
}
}
private fun compileSearchedCase(expr: PartiqlAst.Expr.SearchedCase, metas: MetaContainer): ThunkEnv {
val branchThunks = expr.cases.pairs.map { compileAstExpr(it.first) to compileAstExpr(it.second) }
val elseThunk = when (expr.default) {
null -> thunkFactory.thunkEnv(metas) { valueFactory.nullValue }
else -> compileAstExpr(expr.default)
}
return when (compileOptions.typingMode) {
TypingMode.LEGACY -> thunkFactory.thunkEnv(metas) thunk@{ env ->
branchThunks.forEach { bt ->
val conditionValue = bt.first(env)
// Any unknown value is considered the same as false.
// Note that .booleanValue() here will throw an EvaluationException if
// the data type is not boolean.
// TODO: .booleanValue does not have access to metas, so the EvaluationException is reported to be
// at the line & column of the CASE statement, not the predicate, unfortunately.
if (conditionValue.isNotUnknown() && conditionValue.booleanValue()) {
return@thunk bt.second(env)
}
}
elseThunk(env)
}
// Permissive mode propagates data type mismatches as MISSING, which is
// equivalent to false for searched CASE predicates. To simplify this,
// all we really need to do is consider any non-boolean result from the
// predicate to be false.
TypingMode.PERMISSIVE -> thunkFactory.thunkEnv(metas) thunk@{ env ->
branchThunks.forEach { bt ->
val conditionValue = bt.first(env)
if (conditionValue.type == ExprValueType.BOOL && conditionValue.booleanValue()) {
return@thunk bt.second(env)
}
}
elseThunk(env)
}
}
}
private fun compileStruct(expr: PartiqlAst.Expr.Struct, metas: MetaContainer): ThunkEnv {
class StructFieldThunks(val nameThunk: ThunkEnv, val valueThunk: ThunkEnv)
val fieldThunks = expr.fields.map {
StructFieldThunks(compileAstExpr(it.first), compileAstExpr(it.second))
}
return when (compileOptions.typingMode) {
TypingMode.LEGACY -> thunkFactory.thunkEnv(metas) { env ->
val seq = fieldThunks.map {
val nameValue = it.nameThunk(env)
if (!nameValue.type.isText) {
// Evaluation time error where variable reference might be evaluated to non-text struct field.
err(
"Found struct field key to be of type ${nameValue.type}",
ErrorCode.EVALUATOR_NON_TEXT_STRUCT_FIELD_KEY,
errorContextFrom(metas.sourceLocationMeta).also { pvm ->
pvm[Property.ACTUAL_TYPE] = nameValue.type.toString()
},
internal = false
)
}
it.valueThunk(env).namedValue(nameValue)
}.asSequence()
createStructExprValue(seq, StructOrdering.ORDERED)
}
TypingMode.PERMISSIVE -> thunkFactory.thunkEnv(metas) { env ->
val seq = fieldThunks.map { it.valueThunk(env).namedValue(it.nameThunk(env)) }.asSequence()
.filter {
// fields with non-text keys are filtered out of the struct
val keyType = it.name?.type
keyType != null && keyType.isText
}
createStructExprValue(seq, StructOrdering.ORDERED)
}
}
}
private fun compileSeq(seqType: ExprValueType, itemExprs: List, metas: MetaContainer): ThunkEnv {
require(seqType.isSequence) { "seqType must be a sequence!" }
val itemThunks = compileAstExprs(itemExprs)
val makeItemThunkSequence = when (seqType) {
ExprValueType.BAG -> { env: Environment ->
itemThunks.asSequence().map { itemThunk ->
// call to unnamedValue() makes sure we don't expose any underlying value name/ordinal
itemThunk(env).unnamedValue()
}
}
else -> { env: Environment ->
itemThunks.asSequence().mapIndexed { i, itemThunk -> itemThunk(env).namedValue(i.exprValue()) }
}
}
return thunkFactory.thunkEnv(metas) { env ->
// todo: use valueFactory.newSequence() instead.
SequenceExprValue(
valueFactory.ion,
seqType,
makeItemThunkSequence(env)
)
}
}
private fun compileBagOp(node: PartiqlAst.Expr.BagOp, metas: MetaContainer): ThunkEnv {
val lhs = compileAstExpr(node.operands[0])
val rhs = compileAstExpr(node.operands[1])
val op = ExprValueBagOp.create(node.op, metas)
return thunkFactory.thunkEnv(metas) { env ->
val l = lhs(env)
val r = rhs(env)
val result = when (node.quantifier) {
is PartiqlAst.SetQuantifier.All -> op.eval(l, r)
is PartiqlAst.SetQuantifier.Distinct -> op.eval(l, r).distinct()
}
valueFactory.newBag(result)
}
}
private fun evalLimit(limitThunk: ThunkEnv, env: Environment, limitLocationMeta: SourceLocationMeta?): Long {
val limitExprValue = limitThunk(env)
if (limitExprValue.type != ExprValueType.INT) {
err(
"LIMIT value was not an integer",
ErrorCode.EVALUATOR_NON_INT_LIMIT_VALUE,
errorContextFrom(limitLocationMeta).also {
it[Property.ACTUAL_TYPE] = limitExprValue.type.toString()
},
internal = false
)
}
val originalLimitValue = limitExprValue.numberValue()
val limitValue = originalLimitValue.toLong()
if (originalLimitValue != limitValue as Number) { // Make sure `Number.toLong()` is a lossless transformation
err(
"Integer exceeds Long.MAX_VALUE provided as LIMIT value",
ErrorCode.INTERNAL_ERROR,
errorContextFrom(limitLocationMeta),
internal = true
)
}
if (limitValue < 0) {
err(
"negative LIMIT",
ErrorCode.EVALUATOR_NEGATIVE_LIMIT,
errorContextFrom(limitLocationMeta),
internal = false
)
}
// we can't use the Kotlin's Sequence.take(n) for this since it accepts only an integer.
// this references [Sequence.take(count: Long): Sequence] defined in [org.partiql.util].
return limitValue
}
private fun evalOffset(offsetThunk: ThunkEnv, env: Environment, offsetLocationMeta: SourceLocationMeta?): Long {
val offsetExprValue = offsetThunk(env)
if (offsetExprValue.type != ExprValueType.INT) {
err(
"OFFSET value was not an integer",
ErrorCode.EVALUATOR_NON_INT_OFFSET_VALUE,
errorContextFrom(offsetLocationMeta).also {
it[Property.ACTUAL_TYPE] = offsetExprValue.type.toString()
},
internal = false
)
}
val originalOffsetValue = offsetExprValue.numberValue()
val offsetValue = originalOffsetValue.toLong()
if (originalOffsetValue != offsetValue as Number) { // Make sure `Number.toLong()` is a lossless transformation
err(
"Integer exceeds Long.MAX_VALUE provided as OFFSET value",
ErrorCode.INTERNAL_ERROR,
errorContextFrom(offsetLocationMeta),
internal = true
)
}
if (offsetValue < 0) {
err(
"negative OFFSET",
ErrorCode.EVALUATOR_NEGATIVE_OFFSET,
errorContextFrom(offsetLocationMeta),
internal = false
)
}
return offsetValue
}
private fun compileSelect(selectExpr: PartiqlAst.Expr.Select, metas: MetaContainer): ThunkEnv {
// Get all the FROM source aliases and LET bindings for binding error checks
val fold = object : PartiqlAst.VisitorFold>() {
/** Store all the visited FROM source aliases in the accumulator */
override fun visitFromSourceScan(node: PartiqlAst.FromSource.Scan, accumulator: Set): Set {
val aliases = listOfNotNull(node.asAlias?.text, node.atAlias?.text, node.byAlias?.text)
return accumulator + aliases.toSet()
}
override fun visitLetBinding(node: PartiqlAst.LetBinding, accumulator: Set): Set {
val aliases = listOfNotNull(node.name.text)
return accumulator + aliases
}
/** Prevents visitor from recursing into nested select statements */
override fun walkExprSelect(node: PartiqlAst.Expr.Select, accumulator: Set): Set {
return accumulator
}
}
val allFromSourceAliases = fold.walkFromSource(selectExpr.from, emptySet())
.union(selectExpr.fromLet?.let { fold.walkLet(selectExpr.fromLet, emptySet()) } ?: emptySet())
return nestCompilationContext(ExpressionContext.NORMAL, emptySet()) {
val fromSourceThunks = compileFromSources(selectExpr.from)
val letSourceThunks = selectExpr.fromLet?.let { compileLetSources(it) }
val sourceThunks = compileQueryWithoutProjection(selectExpr, fromSourceThunks, letSourceThunks)
val orderByThunk = selectExpr.order?.let { compileOrderByExpression(selectExpr.order.sortSpecs) }
val orderByLocationMeta = selectExpr.order?.metas?.sourceLocation
val offsetThunk = selectExpr.offset?.let { compileAstExpr(it) }
val offsetLocationMeta = selectExpr.offset?.metas?.sourceLocation
val limitThunk = selectExpr.limit?.let { compileAstExpr(it) }
val limitLocationMeta = selectExpr.limit?.metas?.sourceLocation
fun rowsWithOffsetAndLimit(rows: Sequence, env: Environment): Sequence {
val rowsWithOffset = when (offsetThunk) {
null -> rows
else -> rows.drop(evalOffset(offsetThunk, env, offsetLocationMeta))
}
return when (limitThunk) {
null -> rowsWithOffset
else -> rowsWithOffset.take(evalLimit(limitThunk, env, limitLocationMeta))
}
}
// Returns a thunk that invokes [sourceThunks], and invokes [projectionThunk] to perform the projection.
fun getQueryThunk(selectProjectionThunk: ThunkEnvValue>): ThunkEnv {
val groupByItems = selectExpr.group?.keyList?.keys ?: listOf()
val groupAsName = selectExpr.group?.groupAsAlias
val aggregateListMeta = metas[AggregateCallSiteListMeta.TAG] as AggregateCallSiteListMeta?
val hasAggregateCallSites = aggregateListMeta?.aggregateCallSites?.any() ?: false
val queryThunk = when {
groupByItems.isEmpty() && !hasAggregateCallSites ->
// Grouping is not needed -- simply project the results from the FROM clause directly.
thunkFactory.thunkEnv(metas) { env ->
val sourcedRows = sourceThunks(env)
val orderedRows = when (orderByThunk) {
null -> sourcedRows
else -> evalOrderBy(sourcedRows, orderByThunk, orderByLocationMeta)
}
val projectedRows = orderedRows.map { (joinedValues, projectEnv) ->
selectProjectionThunk(projectEnv, joinedValues)
}
val quantifiedRows = when (selectExpr.setq ?: PartiqlAst.SetQuantifier.All()) {
// wrap the ExprValue to use ExprValue.equals as the equality
is PartiqlAst.SetQuantifier.Distinct -> projectedRows.distinct()
is PartiqlAst.SetQuantifier.All -> projectedRows
}.let { rowsWithOffsetAndLimit(it, env) }
// if order by is specified, return list otherwise bag
when (orderByThunk) {
null -> valueFactory.newBag(
quantifiedRows.map {
// TODO make this expose the ordinal for ordered sequences
// make sure we don't expose the underlying value's name out of a SELECT
it.unnamedValue()
}
)
else -> valueFactory.newList(quantifiedRows.map { it.unnamedValue() })
}
}
else -> {
// Grouping is needed
class CompiledAggregate(val factory: ExprAggregatorFactory, val argThunk: ThunkEnv)
// These aggregate call sites are collected in [AggregateSupportVisitorTransform].
val compiledAggregates = aggregateListMeta?.aggregateCallSites?.map { it ->
val funcName = it.funcName.text
CompiledAggregate(
factory = getAggregatorFactory(
funcName,
it.setq,
it.metas
),
argThunk = compileAstExpr(it.arg)
)
}
// This closure will be invoked to create and initialize a [RegisterBank] for new [Group]s.
val createRegisterBank: () -> RegisterBank = when (aggregateListMeta) {
// If there are no aggregates, create an empty register bank
null -> { -> // -> here forces this block to be a lambda
RegisterBank(0)
}
else -> { ->
RegisterBank(aggregateListMeta.aggregateCallSites.size).apply {
// set up aggregate registers
compiledAggregates?.forEachIndexed { index, ca ->
set(index, ca.factory.create())
}
}
}
}
when {
groupByItems.isEmpty() -> { // There are aggregates but no group by items
// Create a closure that groups all the rows in the FROM source into a single group.
thunkFactory.thunkEnv(metas) { env ->
// Evaluate the FROM clause
val orderedRows = when (orderByThunk) {
null -> sourceThunks(env)
else -> evalOrderBy(sourceThunks(env), orderByThunk, orderByLocationMeta)
}
val fromProductions: Sequence =
rowsWithOffsetAndLimit(orderedRows, env)
val registers = createRegisterBank()
// note: the group key can be anything here because we only ever have a single
// group when aggregates are used without GROUP BY expression
val syntheticGroup = Group(valueFactory.nullValue, registers)
// iterate over the values from the FROM clause and populate our
// aggregate register values.
fromProductions.forEach { fromProduction ->
compiledAggregates?.forEachIndexed { index, ca ->
registers[index].aggregator.next(ca.argThunk(fromProduction.env))
}
}
// generate the final group projection
val groupResult = selectProjectionThunk(
env.copy(currentGroup = syntheticGroup),
listOf(syntheticGroup.key)
)
// if order by is specified, return list otherwise bag
when (orderByThunk) {
null -> valueFactory.newBag(listOf(groupResult).asSequence())
else -> valueFactory.newList(listOf(groupResult).asSequence())
}
}
}
else -> {
// There are GROUP BY expressions and possibly aggregates. (The most complex scenario.)
val compiledGroupByItems = compileGroupByExpressions(groupByItems)
val groupKeyThunk = compileGroupKeyThunk(compiledGroupByItems, metas)
// Memoize a [BindingName] and an [ExprValue] containing the name of each FromSource
// otherwise we would be re-creating them for every row.
val fromSourceBindingNames = fromSourceThunks.map {
FromSourceBindingNamePair(
BindingName(it.alias.asName, BindingCase.SENSITIVE),
it.alias.asName.exprValue()
)
}
val havingThunk = selectExpr.having?.let { compileAstExpr(it) }
val filterHavingAndProject: (Environment, Group) -> ExprValue? =
createFilterHavingAndProjectClosure(havingThunk, selectProjectionThunk)
val getGroupEnv: (Environment, Group) -> Environment =
createGetGroupEnvClosure(groupAsName)
thunkFactory.thunkEnv(metas) { env ->
// Execute the FROM clause
val fromProductions: Sequence = sourceThunks(env)
// For each "row" in the output of the FROM clause
fromProductions.forEach { fromProduction ->
// Determine the group key for this value
val groupKey = groupKeyThunk(fromProduction.env)
// look up existing group using group key (this is slow)
// We can't do that yet because ExprValue does not implement .hashCode()
// and .equals()
val group: Group = env.groups.getOrPut(groupKey) {
// An existing group was not found so create a new one
Group(groupKey, createRegisterBank())
}
compiledAggregates!!.forEachIndexed { index, ca ->
group.registers[index].aggregator.next(ca.argThunk(fromProduction.env))
}
groupAsName.run {
val seq = fromSourceBindingNames.asSequence().map { pair ->
(
fromProduction.env.current[pair.bindingName] ?: errNoContext(
"Could not resolve from source binding name during group as variable mapping",
errorCode = ErrorCode.INTERNAL_ERROR,
internal = true
)
).namedValue(pair.nameExprValue)
}.asSequence()
group.groupValues.add(createStructExprValue(seq, StructOrdering.UNORDERED))
}
}
val groupByEnvValuePairs = env.groups.mapNotNull { g -> getGroupEnv(env, g.value) to g.value }.asSequence()
val orderedGroupEnvPairs = when (orderByThunk) {
null -> groupByEnvValuePairs
else -> evalOrderBy(groupByEnvValuePairs, orderByThunk, orderByLocationMeta)
}
// generate the final group by projection
val projectedRows = orderedGroupEnvPairs.mapNotNull { (groupByEnv, groupValue) ->
filterHavingAndProject(groupByEnv, groupValue)
}.asSequence().let { rowsWithOffsetAndLimit(it, env) }
// if order by is specified, return list otherwise bag
when (orderByThunk) {
null -> valueFactory.newBag(projectedRows)
else -> valueFactory.newList(projectedRows)
}
}
}
}
}
} // do normal map/filter
return thunkFactory.thunkEnv(metas) { env ->
queryThunk(env.nestQuery())
}
} // end of getQueryThunk(...)
when (val project = selectExpr.project) {
is PartiqlAst.Projection.ProjectValue -> {
nestCompilationContext(ExpressionContext.NORMAL, allFromSourceAliases) {
val valueThunk = compileAstExpr(project.value)
getQueryThunk(
thunkFactory.thunkEnvValueList(project.metas) { env, _ ->
valueThunk(
env
)
}
)
}
}
is PartiqlAst.Projection.ProjectPivot -> {
val asExpr = project.value
val atExpr = project.key
nestCompilationContext(ExpressionContext.NORMAL, allFromSourceAliases) {
val asThunk = compileAstExpr(asExpr)
val atThunk = compileAstExpr(atExpr)
thunkFactory.thunkEnv(metas) { env ->
val sourceValue = rowsWithOffsetAndLimit(sourceThunks(env).asSequence(), env)
val seq = sourceValue
.map { (_, env) -> Pair(asThunk(env), atThunk(env)) }
.filter { (name, _) -> name.type.isText }
.map { (name, value) -> value.namedValue(name) }
createStructExprValue(seq, StructOrdering.UNORDERED)
}
}
}
is PartiqlAst.Projection.ProjectList -> {
val items = project.projectItems
nestCompilationContext(ExpressionContext.SELECT_LIST, allFromSourceAliases) {
val projectionThunk: ThunkEnvValue> =
when {
items.filterIsInstance().any() -> {
errNoContext(
"Encountered a PartiqlAst.Projection.ProjectStar--did SelectStarVisitorTransform execute?",
errorCode = ErrorCode.INTERNAL_ERROR,
internal = true
)
}
else -> {
val projectionElements =
compileSelectListToProjectionElements(project)
val ordering = if (items.none { it is PartiqlAst.ProjectItem.ProjectAll })
StructOrdering.ORDERED
else
StructOrdering.UNORDERED
thunkFactory.thunkEnvValueList(project.metas) { env, _ ->
val columns = mutableListOf()
for (element in projectionElements) {
when (element) {
is SingleProjectionElement -> {
val eval = element.thunk(env)
columns.add(eval.namedValue(element.name))
}
is MultipleProjectionElement -> {
for (projThunk in element.thunks) {
val value = projThunk(env)
if (value.type == ExprValueType.MISSING) continue
val children = value.asSequence()
if (!children.any() || value.type.isSequence) {
val name = syntheticColumnName(columns.size).exprValue()
columns.add(value.namedValue(name))
} else {
val valuesToProject =
when (compileOptions.projectionIteration) {
ProjectionIterationBehavior.FILTER_MISSING -> {
value.filter { it.type != ExprValueType.MISSING }
}
ProjectionIterationBehavior.UNFILTERED -> value
}
for (childValue in valuesToProject) {
val namedFacet = childValue.asFacet(Named::class.java)
val name = namedFacet?.name
?: syntheticColumnName(columns.size).exprValue()
columns.add(childValue.namedValue(name))
}
}
}
}
}
}
createStructExprValue(columns.asSequence(), ordering)
}
}
}
getQueryThunk(projectionThunk)
} // nestCompilationContext(ExpressionContext.SELECT_LIST)
} // is SelectProjectionList
is PartiqlAst.Projection.ProjectStar -> error("Internal Error: PartiqlAst.Projection.ProjectStar can only be wrapped in PartiqlAst.Projection.ProjectList")
}
}
}
private fun compileGroupByExpressions(groupByItems: List): List =
groupByItems.map {
val alias = it.asAlias
?: errNoContext(
"GroupByItem.asName was not specified",
errorCode = ErrorCode.INTERNAL_ERROR,
internal = true
)
val uniqueName =
(alias.metas.find(UniqueNameMeta.TAG) as UniqueNameMeta?)?.uniqueName
CompiledGroupByItem(alias.text.exprValue(), uniqueName, compileAstExpr(it.expr))
}
/**
* Create a thunk that uses the compiled GROUP BY expressions to create the group key.
*/
private fun compileGroupKeyThunk(compiledGroupByItems: List, selectMetas: MetaContainer) =
thunkFactory.thunkEnv(selectMetas) { env ->
val uniqueNames = HashMap(compiledGroupByItems.size, 1f)
val keyValues = compiledGroupByItems.map { cgbi ->
val value = cgbi.thunk(env).namedValue(cgbi.alias)
if (cgbi.uniqueId != null) {
uniqueNames[cgbi.uniqueId] = value
}
value
}
GroupKeyExprValue(valueFactory.ion, keyValues.asSequence(), uniqueNames)
}
private fun compileOrderByExpression(sortSpecs: List): List =
sortSpecs.map {
val comparator = when (it.orderingSpec ?: PartiqlAst.OrderingSpec.Asc()) {
is PartiqlAst.OrderingSpec.Asc ->
when (it.nullsSpec) {
is PartiqlAst.NullsSpec.NullsFirst -> NaturalExprValueComparators.NULLS_FIRST_ASC
is PartiqlAst.NullsSpec.NullsLast -> NaturalExprValueComparators.NULLS_LAST_ASC
else -> NaturalExprValueComparators.NULLS_LAST_ASC
}
is PartiqlAst.OrderingSpec.Desc ->
when (it.nullsSpec) {
is PartiqlAst.NullsSpec.NullsFirst -> NaturalExprValueComparators.NULLS_FIRST_DESC
is PartiqlAst.NullsSpec.NullsLast -> NaturalExprValueComparators.NULLS_LAST_DESC
else -> NaturalExprValueComparators.NULLS_FIRST_DESC
}
}
CompiledOrderByItem(comparator, compileAstExpr(it.expr))
}
private fun evalOrderBy(
rows: Sequence,
orderByItems: List,
offsetLocationMeta: SourceLocationMeta?
): Sequence {
val initialComparator: Comparator? = null
val resultComparator = orderByItems.interruptibleFold(initialComparator) { intermediateComparator, orderByItem ->
if (intermediateComparator == null) {
return@interruptibleFold compareBy(orderByItem.comparator) { row ->
val env = resolveEnvironment(row, offsetLocationMeta)
orderByItem.thunk(env)
}
}
return@interruptibleFold intermediateComparator.thenBy(orderByItem.comparator) { row ->
val env = resolveEnvironment(row, offsetLocationMeta)
orderByItem.thunk(env)
}
} ?: errNoContext(
"Order BY comparator cannot be null",
ErrorCode.EVALUATOR_ORDER_BY_NULL_COMPARATOR,
internal = true
)
return rows.sortedWith(resultComparator)
}
private fun resolveEnvironment(envWrapper: T, offsetLocationMeta: SourceLocationMeta?): Environment {
return when (envWrapper) {
is FromProduction -> envWrapper.env
is Pair<*, *> -> {
if (envWrapper.first is Environment) {
envWrapper.first as Environment
} else if (envWrapper.second is Environment) {
envWrapper.second as Environment
} else {
err(
"Environment cannot be resolved from pair",
ErrorCode.EVALUATOR_ENVIRONMENT_CANNOT_BE_RESOLVED,
errorContextFrom(offsetLocationMeta),
internal = true
)
}
}
else -> err(
"Environment cannot be resolved",
ErrorCode.EVALUATOR_ENVIRONMENT_CANNOT_BE_RESOLVED,
errorContextFrom(offsetLocationMeta),
internal = true
)
}
}
/**
* Returns a closure which creates an [Environment] for the specified [Group].
* If a GROUP AS name was specified, also nests that [Environment] in another that
* has a binding for the GROUP AS name.
*/
private fun createGetGroupEnvClosure(groupAsName: SymbolPrimitive?): (Environment, Group) -> Environment =
when {
groupAsName != null -> { groupByEnv, currentGroup ->
val groupAsBindings = Bindings.buildLazyBindings {
addBinding(groupAsName.text) {
valueFactory.newBag(currentGroup.groupValues.asSequence())
}
}
groupByEnv.nest(currentGroup.key.bindings, newGroup = currentGroup)
.nest(groupAsBindings)
}
else -> { groupByEnv, currentGroup ->
groupByEnv.nest(currentGroup.key.bindings, newGroup = currentGroup)
}
}
/**
* Returns a closure which performs the final projection and returns the
* result. If a HAVING clause was included, a different closure is returned
* that evaluates the HAVING clause and performs filtering.
*/
private fun createFilterHavingAndProjectClosure(
havingThunk: ThunkEnv?,
selectProjectionThunk: ThunkEnvValue>
): (Environment, Group) -> ExprValue? =
when {
havingThunk != null -> { groupByEnv, currentGroup ->
// Create a closure that executes the HAVING clause and returns null if the
// HAVING criteria is not met
val havingClauseResult = havingThunk(groupByEnv)
if (havingClauseResult.isNotUnknown() && havingClauseResult.booleanValue()) {
selectProjectionThunk(groupByEnv, listOf(currentGroup.key))
} else {
null
}
}
else -> { groupByEnv, currentGroup ->
// Create a closure that simply performs the final projection and
// returns the result.
selectProjectionThunk(groupByEnv, listOf(currentGroup.key))
}
}
private fun compileCallAgg(expr: PartiqlAst.Expr.CallAgg, metas: MetaContainer): ThunkEnv {
if (metas.containsKey(IsCountStarMeta.TAG) && currentCompilationContext.expressionContext != ExpressionContext.SELECT_LIST) {
err(
"COUNT(*) is not allowed in this context",
ErrorCode.EVALUATOR_COUNT_START_NOT_ALLOWED,
errorContextFrom(metas),
internal = false
)
}
val aggFactory = getAggregatorFactory(expr.funcName.text.toLowerCase(), expr.setq, metas)
val argThunk = nestCompilationContext(ExpressionContext.AGG_ARG, emptySet()) {
compileAstExpr(expr.arg)
}
return when (currentCompilationContext.expressionContext) {
ExpressionContext.AGG_ARG -> {
err(
"The arguments of an aggregate function cannot contain aggregate functions",
ErrorCode.EVALUATOR_INVALID_ARGUMENTS_FOR_AGG_FUNCTION,
errorContextFrom(metas),
internal = false
)
}
ExpressionContext.NORMAL ->
thunkFactory.thunkEnv(metas) { env ->
val aggregator = aggFactory.create()
val argValue = argThunk(env)
argValue.forEach {
aggregator.next(it)
}
aggregator.compute()
}
ExpressionContext.SELECT_LIST -> {
val registerIdMeta = metas[AggregateRegisterIdMeta.TAG] as AggregateRegisterIdMeta
val registerId = registerIdMeta.registerId
thunkFactory.thunkEnv(metas) { env ->
// Note: env.currentGroup must be set by caller.
val registers = env.currentGroup?.registers
?: err(
"No current group or current group has no registers",
ErrorCode.INTERNAL_ERROR,
errorContextFrom(metas),
internal = true
)
registers[registerId].aggregator.compute()
}
}
}
}
private fun getAggregatorFactory(
funcName: String,
setQuantifier: PartiqlAst.SetQuantifier,
metas: MetaContainer
): ExprAggregatorFactory {
val key = funcName.toLowerCase() to setQuantifier
return builtinAggregates[key] ?: err(
"No such function: $funcName",
ErrorCode.EVALUATOR_NO_SUCH_FUNCTION,
errorContextFrom(metas).also { it[Property.FUNCTION_NAME] = funcName },
internal = false
)
}
private fun compileFromSources(
fromSource: PartiqlAst.FromSource,
sources: MutableList = ArrayList(),
joinExpansion: JoinExpansion = JoinExpansion.INNER,
conditionThunk: ThunkEnv? = null
): List {
val metas = fromSource.metas
fun addAliases(
thunk: ThunkEnv,
asName: String?,
atName: String?,
byName: String?,
metas: MetaContainer
) {
sources.add(
CompiledFromSource(
alias = Alias(
asName = asName ?: err(
"PartiqlAst.FromSource.Scan.variables.asName was null", ErrorCode.INTERNAL_ERROR,
errorContextFrom(metas), internal = true
),
atName = atName,
byName = byName
),
thunk = thunk,
joinExpansion = joinExpansion,
filter = conditionThunk
)
)
}
when (fromSource) {
is PartiqlAst.FromSource.Scan -> {
val thunk = compileAstExpr(fromSource.expr)
addAliases(
thunk,
fromSource.asAlias?.text,
fromSource.atAlias?.text,
fromSource.byAlias?.text,
fromSource.metas
)
}
is PartiqlAst.FromSource.Unpivot -> {
val exprThunk = compileAstExpr(fromSource.expr)
val thunk = thunkFactory.thunkEnv(metas) { env -> exprThunk(env).unpivot() }
addAliases(
thunk,
fromSource.asAlias?.text,
fromSource.atAlias?.text,
fromSource.byAlias?.text,
fromSource.metas
)
}
is PartiqlAst.FromSource.Join -> {
val joinOp = fromSource.type
val left = fromSource.left
val right = fromSource.right
val condition = fromSource.predicate
val leftSources = compileFromSources(left)
sources.addAll(leftSources)
val joinExpansionInner = when (joinOp) {
is PartiqlAst.JoinType.Inner -> JoinExpansion.INNER
is PartiqlAst.JoinType.Left -> JoinExpansion.OUTER
is PartiqlAst.JoinType.Right,
is PartiqlAst.JoinType.Full ->
err(
"RIGHT and FULL JOIN not supported",
ErrorCode.EVALUATOR_FEATURE_NOT_SUPPORTED_YET,
errorContextFrom(metas).also {
it[Property.FEATURE_NAME] = "RIGHT and FULL JOIN"
},
internal = false
)
}
val conditionThunkInner = condition?.let { compileAstExpr(it) }
?: compileAstExpr(PartiqlAst.build { lit(ionBool(true)) })
// The right side of a FromSourceJoin can never be another FromSourceJoin -- the parser will currently
// never construct an AST in that fashion.
// TODO: do we need to modify the AST to include this enforce this as a constraint?
// How to modify the ast so it properly constrains the right side
// of FromSourceJoin?
// This means that the call to compileFromSources below can only ever yield one
// additional FromClauseSource, which must contain the JoinExpansion and join
// condition of this current node, so here we pass those down to the next level
// of recursion.
compileFromSources(right, sources, joinExpansionInner, conditionThunkInner)
}
}
return sources
}
private fun compileLetSources(letSource: PartiqlAst.Let): List =
letSource.letBindings.map {
CompiledLetSource(name = it.name.text, thunk = compileAstExpr(it.expr))
}
/**
* Compiles the clauses of the SELECT or PIVOT into a thunk that does not generate
* the final projection.
*/
private fun compileQueryWithoutProjection(
ast: PartiqlAst.Expr.Select,
compiledSources: List,
compiledLetSources: List?
): (Environment) -> Sequence {
val localsBinder = compiledSources.map { it.alias }.localsBinder(valueFactory.missingValue)
val whereThunk = ast.where?.let { compileAstExpr(it) }
return { rootEnv ->
val fromEnv = rootEnv.flipToGlobalsFirst()
// compute the join over the data sources
var seq = compiledSources
.foldLeftProduct({ env: Environment -> env }) { bindEnv: (Environment) -> Environment, source: CompiledFromSource ->
fun correlatedBind(value: ExprValue): Pair<(Environment) -> Environment, ExprValue> {
// add the correlated binding environment thunk
val alias = source.alias
val nextBindEnv = { env: Environment ->
val childEnv = bindEnv(env)
childEnv.nest(
Bindings.buildLazyBindings {
addBinding(alias.asName) { value }
if (alias.atName != null)
addBinding(alias.atName) {
value.name ?: valueFactory.missingValue
}
if (alias.byName != null)
addBinding(alias.byName) {
value.address ?: valueFactory.missingValue
}
},
Environment.CurrentMode.GLOBALS_THEN_LOCALS
)
}
return Pair(nextBindEnv, value)
}
var iter = source.thunk(bindEnv(fromEnv))
.rangeOver()
.asSequence()
.map { correlatedBind(it) }
.iterator()
val filter = source.filter
if (filter != null) {
// evaluate the ON-clause (before calculating the outer join NULL)
// TODO add facet for ExprValue to directly evaluate theta-joins
iter = iter
.asSequence()
.filter { (bindEnv: (Environment) -> Environment, _) ->
// make sure we operate with lexical scoping
val filterEnv = bindEnv(rootEnv).flipToLocals()
val filterResult = filter(filterEnv)
if (filterResult.isUnknown()) {
false
} else {
filterResult.booleanValue()
}
}
.iterator()
}
if (!iter.hasNext()) {
iter = when (source.joinExpansion) {
JoinExpansion.OUTER -> listOf(correlatedBind(valueFactory.nullValue)).iterator()
JoinExpansion.INNER -> iter
}
}
iter
}
.asSequence()
.map { joinedValues ->
// bind the joined value to the bindings for the filter/project
FromProduction(joinedValues, fromEnv.nest(localsBinder.bindLocals(joinedValues)))
}
// Nest LET bindings in the FROM environment
if (compiledLetSources != null) {
seq = seq.map { fromProduction ->
val parentEnv = fromProduction.env
val letEnv: Environment = compiledLetSources.fold(parentEnv) { accEnvironment, curLetSource ->
val letValue = curLetSource.thunk(accEnvironment)
val binding = Bindings.over { bindingName ->
when {
bindingName.isEquivalentTo(curLetSource.name) -> letValue
else -> null
}
}
accEnvironment.nest(newLocals = binding)
}
fromProduction.copy(env = letEnv)
}
}
if (whereThunk != null) {
seq = seq.filter { (_, env) ->
val whereClauseResult = whereThunk(env)
when (whereClauseResult.isUnknown()) {
true -> false
false -> whereClauseResult.booleanValue()
}
}
}
seq
}
}
private fun compileSelectListToProjectionElements(
selectList: PartiqlAst.Projection.ProjectList
): List =
selectList.projectItems.mapIndexed { idx, it ->
when (it) {
is PartiqlAst.ProjectItem.ProjectExpr -> {
val alias = it.asAlias?.text ?: it.expr.extractColumnAlias(idx)
val thunk = compileAstExpr(it.expr)
SingleProjectionElement(valueFactory.newString(alias), thunk)
}
is PartiqlAst.ProjectItem.ProjectAll -> {
MultipleProjectionElement(listOf(compileAstExpr(it.expr)))
}
}
}
private fun compilePath(expr: PartiqlAst.Expr.Path, metas: MetaContainer): ThunkEnv {
val rootThunk = compileAstExpr(expr.root)
val remainingComponents = LinkedList()
expr.steps.forEach { remainingComponents.addLast(it) }
val componentThunk = compilePathComponents(remainingComponents, metas)
return thunkFactory.thunkEnv(metas) { env ->
val rootValue = rootThunk(env)
componentThunk(env, rootValue)
}
}
private fun compilePathComponents(
remainingComponents: LinkedList,
pathMetas: MetaContainer
): ThunkEnvValue {
val componentThunks = ArrayList>()
while (!remainingComponents.isEmpty()) {
val pathComponent = remainingComponents.removeFirst()
val componentMetas = pathComponent.metas
componentThunks.add(
when (pathComponent) {
is PartiqlAst.PathStep.PathExpr -> {
val indexExpr = pathComponent.index
val caseSensitivity = pathComponent.case
when {
// If indexExpr is a literal string, there is no need to evaluate it--just compile a
// thunk that directly returns a bound value
indexExpr is PartiqlAst.Expr.Lit && indexExpr.value.toIonValue(valueFactory.ion) is IonString -> {
val lookupName = BindingName(
indexExpr.value.toIonValue(valueFactory.ion).stringValue()!!,
caseSensitivity.toBindingCase()
)
thunkFactory.thunkEnvValue(componentMetas) { _, componentValue ->
componentValue.bindings[lookupName] ?: valueFactory.missingValue
}
}
else -> {
val indexThunk = compileAstExpr(indexExpr)
thunkFactory.thunkEnvValue(componentMetas) { env, componentValue ->
val indexValue = indexThunk(env)
when {
indexValue.type == ExprValueType.INT -> {
componentValue.ordinalBindings[indexValue.numberValue().toInt()]
}
indexValue.type.isText -> {
val lookupName =
BindingName(indexValue.stringValue(), caseSensitivity.toBindingCase())
componentValue.bindings[lookupName]
}
else -> {
when (compileOptions.typingMode) {
TypingMode.LEGACY -> err(
"Cannot convert index to int/string: $indexValue",
ErrorCode.EVALUATOR_INVALID_CONVERSION,
errorContextFrom(componentMetas),
internal = false
)
TypingMode.PERMISSIVE -> valueFactory.missingValue
}
}
} ?: valueFactory.missingValue
}
}
}
}
is PartiqlAst.PathStep.PathUnpivot -> {
when {
!remainingComponents.isEmpty() -> {
val tempThunk = compilePathComponents(remainingComponents, pathMetas)
thunkFactory.thunkEnvValue(componentMetas) { env, componentValue ->
val mapped = componentValue.unpivot()
.flatMap { tempThunk(env, it).rangeOver() }
.asSequence()
valueFactory.newBag(mapped)
}
}
else ->
thunkFactory.thunkEnvValue(componentMetas) { _, componentValue ->
valueFactory.newBag(componentValue.unpivot().asSequence())
}
}
}
// this is for `path[*].component`
is PartiqlAst.PathStep.PathWildcard -> {
when {
!remainingComponents.isEmpty() -> {
val hasMoreWildCards =
remainingComponents.filterIsInstance().any()
val tempThunk = compilePathComponents(remainingComponents, pathMetas)
when {
!hasMoreWildCards -> thunkFactory.thunkEnvValue(componentMetas) { env, componentValue ->
val mapped = componentValue
.rangeOver()
.map { tempThunk(env, it) }
.asSequence()
valueFactory.newBag(mapped)
}
else -> thunkFactory.thunkEnvValue(componentMetas) { env, componentValue ->
val mapped = componentValue
.rangeOver()
.flatMap {
val tempValue = tempThunk(env, it)
tempValue
}
.asSequence()
valueFactory.newBag(mapped)
}
}
}
else -> {
thunkFactory.thunkEnvValue(componentMetas) { _, componentValue ->
val mapped = componentValue.rangeOver().asSequence()
valueFactory.newBag(mapped)
}
}
}
}
}
)
}
return when (componentThunks.size) {
1 -> componentThunks.first()
else -> thunkFactory.thunkEnvValue(pathMetas) { env, rootValue ->
componentThunks.fold(rootValue) { componentValue, componentThunk ->
componentThunk(env, componentValue)
}
}
}
}
/**
* Given an AST node that represents a `LIKE` predicate, return an ExprThunk that evaluates a `LIKE` predicate.
*
* Three cases
*
* 1. All arguments are literals, then compile and run the pattern
* 1. Search pattern and escape pattern are literals, compile the pattern. Running the pattern deferred to evaluation time.
* 1. Pattern or escape (or both) are *not* literals, compile and running of pattern deferred to evaluation time.
*
* ```
* LIKE [ESCAPE ]
* ```
*
* @return a thunk that when provided with an environment evaluates the `LIKE` predicate
*/
private fun compileLike(expr: PartiqlAst.Expr.Like, metas: MetaContainer): ThunkEnv {
val valueExpr = expr.value
val patternExpr = expr.pattern
val escapeExpr = expr.escape
val patternLocationMeta = patternExpr.metas.sourceLocation
val escapeLocationMeta = escapeExpr?.metas?.sourceLocation
// This is so that null short-circuits can be supported.
fun getRegexPattern(pattern: ExprValue, escape: ExprValue?): (() -> Pattern)? {
val patternArgs = listOfNotNull(pattern, escape)
when {
patternArgs.any { it.type.isUnknown } -> return null
patternArgs.any { !it.type.isText } -> return {
err(
"LIKE expression must be given non-null strings as input",
ErrorCode.EVALUATOR_LIKE_INVALID_INPUTS,
errorContextFrom(metas).also {
it[Property.LIKE_PATTERN] = pattern.toString()
if (escape != null) it[Property.LIKE_ESCAPE] = escape.toString()
},
internal = false
)
}
else -> {
val (patternString: String, escapeChar: Int?) =
checkPattern(pattern.stringValue(), patternLocationMeta, escape?.stringValue(), escapeLocationMeta)
val likeRegexPattern = when {
patternString.isEmpty() -> Pattern.compile("")
else -> parsePattern(patternString, escapeChar)
}
return { likeRegexPattern }
}
}
}
fun matchRegexPattern(value: ExprValue, likePattern: (() -> Pattern)?): ExprValue {
return when {
likePattern == null || value.type.isUnknown -> valueFactory.nullValue
!value.type.isText -> err(
"LIKE expression must be given non-null strings as input",
ErrorCode.EVALUATOR_LIKE_INVALID_INPUTS,
errorContextFrom(metas).also {
it[Property.LIKE_VALUE] = value.toString()
},
internal = false
)
else -> valueFactory.newBoolean(likePattern().matcher(value.stringValue()).matches())
}
}
val valueThunk = compileAstExpr(valueExpr)
// If the pattern and escape expressions are literals then we can compile the pattern now and
// re-use it with every execution. Otherwise, we must re-compile the pattern every time.
return when {
patternExpr is PartiqlAst.Expr.Lit && (escapeExpr == null || escapeExpr is PartiqlAst.Expr.Lit) -> {
val patternParts = getRegexPattern(
valueFactory.newFromIonValue(patternExpr.value.toIonValue(valueFactory.ion)),
(escapeExpr as? PartiqlAst.Expr.Lit)?.value?.toIonValue(valueFactory.ion)
?.let { valueFactory.newFromIonValue(it) }
)
// If valueExpr is also a literal then we can evaluate this at compile time and return a constant.
if (valueExpr is PartiqlAst.Expr.Lit) {
val resultValue = matchRegexPattern(
valueFactory.newFromIonValue(valueExpr.value.toIonValue(valueFactory.ion)),
patternParts
)
return thunkFactory.thunkEnv(metas) { resultValue }
} else {
thunkFactory.thunkEnvOperands(metas, valueThunk) { _, value ->
matchRegexPattern(value, patternParts)
}
}
}
else -> {
val patternThunk = compileAstExpr(patternExpr)
when (escapeExpr) {
null -> {
// thunk that re-compiles the DFA every evaluation without a custom escape sequence
thunkFactory.thunkEnvOperands(metas, valueThunk, patternThunk) { _, value, pattern ->
val pps = getRegexPattern(pattern, null)
matchRegexPattern(value, pps)
}
}
else -> {
// thunk that re-compiles the pattern every evaluation but *with* a custom escape sequence
val escapeThunk = compileAstExpr(escapeExpr)
thunkFactory.thunkEnvOperands(
metas,
valueThunk,
patternThunk,
escapeThunk
) { _, value, pattern, escape ->
val pps = getRegexPattern(pattern, escape)
matchRegexPattern(value, pps)
}
}
}
}
}
}
/**
* Given the pattern and optional escape character in a `LIKE` predicate as [IonValue]s
* check their validity based on the SQL92 spec and return a triple that contains in order
*
* - the search pattern as a string
* - the escape character, possibly `null`
* - the length of the search pattern. The length of the search pattern is either
* - the length of the string representing the search pattern when no escape character is used
* - the length of the string representing the search pattern without counting uses of the escape character
* when an escape character is used
*
* A search pattern is valid when
* 1. pattern is not null
* 1. pattern contains characters where `_` means any 1 character and `%` means any string of length 0 or more
* 1. if the escape character is specified then pattern can be deterministically partitioned into character groups where
* 1. A length 1 character group consists of any character other than the ESCAPE character
* 1. A length 2 character group consists of the ESCAPE character followed by either `_` or `%` or the ESCAPE character itself
*
* @param pattern search pattern
* @param escape optional escape character provided in the `LIKE` predicate
*
* @return a triple that contains in order the search pattern as a [String], optionally the code point for the escape character if one was provided
* and the size of the search pattern excluding uses of the escape character
*/
private fun checkPattern(
pattern: String,
patternLocationMeta: SourceLocationMeta?,
escape: String?,
escapeLocationMeta: SourceLocationMeta?
): Pair {
escape?.let {
val escapeCharString = checkEscapeChar(escape, escapeLocationMeta)
val escapeCharCodePoint = escapeCharString.codePointAt(0) // escape is a string of length 1
val validEscapedChars = setOf('_'.toInt(), '%'.toInt(), escapeCharCodePoint)
val iter = pattern.codePointSequence().iterator()
while (iter.hasNext()) {
val current = iter.next()
if (current == escapeCharCodePoint && (!iter.hasNext() || !validEscapedChars.contains(iter.next()))) {
err(
"Invalid escape sequence : $pattern",
ErrorCode.EVALUATOR_LIKE_PATTERN_INVALID_ESCAPE_SEQUENCE,
errorContextFrom(patternLocationMeta).apply {
set(Property.LIKE_PATTERN, pattern)
set(Property.LIKE_ESCAPE, escapeCharString)
},
internal = false
)
}
}
return Pair(pattern, escapeCharCodePoint)
}
return Pair(pattern, null)
}
/**
* Given an [IonValue] to be used as the escape character in a `LIKE` predicate check that it is
* a valid character based on the SQL Spec.
*
*
* A value is a valid escape when
* 1. it is 1 character long, and,
* 1. Cannot be null (SQL92 spec marks this cases as *unknown*)
*
* @param escape value provided as an escape character for a `LIKE` predicate
*
* @return the escape character as a [String] or throws an exception when the input is invalid
*/
private fun checkEscapeChar(escape: String, locationMeta: SourceLocationMeta?): String {
when (escape) {
"" -> {
err(
"Cannot use empty character as ESCAPE character in a LIKE predicate: $escape",
ErrorCode.EVALUATOR_LIKE_PATTERN_INVALID_ESCAPE_SEQUENCE,
errorContextFrom(locationMeta),
internal = false
)
}
else -> {
if (escape.trim().length != 1) {
err(
"Escape character must have size 1 : $escape",
ErrorCode.EVALUATOR_LIKE_PATTERN_INVALID_ESCAPE_SEQUENCE,
errorContextFrom(locationMeta),
internal = false
)
}
}
}
return escape
}
private fun compileDdl(node: PartiqlAst.Statement.Ddl): ThunkEnv =
{ _ ->
err(
"DDL operations are not supported yet",
ErrorCode.EVALUATOR_FEATURE_NOT_SUPPORTED_YET,
errorContextFrom(node.metas).also {
it[Property.FEATURE_NAME] = "DDL Operations"
},
internal = false
)
}
private fun compileDml(node: PartiqlAst.Statement.Dml): ThunkEnv =
{ _ ->
err(
"DML operations are not supported yet",
ErrorCode.EVALUATOR_FEATURE_NOT_SUPPORTED_YET,
errorContextFrom(node.metas).also {
it[Property.FEATURE_NAME] = "DML Operations"
},
internal = false
)
}
private fun compileExec(node: PartiqlAst.Statement.Exec): ThunkEnv {
val metas = node.metas
val procedureName = node.procedureName.text
val procedure = procedures[procedureName] ?: err(
"No such stored procedure: $procedureName",
ErrorCode.EVALUATOR_NO_SUCH_PROCEDURE,
errorContextFrom(metas).also {
it[Property.PROCEDURE_NAME] = procedureName
},
internal = false
)
val args = node.args
// Check arity
if (args.size !in procedure.signature.arity) {
val errorContext = errorContextFrom(metas).also {
it[Property.EXPECTED_ARITY_MIN] = procedure.signature.arity.first
it[Property.EXPECTED_ARITY_MAX] = procedure.signature.arity.last
}
val message = when {
procedure.signature.arity.first == 1 && procedure.signature.arity.last == 1 ->
"${procedure.signature.name} takes a single argument, received: ${args.size}"
procedure.signature.arity.first == procedure.signature.arity.last ->
"${procedure.signature.name} takes exactly ${procedure.signature.arity.first} arguments, received: ${args.size}"
else ->
"${procedure.signature.name} takes between ${procedure.signature.arity.first} and " +
"${procedure.signature.arity.last} arguments, received: ${args.size}"
}
err(
message,
ErrorCode.EVALUATOR_INCORRECT_NUMBER_OF_ARGUMENTS_TO_PROCEDURE_CALL,
errorContext,
internal = false
)
}
// Compile the procedure's arguments
val argThunks = compileAstExprs(args)
return thunkFactory.thunkEnv(metas) { env ->
val procedureArgValues = argThunks.map { it(env) }
procedure.call(env.session, procedureArgValues)
}
}
private fun compileDate(expr: PartiqlAst.Expr.Date, metas: MetaContainer): ThunkEnv =
thunkFactory.thunkEnv(metas) {
valueFactory.newDate(
expr.year.value.toInt(),
expr.month.value.toInt(),
expr.day.value.toInt()
)
}
private fun compileLitTime(expr: PartiqlAst.Expr.LitTime, metas: MetaContainer): ThunkEnv =
thunkFactory.thunkEnv(metas) {
// Add the default time zone if the type "TIME WITH TIME ZONE" does not have an explicitly specified time zone.
valueFactory.newTime(
Time.of(
expr.value.hour.value.toInt(),
expr.value.minute.value.toInt(),
expr.value.second.value.toInt(),
expr.value.nano.value.toInt(),
expr.value.precision.value.toInt(),
if (expr.value.withTimeZone.value && expr.value.tzMinutes == null) compileOptions.defaultTimezoneOffset.totalMinutes else expr.value.tzMinutes?.value?.toInt()
)
)
}
/** A special wrapper for `UNPIVOT` values as a BAG. */
private class UnpivotedExprValue(private val values: Iterable) : BaseExprValue() {
override val type = ExprValueType.BAG
override fun iterator() = values.iterator()
override val ionValue
get() = throw UnsupportedOperationException("Synthetic value cannot provide ion value")
}
/** Unpivots a `struct`, and synthesizes a synthetic singleton `struct` for other [ExprValue]. */
internal fun ExprValue.unpivot(): ExprValue = when {
// special case for our special UNPIVOT value to avoid double wrapping
this is UnpivotedExprValue -> this
// Wrap into a pseudo-BAG
type == ExprValueType.STRUCT || type == ExprValueType.MISSING -> UnpivotedExprValue(this)
// for non-struct, this wraps any value into a BAG with a synthetic name
else -> UnpivotedExprValue(
listOf(
this.namedValue(valueFactory.newString(syntheticColumnName(0)))
)
)
}
private fun createStructExprValue(seq: Sequence, ordering: StructOrdering) =
valueFactory.newStruct(
when (compileOptions.projectionIteration) {
ProjectionIterationBehavior.FILTER_MISSING -> seq.filter { it.type != ExprValueType.MISSING }
ProjectionIterationBehavior.UNFILTERED -> seq
},
ordering
)
/** Helper to convert [PartiqlAst.Type] in AST to a [TypedOpParameter]. */
private fun PartiqlAst.Type.toTypedOpParameter(): TypedOpParameter {
// hack: to avoid duplicating the function `PartiqlAst.Type.toTypedOpParameter`, we have to convert this
// PartiqlAst.Type to PartiqlPhysical.Type. The easiest way to do that without using a visitor transform
// (which is overkill and comes with some downsides for something this simple), is to transform to and from
// s-expressions again. This will work without difficulty as long as PartiqlAst.Type remains unchanged in all
// permuted domains between PartiqlAst and PartiqlPhysical.
// This is really just a temporary measure, however, which must exist for as long as the type inferencer works only
// on PartiqlAst. When it has been migrated to use PartiqlPhysical instead, there should no longer be a reason
// to keep this function around.
val sexp = this.toIonElement()
val physicalType = PartiqlPhysical.transform(sexp) as PartiqlPhysical.Type
return physicalType.toTypedOpParameter(customTypedOpParameters)
}
}
/**
* Contains data about a compiled from source, including its [Alias], [thunk],
* type of [JoinExpansion] ([JoinExpansion.INNER] for single tables or `CROSS JOIN`S.) and [filter] criteria.
*/
private data class CompiledFromSource(
val alias: Alias,
val thunk: ThunkEnv,
val joinExpansion: JoinExpansion,
val filter: ThunkEnv?
)
/**
* Represents a single `FROM` source production of values.
*
* @param values A single production of values from the `FROM` source.
* @param env The environment scoped to the values of this production.
*/
private data class FromProduction(
val values: List,
val env: Environment
)
/** Specifies the expansion for joins. */
private enum class JoinExpansion {
/** Default for non-joined values, CROSS and INNER JOIN. */
INNER,
/** Expansion mode for LEFT/RIGHT/FULL JOIN. */
OUTER
}
private data class CompiledLetSource(
val name: String,
val thunk: ThunkEnv
)
private enum class ExpressionContext {
/**
* Indicates that the compiler is compiling a normal expression (i.e. not one of the other
* contexts).
*/
NORMAL,
/**
* Indicates that the compiler is compiling an expression in a select list.
*/
SELECT_LIST,
/**
* Indicates that the compiler is compiling an expression that is the argument to an aggregate function.
*/
AGG_ARG
}
/**
* Tracks state used by the compiler while compiling.
*
* @param expressionContext Indicates what part of the grammar is currently being compiled.
* @param fromSourceNames Set of all FROM source aliases for binding error checks.
*/
private class CompilationContext(val expressionContext: ExpressionContext, val fromSourceNames: Set) {
fun createNested(expressionContext: ExpressionContext, fromSourceNames: Set) =
CompilationContext(expressionContext, fromSourceNames)
}
/**
* Represents an element in a select list that is to be projected into the final result.
* i.e. an expression, or a (project_all) node.
*/
private sealed class ProjectionElement
/**
* Represents a single compiled expression to be projected into the final result.
* Given `SELECT a + b as value FROM foo`:
* - `name` is "value"
* - `thunk` is compiled expression, i.e. `a + b`
*/
private class SingleProjectionElement(val name: ExprValue, val thunk: ThunkEnv) : ProjectionElement()
/**
* Represents a wildcard ((path_project_all) node) expression to be projected into the final result.
* This covers two cases. For `SELECT foo.* FROM foo`, `exprThunks` contains a single compiled expression
* `foo`.
*
* For `SELECT * FROM foo, bar, bat`, `exprThunks` would contain a compiled expression for each of `foo`, `bar` and
* `bat`.
*/
private class MultipleProjectionElement(val thunks: List) : ProjectionElement()
internal val MetaContainer.sourceLocationMeta get() = this[SourceLocationMeta.TAG] as? SourceLocationMeta
private fun StaticType.getTypes() = when (val flattened = this.flatten()) {
is AnyOfType -> flattened.types
else -> listOf(this)
}