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

Ports v0.14.9 changes #1702

Merged
merged 5 commits into from
Jan 13, 2025
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 @@ -184,4 +184,20 @@ internal object PErrors {
mapOf("ID" to id, "LOCALS" to locals)
)
}

/**
* @param path see [PError.INVALID_EXCLUDE_PATH]
* @return an error representing [PError.INVALID_EXCLUDE_PATH]
*/
internal fun invalidExcludePath(
path: String
): PError {
return PError(
PError.INVALID_EXCLUDE_PATH,
Severity.WARNING(),
PErrorKind.SEMANTIC(),
null,
mapOf("PATH" to path)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package org.partiql.planner.internal.typer

import org.partiql.planner.internal.PErrors
import org.partiql.planner.internal.ir.Rel
import org.partiql.planner.internal.ir.Rex
import org.partiql.spi.catalog.Identifier
import org.partiql.spi.errors.PErrorListener
import org.partiql.spi.types.PType

internal object ExcludeUtils {
/**
* Checks for every exclude path in [paths], that a value in the [bindings] is excluded. If there is no value excluded,
* a warning is added to the [onProblem] callback.
*/
internal fun checkForInvalidExcludePaths(bindings: List<Rel.Binding>, paths: List<Rel.Op.Exclude.Path>, onProblem: PErrorListener) {
paths.forEach { excludePath ->
val root = excludePath.root
val steps = excludePath.steps
val excludesSomething = bindings.any { binding ->
when (root) {
is Rex.Op.Var.Unresolved -> {
if (root.identifier.first().matches(binding.name)) {
binding.type.checkExclude(steps)
} else {
false
}
}
else -> false // root should be unresolved
}
}
// If nothing is excluded by `excludePath`, add a warning
if (!excludesSomething) {
onProblem.report(PErrors.invalidExcludePath(excludePath.toProblemString()))
}
}
}

/**
* Checks whether [steps] will exclude a value from [this].
*/
private fun PType.checkExclude(steps: List<Rel.Op.Exclude.Step>): Boolean {
return when (this.code()) {
PType.ROW -> this.checkRowExclude(steps)
PType.ARRAY, PType.BAG -> this.checkCollectionExclude(steps)
PType.DYNAMIC, PType.VARIANT -> true
else -> steps.isEmpty()
}
}

/**
* Checks whether [steps] will exclude a value from [this] [PType.ROW].
*/
private fun PType.checkRowExclude(steps: List<Rel.Op.Exclude.Step>): Boolean {
// Ignore open structs
if (steps.isEmpty()) {
return true
}
return steps.all { step ->
fields.any { field ->
when (val type = step.type) {
is Rel.Op.Exclude.Type.StructSymbol -> {
Identifier.regular(type.symbol).first().matches(field.name) && field.type.checkExclude(step.substeps)
}
is Rel.Op.Exclude.Type.StructKey -> {
type.key == field.name && field.type.checkExclude(step.substeps)
}
is Rel.Op.Exclude.Type.StructWildcard -> field.type.checkExclude(step.substeps)
else -> false
}
}
}
}

/**
* Checks whether [steps] will exclude a value from [this] [PType.BAG]/[PType.ARRAY].
*/
private fun PType.checkCollectionExclude(steps: List<Rel.Op.Exclude.Step>): Boolean {
if (steps.isEmpty()) {
return true
}
return steps.all { step ->
when (step.type) {
is Rel.Op.Exclude.Type.CollIndex, is Rel.Op.Exclude.Type.CollWildcard -> {
val e = this.typeParameter
e.checkExclude(step.substeps)
}
else -> false
}
}
}

// `EXCLUDE` path printing functions for problem printing
private fun Rel.Op.Exclude.Path.toProblemString(): String {
val root = when (root) {
is Rex.Op.Var.Unresolved -> root.identifier.toProblemString()
is Rex.Op.Var.Local -> root.ref.toString()
is Rex.Op.Var.Global -> root.ref.toString()
else -> error("This isn't supported.")
}
val steps = steps.map {
when (val type = it.type) {
is Rel.Op.Exclude.Type.CollIndex -> "[${type.index}]"
is Rel.Op.Exclude.Type.CollWildcard -> "[*]"
is Rel.Op.Exclude.Type.StructSymbol -> ".${type.symbol}"
is Rel.Op.Exclude.Type.StructKey -> ".\"${type.key}\""
is Rel.Op.Exclude.Type.StructWildcard -> ".*"
}
}
return root + steps.joinToString(separator = "")
}

private fun Identifier.toProblemString(): String {
return this.joinToString("") { it.toProblemString() }
}

private fun Identifier.Simple.toProblemString(): String {
return when (this.isRegular()) {
false -> "\"${getText()}\""
true -> getText()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,6 @@ internal class PlanTyper(private val env: Env, config: Context) {
* - Excluding collection wildcards (e.g. t.a[*].b)
*
* There are still discussion points regarding the following edge cases:
* - EXCLUDE on a tuple attribute that doesn't exist -- give an error/warning?
* - currently no error
* - EXCLUDE on a tuple attribute that has duplicates -- give an error/warning? exclude one? exclude both?
* - currently excludes both w/ no error
* - EXCLUDE on a collection index as the last step -- mark element type as optional?
Expand All @@ -478,8 +476,9 @@ internal class PlanTyper(private val env: Env, config: Context) {
val input = visitRel(node.input, ctx)

// apply exclusions to the input schema
val init = input.type.schema.map { it.copy() }
val schema = node.paths.fold((init)) { bindings, path -> excludeBindings(bindings, path) }
val initBindings = input.type.schema.map { it.copy() }
ExcludeUtils.checkForInvalidExcludePaths(initBindings, node.paths, _listener)
val schema = node.paths.fold((initBindings)) { bindings, item -> excludeBindings(bindings, item) }

// rewrite
val type = ctx!!.copy(schema = schema)
Expand Down Expand Up @@ -1279,7 +1278,15 @@ internal class PlanTyper(private val env: Env, config: Context) {
when (val root = item.root) {
is Rex.Op.Var.Unresolved -> {
when (root.identifier.hasQualifier()) {
true -> it
true -> {
if (root.identifier.first().matches(it.name)) {
// recompute the StaticType of this binding after apply the exclusions
val type = it.type.exclude(item.steps, false)
it.copy(type = type)
} else {
it
}
}
else -> {
if (root.identifier.matches(it.name)) {
// recompute the PType of this binding after applying the exclusions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package org.partiql.planner.internal.typer

import org.partiql.planner.internal.ir.Rel
import org.partiql.planner.internal.typer.PlanTyper.Companion.toCType
import org.partiql.spi.types.PType

/**
* Applies the given exclusion path to produce the reduced [CompilerType]. [lastStepOptional] indicates if a previous
* step in the exclude path includes a collection index exclude step. Currently, for paths with the last step as
* a struct symbol/key, the type inference will define that struct value as optional if [lastStepOptional] is true.
* Note, this specific behavior could change depending on `EXCLUDE`'s static typing behavior in a future RFC.
*
* e.g. EXCLUDE t.a[1].field_x will define the struct value `field_x` as optional
*
* @param steps
* @param lastStepOptional
* @return
*/
internal fun CompilerType.exclude(steps: List<Rel.Op.Exclude.Step>, lastStepOptional: Boolean = false): CompilerType {
val type = this
return steps.fold(type) { acc, step ->
when (acc.code()) {
PType.DYNAMIC -> CompilerType(PType.dynamic())
PType.ROW -> acc.excludeStruct(step, lastStepOptional)
PType.STRUCT -> acc
PType.ARRAY, PType.BAG -> acc.excludeCollection(step, lastStepOptional)
else -> acc
}
}
}

/**
* Applies exclusions to struct fields.
*
* @param step
* @param lastStepOptional
* @return
*/
internal fun CompilerType.excludeStruct(step: Rel.Op.Exclude.Step, lastStepOptional: Boolean = false): CompilerType {
val type = step.type
val substeps = step.substeps
val output = fields.mapNotNull { field ->
val newField = if (substeps.isEmpty()) {
if (lastStepOptional) {
CompilerType.Field(field.name, field.type)
} else {
null
}
} else {
val k = field.name
val v = field.type.exclude(substeps, lastStepOptional)
CompilerType.Field(k, v)
}
when (type) {
is Rel.Op.Exclude.Type.StructSymbol -> {
if (type.symbol.equals(field.name, ignoreCase = true)) {
newField
} else {
field
}
}

is Rel.Op.Exclude.Type.StructKey -> {
if (type.key == field.name) {
newField
} else {
field
}
}
is Rel.Op.Exclude.Type.StructWildcard -> newField
else -> field
}
}
return CompilerType(PType.row(output))
}

/**
* Applies exclusions to collection element type.
*
* @param step
* @param lastStepOptional
* @return
*/
internal fun CompilerType.excludeCollection(step: Rel.Op.Exclude.Step, lastStepOptional: Boolean = false): CompilerType {
var e = this.typeParameter
val substeps = step.substeps
when (step.type) {
is Rel.Op.Exclude.Type.CollIndex -> {
if (substeps.isNotEmpty()) {
e = e.exclude(substeps, lastStepOptional = true)
}
}

is Rel.Op.Exclude.Type.CollWildcard -> {
if (substeps.isNotEmpty()) {
e = e.exclude(substeps, lastStepOptional)
}
// currently no change to elementType if collection wildcard is last element; this behavior could
// change based on RFC definition
}

else -> {
// currently no change to elementType and no error thrown; could consider an error/warning in
// the future
}
}
return when (this.code()) {
PType.ARRAY -> PType.array(e).toCType()
PType.BAG -> PType.bag(e).toCType()
else -> throw IllegalStateException()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ internal class PlannerPErrorReportingTests {
FROM struct_no_missing as t
""".trimIndent(),
false,
assertOnProblemCount(1, 0),
assertOnProblemCount(2, 0),
BagType(closedStruct(StructType.Field("f1", StaticType.INT2)))
),
TestCase(
Expand All @@ -365,7 +365,7 @@ internal class PlannerPErrorReportingTests {
FROM struct_no_missing as t
""".trimIndent(),
true,
assertOnProblemCount(0, 1),
assertOnProblemCount(1, 1),
BagType(closedStruct(StructType.Field("f1", StaticType.INT2)))
),
// TestCase(
Expand Down
Loading
Loading