Skip to content

Commit b9330c9

Browse files
committed
Modifies how we model paths in the plan
1 parent c23ca8c commit b9330c9

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
@@ -83,9 +83,21 @@ rex::{
8383
steps: list::[step],
8484
_: [
8585
step::[
86+
// The key MUST be an integer expression. Ex: a[0], a[1 + 1]
8687
index::{ key: rex },
87-
symbol::{ identifier: '.identifier.symbol' },
88+
89+
// Case-sensitive lookup. The key MUST be a string expression. Ex: a["b"], a."b", a[CAST(b AS STRING)]
90+
key::{ key: rex },
91+
92+
// Case-insensitive lookup. The key MUST be a literal string. Ex: a.b
93+
symbol::{ key: string },
94+
95+
// For arrays. Ex: a[*]
96+
// TODO: Do we need this? According to specification: [1,2,3][*] ⇔ SELECT VALUE v FROM [1, 2, 3] AS v
8897
wildcard::{},
98+
99+
// For tuples. Ex: a.*
100+
// TODO: Do we need this? According to specification: {'a':1, 'b':2}.* ⇔ SELECT VALUE v FROM UNPIVOT {'a':1, 'b':2} AS v
89101
unpivot::{},
90102
],
91103
],

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
@@ -432,6 +432,7 @@ internal data class Rex(
432432
is Symbol -> visitor.visitRexOpPathStepSymbol(this, ctx)
433433
is Wildcard -> visitor.visitRexOpPathStepWildcard(this, ctx)
434434
is Unpivot -> visitor.visitRexOpPathStepUnpivot(this, ctx)
435+
is Key -> visitor.visitRexOpPathStepKey(this, ctx)
435436
}
436437

437438
internal data class Index(
@@ -453,6 +454,44 @@ internal data class Rex(
453454
}
454455
}
455456

457+
/**
458+
* This represents a case-sensitive lookup on a tuple. Ex: a['b'] or a[CAST('a' || 'b' AS STRING)].
459+
* This would normally contain the dot notation for case-sensitive lookup, however, due to
460+
* limitations -- we cannot consolidate these. See [Symbol] for more information.
461+
*
462+
* The main difference is that this does NOT include `a."b"`
463+
*/
464+
internal data class Key(
465+
@JvmField
466+
internal val key: Rex,
467+
) : Step() {
468+
internal override val children: List<PlanNode> by lazy {
469+
val kids = mutableListOf<PlanNode?>()
470+
kids.add(key)
471+
kids.filterNotNull()
472+
}
473+
474+
internal override fun <R, C> accept(visitor: PlanVisitor<R, C>, ctx: C): R =
475+
visitor.visitRexOpPathStepKey(this, ctx)
476+
477+
internal companion object {
478+
@JvmStatic
479+
internal fun builder(): RexOpPathStepIndexBuilder = RexOpPathStepIndexBuilder()
480+
}
481+
}
482+
483+
/**
484+
* This represents a lookup on a tuple. We differentiate a [Key] and a [Symbol] at this point in the
485+
* pipeline because we NEED to retain some syntactic knowledge for the following reason: we cannot
486+
* use the syntactic index operation on a schema -- as it is not synonymous with a tuple. In other words,
487+
* `<schema-name>."<value-name>"` is not interchangeable with `<schema-name>['<value-name>']`.
488+
*
489+
* So, in order to temporarily differentiate the `a."b"` from `a['b']` (see [Key]), we need to maintain
490+
* the syntactic difference here. Note that this would potentially be mitigated by typing during the AST to Plan
491+
* transformation.
492+
*
493+
* That being said, this represents a lookup on a tuple such as `a.b` or `a."b"`.
494+
*/
456495
internal data class Symbol(
457496
@JvmField
458497
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
@@ -55,6 +55,8 @@ internal fun rexOpPath(root: Rex, steps: List<Rex.Op.Path.Step>): Rex.Op.Path =
5555

5656
internal fun rexOpPathStepIndex(key: Rex): Rex.Op.Path.Step.Index = Rex.Op.Path.Step.Index(key)
5757

58+
internal fun rexOpPathStepKey(key: Rex): Rex.Op.Path.Step.Key = Rex.Op.Path.Step.Key(key)
59+
5860
internal fun rexOpPathStepSymbol(identifier: Identifier.Symbol): Rex.Op.Path.Step.Symbol =
5961
Rex.Op.Path.Step.Symbol(identifier)
6062

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
@@ -174,6 +174,15 @@ internal class PlanBuilder {
174174
return builder.build()
175175
}
176176

177+
internal fun rexOpPathStepKey(
178+
key: Rex? = null,
179+
block: RexOpPathStepKeyBuilder.() -> Unit = {},
180+
): Rex.Op.Path.Step.Key {
181+
val builder = RexOpPathStepKeyBuilder(key)
182+
builder.block()
183+
return builder.build()
184+
}
185+
177186
internal fun rexOpPathStepSymbol(
178187
identifier: Identifier.Symbol? = null,
179188
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
@@ -250,6 +250,16 @@ internal class RexOpPathStepIndexBuilder(
250250
internal fun build(): Rex.Op.Path.Step.Index = Rex.Op.Path.Step.Index(key = key!!)
251251
}
252252

253+
internal class RexOpPathStepKeyBuilder(
254+
internal var key: Rex? = null,
255+
) {
256+
internal fun key(key: Rex?): RexOpPathStepKeyBuilder = this.apply {
257+
this.key = key
258+
}
259+
260+
internal fun build(): Rex.Op.Path.Step.Key = Rex.Op.Path.Step.Key(key = key!!)
261+
}
262+
253263
internal class RexOpPathStepSymbolBuilder(
254264
internal var identifier: Identifier.Symbol? = null,
255265
) {

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
@@ -100,6 +100,7 @@ internal abstract class PlanBaseVisitor<R, C> : PlanVisitor<R, C> {
100100

101101
override fun visitRexOpPathStep(node: Rex.Op.Path.Step, ctx: C): R = when (node) {
102102
is Rex.Op.Path.Step.Index -> visitRexOpPathStepIndex(node, ctx)
103+
is Rex.Op.Path.Step.Key -> visitRexOpPathStepKey(node, ctx)
103104
is Rex.Op.Path.Step.Symbol -> visitRexOpPathStepSymbol(node, ctx)
104105
is Rex.Op.Path.Step.Wildcard -> visitRexOpPathStepWildcard(node, ctx)
105106
is Rex.Op.Path.Step.Unpivot -> visitRexOpPathStepUnpivot(node, ctx)
@@ -108,6 +109,9 @@ internal abstract class PlanBaseVisitor<R, C> : PlanVisitor<R, C> {
108109
override fun visitRexOpPathStepIndex(node: Rex.Op.Path.Step.Index, ctx: C): R =
109110
defaultVisit(node, ctx)
110111

112+
override fun visitRexOpPathStepKey(node: Rex.Op.Path.Step.Key, ctx: C): R =
113+
defaultVisit(node, ctx)
114+
111115
override fun visitRexOpPathStepSymbol(node: Rex.Op.Path.Step.Symbol, ctx: C): R =
112116
defaultVisit(node, ctx)
113117

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
@@ -64,6 +64,8 @@ internal interface PlanVisitor<R, C> {
6464

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

67+
fun visitRexOpPathStepKey(node: Rex.Op.Path.Step.Key, ctx: C): R
68+
6769
fun visitRexOpPathStepSymbol(node: Rex.Op.Path.Step.Symbol, ctx: C): R
6870

6971
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.
@@ -120,10 +126,15 @@ internal object PlanTransform : PlanBaseVisitor<PlanNode, ProblemCallback>() {
120126
key = visitRex(node.key, ctx),
121127
)
122128

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

128139
override fun visitRexOpPathStepWildcard(node: Rex.Op.Path.Step.Wildcard, ctx: ProblemCallback) =
129140
org.partiql.plan.Rex.Op.Path.Step.Wildcard()
@@ -132,7 +143,7 @@ internal object PlanTransform : PlanBaseVisitor<PlanNode, ProblemCallback>() {
132143
org.partiql.plan.Rex.Op.Path.Step.Unpivot()
133144

134145
override fun visitRexOpCall(node: Rex.Op.Call, ctx: ProblemCallback) =
135-
super.visitRexOpCall(node, ctx) as org.partiql.plan.Rex.Op.Call
146+
super.visitRexOpCall(node, ctx) as org.partiql.plan.Rex.Op
136147

137148
override fun visitRexOpCallStatic(node: Rex.Op.Call.Static, ctx: ProblemCallback): org.partiql.plan.Rex.Op {
138149
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)