Skip to content

Commit 32643d5

Browse files
authored
Merge pull request #1271 from partiql/modeling-paths
Modifies how we model paths in the plan
2 parents 8662f3b + b9330c9 commit 32643d5

File tree

11 files changed

+264
-47
lines changed

11 files changed

+264
-47
lines changed

partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/PartiQLSchemaInferencerTests.kt

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import org.partiql.types.BagType
3535
import org.partiql.types.ListType
3636
import org.partiql.types.SexpType
3737
import org.partiql.types.StaticType
38+
import org.partiql.types.StaticType.Companion.ANY
3839
import org.partiql.types.StaticType.Companion.BAG
3940
import org.partiql.types.StaticType.Companion.BOOL
4041
import org.partiql.types.StaticType.Companion.DATE
@@ -2386,6 +2387,124 @@ class PartiQLSchemaInferencerTests {
23862387
)
23872388
)
23882389
),
2390+
SuccessTestCase(
2391+
name = "Pathing into resolved local variable without qualification and with sensitivity",
2392+
query = """
2393+
SELECT address."street" AS s FROM employer;
2394+
""",
2395+
catalog = "pql",
2396+
catalogPath = listOf("main"),
2397+
expected = BagType(
2398+
StructType(
2399+
fields = mapOf(
2400+
"s" to STRING,
2401+
),
2402+
contentClosed = true,
2403+
constraints = setOf(
2404+
TupleConstraint.Open(false),
2405+
TupleConstraint.UniqueAttrs(true),
2406+
TupleConstraint.Ordered
2407+
)
2408+
)
2409+
)
2410+
),
2411+
SuccessTestCase(
2412+
name = "Pathing into resolved local variable without qualification and with indexing syntax",
2413+
query = """
2414+
SELECT address['street'] AS s FROM employer;
2415+
""",
2416+
catalog = "pql",
2417+
catalogPath = listOf("main"),
2418+
expected = BagType(
2419+
StructType(
2420+
fields = mapOf(
2421+
"s" to STRING,
2422+
),
2423+
contentClosed = true,
2424+
constraints = setOf(
2425+
TupleConstraint.Open(false),
2426+
TupleConstraint.UniqueAttrs(true),
2427+
TupleConstraint.Ordered
2428+
)
2429+
)
2430+
)
2431+
),
2432+
SuccessTestCase(
2433+
name = "Pathing into resolved local variable without qualification and with indexing syntax and fully-qualified FROM",
2434+
query = """
2435+
SELECT e.address['street'] AS s FROM "pql"."main"."employer" AS e;
2436+
""",
2437+
expected = BagType(
2438+
StructType(
2439+
fields = mapOf(
2440+
"s" to STRING,
2441+
),
2442+
contentClosed = true,
2443+
constraints = setOf(
2444+
TupleConstraint.Open(false),
2445+
TupleConstraint.UniqueAttrs(true),
2446+
TupleConstraint.Ordered
2447+
)
2448+
)
2449+
)
2450+
),
2451+
ErrorTestCase(
2452+
name = "Show that we can't use [<string>] to reference a value in a schema. It can only be used on tuples.",
2453+
query = """
2454+
SELECT VALUE 1 FROM "pql"."main"['employer'] AS e;
2455+
""",
2456+
expected = BagType(INT4),
2457+
problemHandler = assertProblemExists {
2458+
Problem(
2459+
UNKNOWN_PROBLEM_LOCATION,
2460+
PlanningProblemDetails.UndefinedVariable("main", true)
2461+
)
2462+
}
2463+
),
2464+
ErrorTestCase(
2465+
name = "Show that we can't use [<string>] to reference a schema in a catalog. It can only be used on tuples.",
2466+
query = """
2467+
SELECT VALUE 1 FROM "pql"['main']."employer" AS e;
2468+
""",
2469+
expected = BagType(INT4),
2470+
problemHandler = assertProblemExists {
2471+
Problem(
2472+
UNKNOWN_PROBLEM_LOCATION,
2473+
PlanningProblemDetails.UndefinedVariable("pql", true)
2474+
)
2475+
}
2476+
),
2477+
SuccessTestCase(
2478+
name = "Tuple indexing syntax on literal tuple with literal string key",
2479+
query = """
2480+
{ 'aBc': 1, 'AbC': 2.0 }['AbC'];
2481+
""",
2482+
expected = DECIMAL
2483+
),
2484+
// This should fail because the Spec says tuple indexing MUST use a literal string or explicit cast.
2485+
ErrorTestCase(
2486+
name = "Array indexing syntax on literal tuple with non-literal and non-cast key",
2487+
query = """
2488+
{ 'aBc': 1, 'AbC': 2.0 }['Ab' || 'C'];
2489+
""",
2490+
expected = MISSING,
2491+
problemHandler = assertProblemExists {
2492+
Problem(
2493+
sourceLocation = UNKNOWN_PROBLEM_LOCATION,
2494+
details = PlanningProblemDetails.ExpressionAlwaysReturnsNullOrMissing
2495+
)
2496+
}
2497+
),
2498+
// The reason this is ANY is because we do not have support for constant-folding. We don't know what
2499+
// CAST('Ab' || 'C' AS STRING) will evaluate to, and therefore, we don't know what the indexing operation
2500+
// will return.
2501+
SuccessTestCase(
2502+
name = "Tuple indexing syntax on literal tuple with explicit cast key",
2503+
query = """
2504+
{ 'aBc': 1, 'AbC': 2.0 }[CAST('Ab' || 'C' AS STRING)];
2505+
""",
2506+
expected = ANY
2507+
),
23892508
)
23902509

23912510
@JvmStatic

partiql-plan/src/main/resources/partiql_plan.ion

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,21 @@ rex::{
7878
steps: list::[step],
7979
_: [
8080
step::[
81+
// The key MUST be an integer expression. Ex: a[0], a[1 + 1]
8182
index::{ key: rex },
82-
symbol::{ identifier: '.identifier.symbol' },
83+
84+
// Case-sensitive lookup. The key MUST be a string expression. Ex: a["b"], a."b", a[CAST(b AS STRING)]
85+
key::{ key: rex },
86+
87+
// Case-insensitive lookup. The key MUST be a literal string. Ex: a.b
88+
symbol::{ key: string },
89+
90+
// For arrays. Ex: a[*]
91+
// TODO: Do we need this? According to specification: [1,2,3][*] ⇔ SELECT VALUE v FROM [1, 2, 3] AS v
8392
wildcard::{},
93+
94+
// For tuples. Ex: a.*
95+
// TODO: Do we need this? According to specification: {'a':1, 'b':2}.* ⇔ SELECT VALUE v FROM UNPIVOT {'a':1, 'b':2} AS v
8496
unpivot::{},
8597
],
8698
],

partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Nodes.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ internal data class Rex(
415415
is Symbol -> visitor.visitRexOpPathStepSymbol(this, ctx)
416416
is Wildcard -> visitor.visitRexOpPathStepWildcard(this, ctx)
417417
is Unpivot -> visitor.visitRexOpPathStepUnpivot(this, ctx)
418+
is Key -> visitor.visitRexOpPathStepKey(this, ctx)
418419
}
419420

420421
internal data class Index(
@@ -436,6 +437,44 @@ internal data class Rex(
436437
}
437438
}
438439

440+
/**
441+
* This represents a case-sensitive lookup on a tuple. Ex: a['b'] or a[CAST('a' || 'b' AS STRING)].
442+
* This would normally contain the dot notation for case-sensitive lookup, however, due to
443+
* limitations -- we cannot consolidate these. See [Symbol] for more information.
444+
*
445+
* The main difference is that this does NOT include `a."b"`
446+
*/
447+
internal data class Key(
448+
@JvmField
449+
internal val key: Rex,
450+
) : Step() {
451+
internal override val children: List<PlanNode> by lazy {
452+
val kids = mutableListOf<PlanNode?>()
453+
kids.add(key)
454+
kids.filterNotNull()
455+
}
456+
457+
internal override fun <R, C> accept(visitor: PlanVisitor<R, C>, ctx: C): R =
458+
visitor.visitRexOpPathStepKey(this, ctx)
459+
460+
internal companion object {
461+
@JvmStatic
462+
internal fun builder(): RexOpPathStepIndexBuilder = RexOpPathStepIndexBuilder()
463+
}
464+
}
465+
466+
/**
467+
* This represents a lookup on a tuple. We differentiate a [Key] and a [Symbol] at this point in the
468+
* pipeline because we NEED to retain some syntactic knowledge for the following reason: we cannot
469+
* use the syntactic index operation on a schema -- as it is not synonymous with a tuple. In other words,
470+
* `<schema-name>."<value-name>"` is not interchangeable with `<schema-name>['<value-name>']`.
471+
*
472+
* So, in order to temporarily differentiate the `a."b"` from `a['b']` (see [Key]), we need to maintain
473+
* the syntactic difference here. Note that this would potentially be mitigated by typing during the AST to Plan
474+
* transformation.
475+
*
476+
* That being said, this represents a lookup on a tuple such as `a.b` or `a."b"`.
477+
*/
439478
internal data class Symbol(
440479
@JvmField
441480
internal val identifier: Identifier.Symbol,

partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/Plan.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ internal fun rexOpPath(root: Rex, steps: List<Rex.Op.Path.Step>): Rex.Op.Path =
5353

5454
internal fun rexOpPathStepIndex(key: Rex): Rex.Op.Path.Step.Index = Rex.Op.Path.Step.Index(key)
5555

56+
internal fun rexOpPathStepKey(key: Rex): Rex.Op.Path.Step.Key = Rex.Op.Path.Step.Key(key)
57+
5658
internal fun rexOpPathStepSymbol(identifier: Identifier.Symbol): Rex.Op.Path.Step.Symbol =
5759
Rex.Op.Path.Step.Symbol(identifier)
5860

partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/builder/PlanBuilder.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@ internal class PlanBuilder {
165165
return builder.build()
166166
}
167167

168+
internal fun rexOpPathStepKey(
169+
key: Rex? = null,
170+
block: RexOpPathStepKeyBuilder.() -> Unit = {},
171+
): Rex.Op.Path.Step.Key {
172+
val builder = RexOpPathStepKeyBuilder(key)
173+
builder.block()
174+
return builder.build()
175+
}
176+
168177
internal fun rexOpPathStepSymbol(
169178
identifier: Identifier.Symbol? = null,
170179
block: RexOpPathStepSymbolBuilder.() -> Unit = {},

partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/builder/PlanBuilders.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,16 @@ internal class RexOpPathStepIndexBuilder(
239239
internal fun build(): Rex.Op.Path.Step.Index = Rex.Op.Path.Step.Index(key = key!!)
240240
}
241241

242+
internal class RexOpPathStepKeyBuilder(
243+
internal var key: Rex? = null,
244+
) {
245+
internal fun key(key: Rex?): RexOpPathStepKeyBuilder = this.apply {
246+
this.key = key
247+
}
248+
249+
internal fun build(): Rex.Op.Path.Step.Key = Rex.Op.Path.Step.Key(key = key!!)
250+
}
251+
242252
internal class RexOpPathStepSymbolBuilder(
243253
internal var identifier: Identifier.Symbol? = null,
244254
) {

partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/visitor/PlanBaseVisitor.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ internal abstract class PlanBaseVisitor<R, C> : PlanVisitor<R, C> {
9797

9898
override fun visitRexOpPathStep(node: Rex.Op.Path.Step, ctx: C): R = when (node) {
9999
is Rex.Op.Path.Step.Index -> visitRexOpPathStepIndex(node, ctx)
100+
is Rex.Op.Path.Step.Key -> visitRexOpPathStepKey(node, ctx)
100101
is Rex.Op.Path.Step.Symbol -> visitRexOpPathStepSymbol(node, ctx)
101102
is Rex.Op.Path.Step.Wildcard -> visitRexOpPathStepWildcard(node, ctx)
102103
is Rex.Op.Path.Step.Unpivot -> visitRexOpPathStepUnpivot(node, ctx)
@@ -105,6 +106,9 @@ internal abstract class PlanBaseVisitor<R, C> : PlanVisitor<R, C> {
105106
override fun visitRexOpPathStepIndex(node: Rex.Op.Path.Step.Index, ctx: C): R =
106107
defaultVisit(node, ctx)
107108

109+
override fun visitRexOpPathStepKey(node: Rex.Op.Path.Step.Key, ctx: C): R =
110+
defaultVisit(node, ctx)
111+
108112
override fun visitRexOpPathStepSymbol(node: Rex.Op.Path.Step.Symbol, ctx: C): R =
109113
defaultVisit(node, ctx)
110114

partiql-planner/src/main/kotlin/org/partiql/planner/internal/ir/visitor/PlanVisitor.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ internal interface PlanVisitor<R, C> {
6262

6363
fun visitRexOpPathStepIndex(node: Rex.Op.Path.Step.Index, ctx: C): R
6464

65+
fun visitRexOpPathStepKey(node: Rex.Op.Path.Step.Key, ctx: C): R
66+
6567
fun visitRexOpPathStepSymbol(node: Rex.Op.Path.Step.Symbol, ctx: C): R
6668

6769
fun visitRexOpPathStepWildcard(node: Rex.Op.Path.Step.Wildcard, ctx: C): R

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package org.partiql.planner.internal.transforms
33
import org.partiql.errors.ProblemCallback
44
import org.partiql.plan.PlanNode
55
import org.partiql.plan.partiQLPlan
6+
import org.partiql.plan.rex
7+
import org.partiql.plan.rexOpLit
8+
import org.partiql.plan.rexOpPathStepKey
9+
import org.partiql.plan.rexOpPathStepSymbol
610
import org.partiql.planner.internal.ir.Agg
711
import org.partiql.planner.internal.ir.Fn
812
import org.partiql.planner.internal.ir.Global
@@ -12,7 +16,9 @@ import org.partiql.planner.internal.ir.Rel
1216
import org.partiql.planner.internal.ir.Rex
1317
import org.partiql.planner.internal.ir.Statement
1418
import org.partiql.planner.internal.ir.visitor.PlanBaseVisitor
19+
import org.partiql.types.StaticType
1520
import org.partiql.value.PartiQLValueExperimental
21+
import org.partiql.value.stringValue
1622

1723
/**
1824
* This is an internal utility to translate from the internal unresolved plan used for typing to the public plan IR.
@@ -118,10 +124,15 @@ internal object PlanTransform : PlanBaseVisitor<PlanNode, ProblemCallback>() {
118124
key = visitRex(node.key, ctx),
119125
)
120126

121-
override fun visitRexOpPathStepSymbol(node: Rex.Op.Path.Step.Symbol, ctx: ProblemCallback) =
122-
org.partiql.plan.Rex.Op.Path.Step.Symbol(
123-
identifier = visitIdentifierSymbol(node.identifier, ctx),
124-
)
127+
@OptIn(PartiQLValueExperimental::class)
128+
override fun visitRexOpPathStepSymbol(node: Rex.Op.Path.Step.Symbol, ctx: ProblemCallback) = when (node.identifier.caseSensitivity) {
129+
Identifier.CaseSensitivity.SENSITIVE -> rexOpPathStepKey(rex(StaticType.STRING, rexOpLit(stringValue(node.identifier.symbol))))
130+
Identifier.CaseSensitivity.INSENSITIVE -> rexOpPathStepSymbol(node.identifier.symbol)
131+
}
132+
133+
override fun visitRexOpPathStepKey(node: Rex.Op.Path.Step.Key, ctx: ProblemCallback): PlanNode = rexOpPathStepKey(
134+
key = visitRex(node.key, ctx)
135+
)
125136

126137
override fun visitRexOpPathStepWildcard(node: Rex.Op.Path.Step.Wildcard, ctx: ProblemCallback) =
127138
org.partiql.plan.Rex.Op.Path.Step.Wildcard()
@@ -130,7 +141,7 @@ internal object PlanTransform : PlanBaseVisitor<PlanNode, ProblemCallback>() {
130141
org.partiql.plan.Rex.Op.Path.Step.Unpivot()
131142

132143
override fun visitRexOpCall(node: Rex.Op.Call, ctx: ProblemCallback) =
133-
super.visitRexOpCall(node, ctx) as org.partiql.plan.Rex.Op.Call
144+
super.visitRexOpCall(node, ctx) as org.partiql.plan.Rex.Op
134145

135146
override fun visitRexOpCallStatic(node: Rex.Op.Call.Static, ctx: ProblemCallback): org.partiql.plan.Rex.Op {
136147
val fn = visitFn(node.fn, ctx)

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import org.partiql.planner.internal.ir.rexOpCollection
3333
import org.partiql.planner.internal.ir.rexOpLit
3434
import org.partiql.planner.internal.ir.rexOpPath
3535
import org.partiql.planner.internal.ir.rexOpPathStepIndex
36+
import org.partiql.planner.internal.ir.rexOpPathStepKey
3637
import org.partiql.planner.internal.ir.rexOpPathStepSymbol
3738
import org.partiql.planner.internal.ir.rexOpPathStepUnpivot
3839
import org.partiql.planner.internal.ir.rexOpPathStepWildcard
@@ -46,6 +47,7 @@ import org.partiql.planner.internal.typer.toStaticType
4647
import org.partiql.types.StaticType
4748
import org.partiql.types.TimeType
4849
import org.partiql.value.PartiQLValueExperimental
50+
import org.partiql.value.StringValue
4951
import org.partiql.value.boolValue
5052
import org.partiql.value.int32Value
5153
import org.partiql.value.int64Value
@@ -145,7 +147,17 @@ internal object RexConverter {
145147
when (it) {
146148
is Expr.Path.Step.Index -> {
147149
val key = visitExprCoerce(it.key, context)
148-
rexOpPathStepIndex(key)
150+
when (val astKey = it.key) {
151+
is Expr.Lit -> when (astKey.value) {
152+
is StringValue -> rexOpPathStepKey(key)
153+
else -> rexOpPathStepIndex(key)
154+
}
155+
is Expr.Cast -> when (astKey.asType is Type.String) {
156+
true -> rexOpPathStepKey(key)
157+
false -> rexOpPathStepIndex(key)
158+
}
159+
else -> rexOpPathStepIndex(key)
160+
}
149161
}
150162
is Expr.Path.Step.Symbol -> {
151163
val identifier = AstToPlan.convert(it.symbol)

0 commit comments

Comments
 (0)