Skip to content

Commit 89d38df

Browse files
authored
Isolates local variable resolution logic (#1344)
1 parent d37369e commit 89d38df

File tree

5 files changed

+362
-314
lines changed

5 files changed

+362
-314
lines changed

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

+36-228
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import org.partiql.planner.PartiQLPlanner
44
import org.partiql.planner.internal.ir.Agg
55
import org.partiql.planner.internal.ir.Catalog
66
import org.partiql.planner.internal.ir.Fn
7-
import org.partiql.planner.internal.ir.Identifier
8-
import org.partiql.planner.internal.ir.Rel
97
import org.partiql.planner.internal.ir.Rex
10-
import org.partiql.planner.internal.ir.identifierSymbol
8+
import org.partiql.planner.internal.ir.catalogSymbolRef
9+
import org.partiql.planner.internal.ir.rex
10+
import org.partiql.planner.internal.ir.rexOpGlobal
11+
import org.partiql.planner.internal.ir.rexOpLit
12+
import org.partiql.planner.internal.ir.rexOpPathKey
13+
import org.partiql.planner.internal.ir.rexOpPathSymbol
1114
import org.partiql.planner.internal.typer.FnResolver
15+
import org.partiql.planner.internal.typer.TypeEnv
1216
import org.partiql.spi.BindingCase
1317
import org.partiql.spi.BindingName
1418
import org.partiql.spi.BindingPath
@@ -17,94 +21,29 @@ import org.partiql.spi.connector.ConnectorObjectHandle
1721
import org.partiql.spi.connector.ConnectorObjectPath
1822
import org.partiql.spi.connector.ConnectorSession
1923
import org.partiql.types.StaticType
20-
import org.partiql.types.StructType
21-
import org.partiql.types.TupleConstraint
2224
import org.partiql.types.function.FunctionSignature
25+
import org.partiql.value.PartiQLValueExperimental
26+
import org.partiql.value.stringValue
2327

2428
/**
2529
* Handle for associating a catalog name with catalog related metadata objects.
2630
*/
2731
internal typealias Handle<T> = Pair<String, T>
2832

2933
/**
30-
* TypeEnv represents the environment in which we type expressions and resolve variables while planning.
34+
* Metadata for a resolved global variable
3135
*
32-
* TODO TypeEnv should be a stack of locals; also the strategy has been kept here because it's easier to
33-
* pass through the traversal like this, but is conceptually odd to associate with the TypeEnv.
34-
* @property schema
35-
* @property strategy
36-
*/
37-
internal class TypeEnv(
38-
val schema: List<Rel.Binding>,
39-
val strategy: ResolutionStrategy,
40-
) {
41-
42-
/**
43-
* Return a copy with GLOBAL lookup strategy
44-
*/
45-
fun global() = TypeEnv(schema, ResolutionStrategy.GLOBAL)
46-
47-
/**
48-
* Return a copy with LOCAL lookup strategy
49-
*/
50-
fun local() = TypeEnv(schema, ResolutionStrategy.LOCAL)
51-
52-
/**
53-
* Debug string
54-
*/
55-
override fun toString() = buildString {
56-
append("(")
57-
append("strategy=$strategy")
58-
append(", ")
59-
val bindings = "< " + schema.joinToString { "${it.name}: ${it.type}" } + " >"
60-
append("bindings=$bindings")
61-
append(")")
62-
}
63-
}
64-
65-
/**
66-
* Metadata regarding a resolved variable.
36+
* @property type Resolved StaticType
37+
* @property ordinal The relevant catalog's index offset in the [Env.catalogs] list
6738
* @property depth The depth/level of the path match.
39+
* @property position The relevant value's index offset in the [Catalog.values] list
6840
*/
69-
internal sealed interface ResolvedVar {
70-
71-
public val type: StaticType
72-
public val ordinal: Int
73-
public val depth: Int
74-
75-
/**
76-
* Metadata for a resolved local variable.
77-
*
78-
* @property type Resolved StaticType
79-
* @property ordinal Index offset in [TypeEnv]
80-
* @property resolvedSteps The fully resolved path steps.s
81-
*/
82-
class Local(
83-
override val type: StaticType,
84-
override val ordinal: Int,
85-
val rootType: StaticType,
86-
val resolvedSteps: List<BindingName>,
87-
) : ResolvedVar {
88-
// the depth are always going to be 1 because this is local variable.
89-
// the global path, however the path length maybe, going to be replaced by a binding name.
90-
override val depth: Int = 1
91-
}
92-
93-
/**
94-
* Metadata for a resolved global variable
95-
*
96-
* @property type Resolved StaticType
97-
* @property ordinal The relevant catalog's index offset in the [Env.catalogs] list
98-
* @property depth The depth/level of the path match.
99-
* @property position The relevant value's index offset in the [Catalog.values] list
100-
*/
101-
class Global(
102-
override val type: StaticType,
103-
override val ordinal: Int,
104-
override val depth: Int,
105-
val position: Int,
106-
) : ResolvedVar
107-
}
41+
internal class ResolvedVar(
42+
val type: StaticType,
43+
val ordinal: Int,
44+
val depth: Int,
45+
val position: Int,
46+
)
10847

10948
/**
11049
* Variable resolution strategies — https://partiql.org/assets/PartiQL-Specification.pdf#page=35
@@ -235,7 +174,7 @@ internal class Env(
235174
type
236175
)
237176
// Return resolution metadata
238-
ResolvedVar.Global(type, catalogIndex, depth, valueIndex)
177+
ResolvedVar(type, catalogIndex, depth, valueIndex)
239178
}
240179
}
241180
}
@@ -282,31 +221,13 @@ internal class Env(
282221
}
283222
}
284223

285-
private fun BindingPath.toCaseSensitive(): BindingPath {
286-
return this.copy(steps = this.steps.map { it.copy(bindingCase = BindingCase.SENSITIVE) })
287-
}
288-
289224
/**
290225
* Attempt to resolve a [BindingPath] in the global + local type environments.
291226
*/
292-
fun resolve(path: BindingPath, locals: TypeEnv, scope: Rex.Op.Var.Scope): ResolvedVar? {
293-
val strategy = when (scope) {
294-
Rex.Op.Var.Scope.DEFAULT -> locals.strategy
295-
Rex.Op.Var.Scope.LOCAL -> ResolutionStrategy.LOCAL
296-
}
227+
fun resolve(path: BindingPath, locals: TypeEnv, strategy: ResolutionStrategy): Rex? {
297228
return when (strategy) {
298-
ResolutionStrategy.LOCAL -> {
299-
var type: ResolvedVar? = null
300-
type = type ?: resolveLocalBind(path, locals.schema)
301-
type = type ?: resolveGlobalBind(path)
302-
type
303-
}
304-
ResolutionStrategy.GLOBAL -> {
305-
var type: ResolvedVar? = null
306-
type = type ?: resolveGlobalBind(path)
307-
type = type ?: resolveLocalBind(path, locals.schema)
308-
type
309-
}
229+
ResolutionStrategy.LOCAL -> locals.resolve(path) ?: resolveGlobalBind(path)
230+
ResolutionStrategy.GLOBAL -> resolveGlobalBind(path) ?: locals.resolve(path)
310231
}
311232
}
312233

@@ -320,7 +241,7 @@ internal class Env(
320241
* TODO: Add global bindings
321242
* TODO: Replace paths with global variable references if found
322243
*/
323-
private fun resolveGlobalBind(path: BindingPath): ResolvedVar? {
244+
private fun resolveGlobalBind(path: BindingPath): Rex? {
324245
val currentCatalog = session.currentCatalog?.let { BindingName(it, BindingCase.SENSITIVE) }
325246
val currentCatalogPath = BindingPath(session.currentDirectory.map { BindingName(it, BindingCase.SENSITIVE) })
326247
val absoluteCatalogPath = BindingPath(currentCatalogPath.steps + path.steps)
@@ -335,122 +256,13 @@ internal class Env(
335256
?: getGlobalType(currentCatalog, path, path)
336257
?: getGlobalType(currentCatalog, path, absoluteCatalogPath)
337258
}
338-
}
339-
return resolvedVar
259+
} ?: return null
260+
// rewrite as path expression for any remaining steps.
261+
val root = rex(resolvedVar.type, rexOpGlobal(catalogSymbolRef(resolvedVar.ordinal, resolvedVar.position)))
262+
val tail = path.steps.drop(resolvedVar.depth)
263+
return if (tail.isEmpty()) root else root.toPath(tail)
340264
}
341265

342-
/**
343-
* Check locals, else search structs.
344-
*/
345-
internal fun resolveLocalBind(path: BindingPath, locals: List<Rel.Binding>): ResolvedVar? {
346-
if (path.steps.isEmpty()) {
347-
return null
348-
}
349-
350-
// 1. Check locals for root
351-
locals.forEachIndexed { ordinal, binding ->
352-
val root = path.steps[0]
353-
if (root.isEquivalentTo(binding.name)) {
354-
return ResolvedVar.Local(binding.type, ordinal, binding.type, path.steps)
355-
}
356-
}
357-
358-
// 2. Check if this variable is referencing a struct field, carrying ordinals
359-
val matches = mutableListOf<ResolvedVar.Local>()
360-
for (ordinal in locals.indices) {
361-
val rootType = locals[ordinal].type
362-
val pathPrefix = BindingName(locals[ordinal].name, BindingCase.SENSITIVE)
363-
if (rootType is StructType) {
364-
val varType = inferStructLookup(rootType, path)
365-
if (varType != null) {
366-
// we found this path within a struct!
367-
val match = ResolvedVar.Local(
368-
varType.resolvedType,
369-
ordinal,
370-
rootType,
371-
listOf(pathPrefix) + varType.replacementPath.steps,
372-
)
373-
matches.add(match)
374-
}
375-
}
376-
}
377-
378-
// 0 -> no match
379-
// 1 -> resolved
380-
// N -> ambiguous
381-
return when (matches.size) {
382-
0 -> null
383-
1 -> matches.single()
384-
else -> null // TODO emit ambiguous error
385-
}
386-
}
387-
388-
/**
389-
* Searches for the path within the given struct, returning null if not found.
390-
*
391-
* @return a [ResolvedPath] that contains the disambiguated [ResolvedPath.replacementPath] and the path's
392-
* [StaticType]. Returns NULL if unable to find the [path] given the [struct].
393-
*/
394-
private fun inferStructLookup(struct: StructType, path: BindingPath): ResolvedPath? {
395-
var curr: StaticType = struct
396-
val replacementSteps = path.steps.map { step ->
397-
// Assume ORDERED for now
398-
val currentStruct = curr as? StructType ?: return null
399-
val (replacement, stepType) = inferStructLookup(currentStruct, step) ?: return null
400-
curr = stepType
401-
replacement
402-
}
403-
// Lookup final field
404-
return ResolvedPath(
405-
BindingPath(replacementSteps),
406-
curr
407-
)
408-
}
409-
410-
/**
411-
* Represents a disambiguated [BindingPath] and its inferred [StaticType].
412-
*/
413-
private class ResolvedPath(
414-
val replacementPath: BindingPath,
415-
val resolvedType: StaticType,
416-
)
417-
418-
/**
419-
* @return a disambiguated [key] and the resulting [StaticType].
420-
*/
421-
private fun inferStructLookup(struct: StructType, key: BindingName): Pair<BindingName, StaticType>? {
422-
val isClosed = struct.constraints.contains(TupleConstraint.Open(false))
423-
val isOrdered = struct.constraints.contains(TupleConstraint.Ordered)
424-
return when {
425-
// 1. Struct is closed and ordered
426-
isClosed && isOrdered -> {
427-
struct.fields.firstOrNull { entry -> key.isEquivalentTo(entry.key) }?.let {
428-
(sensitive(it.key) to it.value)
429-
}
430-
}
431-
// 2. Struct is closed
432-
isClosed -> {
433-
val matches = struct.fields.filter { entry -> key.isEquivalentTo(entry.key) }
434-
when (matches.size) {
435-
0 -> null
436-
1 -> matches.first().let { (sensitive(it.key) to it.value) }
437-
else -> {
438-
val firstKey = matches.first().key
439-
val sharedKey = when (matches.all { it.key == firstKey }) {
440-
true -> sensitive(firstKey)
441-
false -> key
442-
}
443-
sharedKey to StaticType.unionOf(matches.map { it.value }.toSet()).flatten()
444-
}
445-
}
446-
}
447-
// 3. Struct is open
448-
else -> key to StaticType.ANY
449-
}
450-
}
451-
452-
private fun sensitive(str: String): BindingName = BindingName(str, BindingCase.SENSITIVE)
453-
454266
/**
455267
* Logic for determining how many BindingNames were “matched” by the ConnectorMetadata
456268
* 1. Matched = RelativePath - Not Found
@@ -466,16 +278,12 @@ internal class Env(
466278
return originalPath.steps.size + outputCatalogPath.steps.size - inputCatalogPath.steps.size
467279
}
468280

469-
private fun String.toIdentifier() = identifierSymbol(
470-
symbol = this,
471-
caseSensitivity = Identifier.CaseSensitivity.SENSITIVE
472-
)
473-
474-
private fun BindingName.toIdentifier() = identifierSymbol(
475-
symbol = name,
476-
caseSensitivity = when (bindingCase) {
477-
BindingCase.SENSITIVE -> Identifier.CaseSensitivity.SENSITIVE
478-
BindingCase.INSENSITIVE -> Identifier.CaseSensitivity.INSENSITIVE
281+
@OptIn(PartiQLValueExperimental::class)
282+
private fun Rex.toPath(steps: List<BindingName>): Rex = steps.fold(this) { curr, step ->
283+
val op = when (step.bindingCase) {
284+
BindingCase.SENSITIVE -> rexOpPathKey(curr, rex(StaticType.STRING, rexOpLit(stringValue(step.name))))
285+
BindingCase.INSENSITIVE -> rexOpPathSymbol(curr, step.name)
479286
}
480-
)
287+
rex(StaticType.ANY, op)
288+
}
481289
}

0 commit comments

Comments
 (0)