Skip to content

Commit 161fca0

Browse files
authored
Merge d767d08 into 92cc57a
2 parents 92cc57a + d767d08 commit 161fca0

File tree

13 files changed

+439
-64
lines changed

13 files changed

+439
-64
lines changed

CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,18 @@ Thank you to all who have contributed!
3333
### Changed
3434
- Change `StaticType.AnyOfType`'s `.toString` to not perform `.flatten()`
3535
- Change modeling of `COALESCE` and `NULLIF` to dedicated nodes in logical plan
36+
- Function resolution logic: Now the function resolver would match all possible candidate (based on if the argument can be coerced to the Signature parameter type). If there are multiple match it will first attempt to pick the one requires the least cast, then pick the function with the highest precedence.
37+
- **Behavioral change**: The COUNT aggregate function now returns INT64.
3638

3739
### Deprecated
3840
- The current SqlBlock, SqlDialect, and SqlLayout are marked as deprecated and will be slightly changed in the next release.
41+
- Deprecates constructor and properties `variableName` and `caseSensitive` of `org.partiql.planner.PlanningProblemDetails.UndefinedVariable`
42+
in favor of newly added constructor and properties `name` and `inScopeVariables`.
3943

4044
### Fixed
4145
- `StaticType.flatten()` on an `AnyOfType` with `AnyType` will return `AnyType`
4246
- Updates the default `.sql()` method to use a more efficient (internal) printer implementation.
43-
47+
- Fixes aggregations of attribute references to values of union types. This fix also allows for proper error handling by passing the UnknownAggregateFunction problem to the ProblemCallback. Please note that, with this change, the planner will no longer immediately throw an IllegalStateException for this exact scenario.
4448

4549
### Removed
4650

@@ -51,6 +55,7 @@ Thank you to all who have contributed!
5155
- @<your-username>
5256
- @rchowell
5357
- @alancai98
58+
- @johnedquinn
5459

5560
## [0.14.4]
5661

partiql-planner/src/main/kotlin/org/partiql/planner/Errors.kt

+61-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package org.partiql.planner
22

33
import org.partiql.errors.ProblemDetails
44
import org.partiql.errors.ProblemSeverity
5+
import org.partiql.plan.Identifier
6+
import org.partiql.planner.internal.utils.PlanUtils
57
import org.partiql.types.StaticType
68

79
/**
@@ -24,15 +26,60 @@ public sealed class PlanningProblemDetails(
2426
public data class CompileError(val errorMessage: String) :
2527
PlanningProblemDetails(ProblemSeverity.ERROR, { errorMessage })
2628

27-
public data class UndefinedVariable(val variableName: String, val caseSensitive: Boolean) :
28-
PlanningProblemDetails(
29-
ProblemSeverity.ERROR,
30-
{
31-
"Undefined variable '$variableName'." +
32-
quotationHint(caseSensitive)
29+
public data class UndefinedVariable(
30+
val name: Identifier,
31+
val inScopeVariables: Set<String>
32+
) : PlanningProblemDetails(
33+
ProblemSeverity.ERROR,
34+
{
35+
val humanReadableName = PlanUtils.identifierToString(name)
36+
"Variable $humanReadableName does not exist in the database environment and is not an attribute of the following in-scope variables $inScopeVariables." +
37+
quotationHint(isSymbolAndCaseSensitive(name))
38+
}
39+
) {
40+
41+
@Deprecated("This will be removed in a future major version release.", replaceWith = ReplaceWith("name"))
42+
val variableName: String = when (name) {
43+
is Identifier.Symbol -> name.symbol
44+
is Identifier.Qualified -> when (name.steps.size) {
45+
0 -> name.root.symbol
46+
else -> name.steps.last().symbol
3347
}
48+
}
49+
50+
@Deprecated("This will be removed in a future major version release.", replaceWith = ReplaceWith("name"))
51+
val caseSensitive: Boolean = when (name) {
52+
is Identifier.Symbol -> name.caseSensitivity == Identifier.CaseSensitivity.SENSITIVE
53+
is Identifier.Qualified -> when (name.steps.size) {
54+
0 -> name.root.caseSensitivity == Identifier.CaseSensitivity.SENSITIVE
55+
else -> name.steps.last().caseSensitivity == Identifier.CaseSensitivity.SENSITIVE
56+
}
57+
}
58+
59+
@Deprecated("This will be removed in a future major version release.", replaceWith = ReplaceWith("UndefinedVariable(Identifier, Set<String>)"))
60+
public constructor(variableName: String, caseSensitive: Boolean) : this(
61+
Identifier.Symbol(
62+
variableName,
63+
when (caseSensitive) {
64+
true -> Identifier.CaseSensitivity.SENSITIVE
65+
false -> Identifier.CaseSensitivity.INSENSITIVE
66+
}
67+
),
68+
emptySet()
3469
)
3570

71+
private companion object {
72+
/**
73+
* Used to check whether the [id] is an [Identifier.Symbol] and whether it is case-sensitive. This is helpful
74+
* for giving the [quotationHint] to the user.
75+
*/
76+
private fun isSymbolAndCaseSensitive(id: Identifier): Boolean = when (id) {
77+
is Identifier.Symbol -> id.caseSensitivity == Identifier.CaseSensitivity.SENSITIVE
78+
is Identifier.Qualified -> false
79+
}
80+
}
81+
}
82+
3683
public data class UndefinedDmlTarget(val variableName: String, val caseSensitive: Boolean) :
3784
PlanningProblemDetails(
3885
ProblemSeverity.ERROR,
@@ -94,6 +141,14 @@ public sealed class PlanningProblemDetails(
94141
"Unknown function `$identifier($types)"
95142
})
96143

144+
public data class UnknownAggregateFunction(
145+
val identifier: Identifier,
146+
val args: List<StaticType>,
147+
) : PlanningProblemDetails(ProblemSeverity.ERROR, {
148+
val types = args.joinToString { "<${it.toString().lowercase()}>" }
149+
"Unknown aggregate function `$identifier($types)"
150+
})
151+
97152
public object ExpressionAlwaysReturnsNullOrMissing : PlanningProblemDetails(
98153
severity = ProblemSeverity.ERROR,
99154
messageFormatter = { "Expression always returns null or missing." }

partiql-planner/src/main/kotlin/org/partiql/planner/internal/PartiQLHeader.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -702,13 +702,13 @@ internal object PartiQLHeader : Header() {
702702
private fun count() = listOf(
703703
FunctionSignature.Aggregation(
704704
name = "count",
705-
returns = INT32,
705+
returns = INT64,
706706
parameters = listOf(FunctionParameter("value", ANY)),
707707
isNullable = false,
708708
),
709709
FunctionSignature.Aggregation(
710710
name = "count_star",
711-
returns = INT32,
711+
returns = INT64,
712712
parameters = listOf(),
713713
isNullable = false,
714714
),
@@ -741,6 +741,15 @@ internal object PartiQLHeader : Header() {
741741
)
742742
}
743743

744+
/**
745+
* According to SQL:1999 Section 6.16 Syntax Rule 14.c and Rule 14.d:
746+
* > If AVG is specified and DT is exact numeric, then the declared type of the result is exact
747+
* numeric with implementation-defined precision not less than the precision of DT and
748+
* implementation-defined scale not less than the scale of DT.
749+
*
750+
* > If DT is approximate numeric, then the declared type of the result is approximate numeric
751+
* with implementation-defined precision not less than the precision of DT.
752+
*/
744753
private fun avg() = types.numeric.map {
745754
FunctionSignature.Aggregation(
746755
name = "avg",

partiql-planner/src/main/kotlin/org/partiql/planner/internal/transforms/PlanTransform.kt

+36-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.partiql.planner.internal.transforms
22

3+
import org.partiql.errors.Problem
34
import org.partiql.errors.ProblemCallback
5+
import org.partiql.errors.UNKNOWN_PROBLEM_LOCATION
46
import org.partiql.plan.PlanNode
57
import org.partiql.plan.partiQLPlan
8+
import org.partiql.planner.PlanningProblemDetails
69
import org.partiql.planner.internal.ir.Agg
710
import org.partiql.planner.internal.ir.Catalog
811
import org.partiql.planner.internal.ir.Fn
@@ -12,7 +15,10 @@ import org.partiql.planner.internal.ir.Rel
1215
import org.partiql.planner.internal.ir.Rex
1316
import org.partiql.planner.internal.ir.Statement
1417
import org.partiql.planner.internal.ir.visitor.PlanBaseVisitor
18+
import org.partiql.planner.internal.utils.PlanUtils
19+
import org.partiql.types.function.FunctionSignature
1520
import org.partiql.value.PartiQLValueExperimental
21+
import org.partiql.value.PartiQLValueType
1622

1723
/**
1824
* This is an internal utility to translate from the internal unresolved plan used for typing to the public plan IR.
@@ -58,7 +64,7 @@ internal object PlanTransform : PlanBaseVisitor<PlanNode, ProblemCallback>() {
5864
override fun visitAggResolved(node: Agg.Resolved, ctx: ProblemCallback) = org.partiql.plan.Agg(node.signature)
5965

6066
override fun visitAggUnresolved(node: Agg.Unresolved, ctx: ProblemCallback): org.partiql.plan.Rex.Op {
61-
error("Unresolved aggregation ${node.identifier}")
67+
error("Internal error: This should have been handled somewhere else. Cause: Unresolved aggregation ${node.identifier}.")
6268
}
6369

6470
override fun visitStatement(node: Statement, ctx: ProblemCallback) =
@@ -342,11 +348,37 @@ internal object PlanTransform : PlanBaseVisitor<PlanNode, ProblemCallback>() {
342348
groups = node.groups.map { visitRex(it, ctx) },
343349
)
344350

345-
override fun visitRelOpAggregateCall(node: Rel.Op.Aggregate.Call, ctx: ProblemCallback) =
346-
org.partiql.plan.Rel.Op.Aggregate.Call(
347-
agg = visitAgg(node.agg, ctx),
351+
@OptIn(PartiQLValueExperimental::class)
352+
override fun visitRelOpAggregateCall(node: Rel.Op.Aggregate.Call, ctx: ProblemCallback): org.partiql.plan.Rel.Op.Aggregate.Call {
353+
val agg = when (val agg = node.agg) {
354+
is Agg.Unresolved -> {
355+
val name = PlanUtils.identifierToString(visitIdentifier(agg.identifier, ctx))
356+
ctx.invoke(
357+
Problem(
358+
UNKNOWN_PROBLEM_LOCATION,
359+
PlanningProblemDetails.UnknownAggregateFunction(
360+
visitIdentifier(agg.identifier, ctx),
361+
node.args.map { it.type }
362+
)
363+
)
364+
)
365+
org.partiql.plan.Agg(
366+
FunctionSignature.Aggregation(
367+
"UNKNOWN_AGG::$name",
368+
returns = PartiQLValueType.MISSING,
369+
parameters = emptyList()
370+
)
371+
)
372+
}
373+
is Agg.Resolved -> {
374+
visitAggResolved(agg, ctx)
375+
}
376+
}
377+
return org.partiql.plan.Rel.Op.Aggregate.Call(
378+
agg = agg,
348379
args = node.args.map { visitRex(it, ctx) },
349380
)
381+
}
350382

351383
override fun visitRelOpExclude(node: Rel.Op.Exclude, ctx: ProblemCallback) = org.partiql.plan.Rel.Op.Exclude(
352384
input = visitRel(node.input, ctx),

partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/PlanTyper.kt

+13-13
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import org.partiql.planner.internal.ir.rexOpStructField
7070
import org.partiql.planner.internal.ir.rexOpTupleUnion
7171
import org.partiql.planner.internal.ir.statementQuery
7272
import org.partiql.planner.internal.ir.util.PlanRewriter
73+
import org.partiql.planner.internal.transforms.PlanTransform
7374
import org.partiql.spi.BindingCase
7475
import org.partiql.spi.BindingName
7576
import org.partiql.spi.BindingPath
@@ -451,8 +452,8 @@ internal class PlanTyper(
451452
}
452453
val resolvedVar = env.resolve(path, locals, strategy)
453454
if (resolvedVar == null) {
454-
handleUndefinedVariable(path.steps.last())
455-
return rex(ANY, rexOpErr("Undefined variable ${node.identifier}"))
455+
val details = handleUndefinedVariable(node.identifier, locals.schema.map { it.name }.toSet())
456+
return rex(ANY, rexOpErr(details.message))
456457
}
457458
return visitRex(resolvedVar, null)
458459
}
@@ -1266,19 +1267,11 @@ internal class PlanTyper(
12661267
* to each row of T and eliminating null values <--- all NULL values are eliminated as inputs
12671268
*/
12681269
fun resolveAgg(agg: Agg.Unresolved, arguments: List<Rex>): Pair<Rel.Op.Aggregate.Call, StaticType> {
1269-
var missingArg = false
12701270
val args = arguments.map {
1271-
val arg = visitRex(it, null)
1272-
if (arg.type.isMissable()) missingArg = true
1271+
val arg = visitRex(it, it.type)
12731272
arg
12741273
}
12751274

1276-
//
1277-
if (missingArg) {
1278-
handleAlwaysMissing()
1279-
return relOpAggregateCall(agg, listOf(rexErr("MISSING"))) to MissingType
1280-
}
1281-
12821275
// Try to match the arguments to functions defined in the catalog
12831276
return when (val match = env.resolveAgg(agg, args)) {
12841277
is FnMatch.Ok -> {
@@ -1399,13 +1392,20 @@ internal class PlanTyper(
13991392

14001393
// ERRORS
14011394

1402-
private fun handleUndefinedVariable(name: BindingName) {
1395+
/**
1396+
* Invokes [onProblem] with a newly created [PlanningProblemDetails.UndefinedVariable] and returns the
1397+
* [PlanningProblemDetails.UndefinedVariable].
1398+
*/
1399+
private fun handleUndefinedVariable(name: Identifier, locals: Set<String>): PlanningProblemDetails.UndefinedVariable {
1400+
val planName = PlanTransform.visitIdentifier(name, onProblem)
1401+
val details = PlanningProblemDetails.UndefinedVariable(planName, locals)
14031402
onProblem(
14041403
Problem(
14051404
sourceLocation = UNKNOWN_PROBLEM_LOCATION,
1406-
details = PlanningProblemDetails.UndefinedVariable(name.name, name.bindingCase == BindingCase.SENSITIVE)
1405+
details = details
14071406
)
14081407
)
1408+
return details
14091409
}
14101410

14111411
private fun handleUnexpectedType(actual: StaticType, expected: Set<StaticType>) {

partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeEnv.kt

+61-20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import org.partiql.planner.internal.ir.rexOpVarResolved
1010
import org.partiql.spi.BindingCase
1111
import org.partiql.spi.BindingName
1212
import org.partiql.spi.BindingPath
13+
import org.partiql.types.AnyOfType
14+
import org.partiql.types.AnyType
1315
import org.partiql.types.StaticType
1416
import org.partiql.types.StructType
1517
import org.partiql.types.TupleConstraint
@@ -85,30 +87,28 @@ internal class TypeEnv(public val schema: List<Rel.Binding>) {
8587
for (i in schema.indices) {
8688
val local = schema[i]
8789
val type = local.type
88-
if (type is StructType) {
89-
when (type.containsKey(name)) {
90-
true -> {
91-
if (c != null && known) {
92-
// TODO root was already definitively matched, emit ambiguous error.
93-
return null
94-
}
95-
c = rex(type, rexOpVarResolved(i))
96-
known = true
90+
when (type.containsKey(name)) {
91+
true -> {
92+
if (c != null && known) {
93+
// TODO root was already definitively matched, emit ambiguous error.
94+
return null
9795
}
98-
null -> {
99-
if (c != null) {
100-
if (known) {
101-
continue
102-
} else {
103-
// TODO we have more than one possible match, emit ambiguous error.
104-
return null
105-
}
96+
c = rex(type, rexOpVarResolved(i))
97+
known = true
98+
}
99+
null -> {
100+
if (c != null) {
101+
if (known) {
102+
continue
103+
} else {
104+
// TODO we have more than one possible match, emit ambiguous error.
105+
return null
106106
}
107-
c = rex(type, rexOpVarResolved(i))
108-
known = false
109107
}
110-
false -> continue
108+
c = rex(type, rexOpVarResolved(i))
109+
known = false
111110
}
111+
false -> continue
112112
}
113113
}
114114
return c
@@ -152,4 +152,45 @@ internal class TypeEnv(public val schema: List<Rel.Binding>) {
152152
val closed = constraints.contains(TupleConstraint.Open(false))
153153
return if (closed) false else null
154154
}
155+
156+
/**
157+
* Searches for the [BindingName] within the given [StaticType].
158+
*
159+
* Returns
160+
* - true iff known to contain key
161+
* - false iff known to NOT contain key
162+
* - null iff NOT known to contain key
163+
*
164+
* @param name
165+
* @return
166+
*/
167+
private fun StaticType.containsKey(name: BindingName): Boolean? {
168+
return when (val type = this.flatten()) {
169+
is StructType -> type.containsKey(name)
170+
is AnyOfType -> {
171+
var anyKnownToContainKey = false
172+
var anyKnownToNotContainKey = false
173+
var anyNotKnownToContainKey = false
174+
for (t in type.allTypes) {
175+
val containsKey = t.containsKey(name)
176+
anyKnownToContainKey = anyKnownToContainKey || (containsKey == true)
177+
anyKnownToNotContainKey = anyKnownToNotContainKey || (containsKey == false)
178+
anyNotKnownToContainKey = anyNotKnownToContainKey || (containsKey == null)
179+
}
180+
when {
181+
// There are:
182+
// - No subtypes that are known to not contain the key
183+
// - No subtypes that are not known to contain the key
184+
anyKnownToNotContainKey.not() && anyNotKnownToContainKey.not() -> true
185+
// There are:
186+
// - No subtypes that are known to contain the key
187+
// - No subtypes that are not known to contain the key
188+
anyKnownToContainKey.not() && anyNotKnownToContainKey.not() -> false
189+
else -> null
190+
}
191+
}
192+
is AnyType -> null
193+
else -> false
194+
}
195+
}
155196
}

partiql-planner/src/main/kotlin/org/partiql/planner/internal/typer/TypeUtils.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ internal fun StaticType.toRuntimeType(): PartiQLValueType {
9393
// handle anyOf(null, T) cases
9494
val t = types.filter { it !is NullType && it !is MissingType }
9595
return if (t.size != 1) {
96-
error("Cannot have a UNION runtime type: $this")
96+
PartiQLValueType.ANY
9797
} else {
9898
t.first().asRuntimeType()
9999
}

0 commit comments

Comments
 (0)