Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for PERMISSIVE vs STRICT #1353

Merged
merged 3 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ public interface PartiQLEngine {

public class Session @OptIn(PartiQLFunctionExperimental::class) constructor(
val bindings: Map<String, ConnectorBindings> = mapOf(),
val functions: Map<String, List<PartiQLFunction>> = mapOf()
val functions: Map<String, List<PartiQLFunction>> = mapOf(),
val mode: Mode = Mode.PERMISSIVE
)

public enum class Mode {
PERMISSIVE,
STRICT // AKA, Type Checking Mode in the PartiQL Specification
}
}
51 changes: 40 additions & 11 deletions partiql-eval/src/main/kotlin/org/partiql/eval/internal/Compiler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import org.partiql.eval.internal.operator.rel.RelJoinRight
import org.partiql.eval.internal.operator.rel.RelProject
import org.partiql.eval.internal.operator.rel.RelScan
import org.partiql.eval.internal.operator.rel.RelScanIndexed
import org.partiql.eval.internal.operator.rel.RelScanIndexedPermissive
import org.partiql.eval.internal.operator.rel.RelScanPermissive
import org.partiql.eval.internal.operator.rex.ExprCallDynamic
import org.partiql.eval.internal.operator.rex.ExprCallStatic
import org.partiql.eval.internal.operator.rex.ExprCase
Expand All @@ -20,7 +22,9 @@ import org.partiql.eval.internal.operator.rex.ExprLiteral
import org.partiql.eval.internal.operator.rex.ExprPathIndex
import org.partiql.eval.internal.operator.rex.ExprPathKey
import org.partiql.eval.internal.operator.rex.ExprPathSymbol
import org.partiql.eval.internal.operator.rex.ExprPermissive
import org.partiql.eval.internal.operator.rex.ExprPivot
import org.partiql.eval.internal.operator.rex.ExprPivotPermissive
import org.partiql.eval.internal.operator.rex.ExprSelect
import org.partiql.eval.internal.operator.rex.ExprStruct
import org.partiql.eval.internal.operator.rex.ExprTupleUnion
Expand All @@ -37,6 +41,7 @@ import org.partiql.spi.function.PartiQLFunction
import org.partiql.spi.function.PartiQLFunctionExperimental
import org.partiql.types.function.FunctionSignature
import org.partiql.value.PartiQLValueExperimental
import org.partiql.value.PartiQLValueType
import java.lang.IllegalStateException

internal class Compiler @OptIn(PartiQLFunctionExperimental::class) constructor(
Expand Down Expand Up @@ -71,7 +76,7 @@ internal class Compiler @OptIn(PartiQLFunctionExperimental::class) constructor(

// TODO: Re-look at
override fun visitStatementQuery(node: Statement.Query, ctx: Unit): Operator.Expr {
return visitRex(node.root, ctx)
return visitRex(node.root, ctx).modeHandled()
}

// REX
Expand All @@ -81,27 +86,31 @@ internal class Compiler @OptIn(PartiQLFunctionExperimental::class) constructor(
}

override fun visitRexOpCollection(node: Rex.Op.Collection, ctx: Unit): Operator {
val values = node.values.map { visitRex(it, ctx) }
val values = node.values.map { visitRex(it, ctx).modeHandled() }
return ExprCollection(values)
}
override fun visitRexOpStruct(node: Rex.Op.Struct, ctx: Unit): Operator {
val fields = node.fields.map {
ExprStruct.Field(visitRex(it.k, ctx), visitRex(it.v, ctx))
val value = visitRex(it.v, ctx).modeHandled()
ExprStruct.Field(visitRex(it.k, ctx), value)
}
return ExprStruct(fields)
}

override fun visitRexOpSelect(node: Rex.Op.Select, ctx: Unit): Operator {
val rel = visitRel(node.rel, ctx)
val constructor = visitRex(node.constructor, ctx)
val constructor = visitRex(node.constructor, ctx).modeHandled()
return ExprSelect(rel, constructor)
}

override fun visitRexOpPivot(node: Rex.Op.Pivot, ctx: Unit): Operator {
val rel = visitRel(node.rel, ctx)
val key = visitRex(node.key, ctx)
val value = visitRex(node.value, ctx)
return ExprPivot(rel, key, value)
return when (session.mode) {
PartiQLEngine.Mode.PERMISSIVE -> ExprPivotPermissive(rel, key, value)
PartiQLEngine.Mode.STRICT -> ExprPivot(rel, key, value)
}
}
override fun visitRexOpVar(node: Rex.Op.Var, ctx: Unit): Operator {
return ExprVar(node.ref)
Expand Down Expand Up @@ -133,16 +142,22 @@ internal class Compiler @OptIn(PartiQLFunctionExperimental::class) constructor(
return ExprPathIndex(root, index)
}

@OptIn(PartiQLFunctionExperimental::class)
@OptIn(PartiQLFunctionExperimental::class, PartiQLValueExperimental::class)
override fun visitRexOpCallStatic(node: Rex.Op.Call.Static, ctx: Unit): Operator {
val function = getFunction(node.fn.signature)
val args = node.args.map { visitRex(it, ctx) }.toTypedArray()
return ExprCallStatic(function, args)
val fnTakesInMissing = function.signature.parameters.any {
it.type == PartiQLValueType.MISSING || it.type == PartiQLValueType.ANY
}
return when (fnTakesInMissing) {
true -> ExprCallStatic(function, args.map { it.modeHandled() }.toTypedArray())
false -> ExprCallStatic(function, args)
}
}

@OptIn(PartiQLFunctionExperimental::class, PartiQLValueExperimental::class)
override fun visitRexOpCallDynamic(node: Rex.Op.Call.Dynamic, ctx: Unit): Operator {
val args = node.args.map { visitRex(it, ctx) }.toTypedArray()
val args = node.args.map { visitRex(it, ctx).modeHandled() }.toTypedArray()
val candidates = node.candidates.map { candidate ->
val fn = getFunction(candidate.fn.signature)
val coercions = candidate.coercions.map { it?.signature?.let { sig -> getFunction(sig) } }
Expand Down Expand Up @@ -174,18 +189,24 @@ internal class Compiler @OptIn(PartiQLFunctionExperimental::class) constructor(

override fun visitRelOpScan(node: Rel.Op.Scan, ctx: Unit): Operator {
val rex = visitRex(node.rex, ctx)
return RelScan(rex)
return when (session.mode) {
PartiQLEngine.Mode.PERMISSIVE -> RelScanPermissive(rex)
PartiQLEngine.Mode.STRICT -> RelScan(rex)
}
}

override fun visitRelOpProject(node: Rel.Op.Project, ctx: Unit): Operator {
val input = visitRel(node.input, ctx)
val projections = node.projections.map { visitRex(it, ctx) }
val projections = node.projections.map { visitRex(it, ctx).modeHandled() }
return RelProject(input, projections)
}

override fun visitRelOpScanIndexed(node: Rel.Op.ScanIndexed, ctx: Unit): Operator {
val rex = visitRex(node.rex, ctx)
return RelScanIndexed(rex)
return when (session.mode) {
PartiQLEngine.Mode.PERMISSIVE -> RelScanIndexedPermissive(rex)
PartiQLEngine.Mode.STRICT -> RelScanIndexed(rex)
}
}

override fun visitRexOpTupleUnion(node: Rex.Op.TupleUnion, ctx: Unit): Operator {
Expand Down Expand Up @@ -228,4 +249,12 @@ internal class Compiler @OptIn(PartiQLFunctionExperimental::class) constructor(
val condition = visitRex(node.predicate, ctx)
return RelFilter(input, condition)
}

// HELPERS
private fun Operator.Expr.modeHandled(): Operator.Expr {
return when (session.mode) {
PartiQLEngine.Mode.PERMISSIVE -> ExprPermissive(this)
PartiQLEngine.Mode.STRICT -> this
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.partiql.eval.internal.operator.rel

import org.partiql.errors.TypeCheckException
import org.partiql.eval.internal.Record
import org.partiql.eval.internal.operator.Operator
import org.partiql.value.CollectionValue
Expand All @@ -16,7 +17,10 @@ internal class RelScan(
val r = expr.eval(Record.empty)
records = when (r) {
is CollectionValue<*> -> r.map { Record.of(it) }.iterator()
else -> iterator { yield(Record.of(r)) }
else -> {
close()
throw TypeCheckException()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.partiql.eval.internal.operator.rel

import org.partiql.errors.TypeCheckException
import org.partiql.eval.internal.Record
import org.partiql.eval.internal.operator.Operator
import org.partiql.value.BagValue
import org.partiql.value.CollectionValue
import org.partiql.value.PartiQLValue
import org.partiql.value.PartiQLValueExperimental
Expand All @@ -19,8 +21,15 @@ internal class RelScanIndexed(
val r = expr.eval(Record.empty)
index = 0
iterator = when (r) {
is BagValue<*> -> {
close()
throw TypeCheckException()
}
is CollectionValue<*> -> r.iterator()
else -> iterator { yield(r) }
else -> {
close()
throw TypeCheckException()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.partiql.eval.internal.operator.rel

import org.partiql.eval.internal.Record
import org.partiql.eval.internal.operator.Operator
import org.partiql.value.BagValue
import org.partiql.value.CollectionValue
import org.partiql.value.PartiQLValue
import org.partiql.value.PartiQLValueExperimental
import org.partiql.value.int64Value
import org.partiql.value.missingValue

@OptIn(PartiQLValueExperimental::class)
internal class RelScanIndexedPermissive(
private val expr: Operator.Expr
) : Operator.Relation {

private lateinit var iterator: Iterator<PartiQLValue>
private var index: Long = 0
private var isIndexable: Boolean = true

override fun open() {
val r = expr.eval(Record.empty)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but does this work for subquery?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question. I have a subquery PR out, and when I rebase, I'll take a look at that.

index = 0
iterator = when (r) {
is BagValue<*> -> {
isIndexable = false
r.iterator()
}
is CollectionValue<*> -> r.iterator()
else -> {
isIndexable = false
iterator { yield(r) }
}
}
}

override fun next(): Record? {
if (!iterator.hasNext()) {
return null
}
val v = iterator.next()
return when (isIndexable) {
true -> {
val i = index
index += 1
Record.of(v, int64Value(i))
}
false -> Record.of(v, missingValue())
}
}

override fun close() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.partiql.eval.internal.operator.rel

import org.partiql.eval.internal.Record
import org.partiql.eval.internal.operator.Operator
import org.partiql.value.CollectionValue
import org.partiql.value.PartiQLValueExperimental

@OptIn(PartiQLValueExperimental::class)
internal class RelScanPermissive(
private val expr: Operator.Expr
) : Operator.Relation {

private lateinit var records: Iterator<Record>

override fun open() {
val r = expr.eval(Record.empty)
records = when (r) {
is CollectionValue<*> -> r.map { Record.of(it) }.iterator()
else -> iterator { yield(Record.of(r)) }
}
}

override fun next(): Record? {
return if (records.hasNext()) {
records.next()
} else {
null
}
}

override fun close() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ internal class ExprCallDynamic(
@OptIn(PartiQLValueExperimental::class)
internal fun matches(args: Array<PartiQLValue>): Boolean {
for (i in args.indices) {
if (types[i] == PartiQLValueType.ANY) {
return true
}
if (args[i].type != types[i]) {
return false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package org.partiql.eval.internal.operator.rex

import org.partiql.errors.TypeCheckException
import org.partiql.eval.internal.Record
import org.partiql.eval.internal.helpers.toNull
import org.partiql.eval.internal.operator.Operator
import org.partiql.spi.function.PartiQLFunction
import org.partiql.spi.function.PartiQLFunctionExperimental
import org.partiql.value.PartiQLValue
import org.partiql.value.PartiQLValueExperimental
import org.partiql.value.missingValue

@OptIn(PartiQLValueExperimental::class, PartiQLFunctionExperimental::class)
internal class ExprCallStatic(
Expand All @@ -22,15 +20,13 @@ internal class ExprCallStatic(
@OptIn(PartiQLValueExperimental::class)
private val nil = fn.signature.returns.toNull()

override fun eval(record: Record): PartiQLValue = try {
override fun eval(record: Record): PartiQLValue {
// Evaluate arguments
val args = inputs.map { input ->
val r = input.eval(record)
if (r.isNull && fn.signature.isNullCall) return nil()
r
}.toTypedArray()
fn.invoke(args)
} catch (ex: TypeCheckException) {
missingValue()
return fn.invoke(args)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.partiql.eval.internal.operator.rex

import org.partiql.errors.TypeCheckException
import org.partiql.eval.internal.Record
import org.partiql.eval.internal.operator.Operator
import org.partiql.value.CollectionValue
Expand All @@ -11,7 +12,6 @@ import org.partiql.value.IntValue
import org.partiql.value.PartiQLValue
import org.partiql.value.PartiQLValueExperimental
import org.partiql.value.check
import org.partiql.value.missingValue

internal class ExprPathIndex(
@JvmField val root: Operator.Expr,
Expand All @@ -21,7 +21,6 @@ internal class ExprPathIndex(
@OptIn(PartiQLValueExperimental::class)
override fun eval(record: Record): PartiQLValue {
val collection = root.eval(record).check<CollectionValue<PartiQLValue>>()
val value = missingValue()

// Calculate index
val index = when (val k = key.eval(record)) {
Expand All @@ -30,8 +29,8 @@ internal class ExprPathIndex(
is Int64Value -> k.int
is Int8Value -> k.int
is IntValue -> k.int
else -> return value
} ?: return value
else -> throw TypeCheckException()
} ?: throw TypeCheckException()

// Get element
val iterator = collection.iterator()
Expand All @@ -43,6 +42,6 @@ internal class ExprPathIndex(
}
i++
}
return value
throw TypeCheckException()
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.partiql.eval.internal.operator.rex

import org.partiql.errors.TypeCheckException
import org.partiql.eval.internal.Record
import org.partiql.eval.internal.operator.Operator
import org.partiql.value.PartiQLValue
import org.partiql.value.PartiQLValueExperimental
import org.partiql.value.StringValue
import org.partiql.value.StructValue
import org.partiql.value.check
import org.partiql.value.missingValue

internal class ExprPathKey(
@JvmField val root: Operator.Expr,
Expand All @@ -19,6 +19,6 @@ internal class ExprPathKey(
val rootEvaluated = root.eval(record).check<StructValue<PartiQLValue>>()
val keyEvaluated = key.eval(record).check<StringValue>()
val keyString = keyEvaluated.value ?: error("String value was null")
return rootEvaluated[keyString] ?: missingValue()
return rootEvaluated[keyString] ?: throw TypeCheckException()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tuple navigation in tuples with duplicate attributes When the tuple t has multiple attributes a, the tuple
path navigation t.a will return the first instance of a. Note that for tuples whose order is defined by schema, this is well-defined, for unordered tuples, it is implementation defined which attribute is returned in permissive mode or an error in type checking mode, which is described in Section 4.1

It seems like right now we don't have the notion of ordered vs unordered struct during runtime. Maybe just keep a todo note here?

}
}
Loading
Loading