Skip to content

Commit

Permalink
[1/2] Catalog obj resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
rchowell committed Jan 25, 2024
1 parent 65fafeb commit 77760dc
Show file tree
Hide file tree
Showing 18 changed files with 232 additions and 804 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import org.partiql.ast.Statement
import org.partiql.ast.normalize.normalize
import org.partiql.errors.ProblemCallback
import org.partiql.planner.internal.Env
import org.partiql.planner.internal.ir.PartiQLVersion
import org.partiql.planner.internal.transforms.AstToPlan
import org.partiql.planner.internal.transforms.PlanTransform
import org.partiql.planner.internal.typer.PlanTyper
Expand Down
160 changes: 29 additions & 131 deletions partiql-planner/src/main/kotlin/org/partiql/planner/internal/Env.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,162 +2,60 @@ package org.partiql.planner.internal

import org.partiql.planner.PartiQLPlanner
import org.partiql.planner.internal.ir.Catalog
import org.partiql.planner.internal.ir.Rex
import org.partiql.planner.internal.ir.rex
import org.partiql.planner.internal.ir.rexOpGlobal
import org.partiql.planner.internal.typer.TypeEnv.Companion.toPath
import org.partiql.spi.BindingPath
import org.partiql.spi.connector.ConnectorMetadata
import org.partiql.types.StaticType

/**
* [Env] represents the combination of the database type environment (db env) and some local env (type env).
* [Env] is similar to the database type environment from the PartiQL Specification. This includes resolution of
* database binding values and scoped functions.
*
* See [TypeEnv] for the variables type environment.
*
* @property session
*/
internal class Env(private val session: PartiQLPlanner.Session) {

/**
* Collect the list of all referenced globals during planning.
*/
public val catalogs = mutableListOf<Catalog>()

/**
* Catalog Metadata for this query session.
* Maintain a list of all resolved catalog symbols (objects and functions).
*/
private val connectors = session.catalogs
private val symbols: Symbols = Symbols.empty()

/**
* Leverages a [FnResolver] to find a matching function defined in the [Header] scalar function catalog.
* Convert the symbols structure into a list of [Catalog] for shipping in the plan.
*/
internal fun resolveFn(fn: Fn.Unresolved, args: List<Rex>) = fnResolver.resolveFn(fn, args)
internal fun catalogs(): List<Catalog> = symbols.build()

/**
* Leverages a [FnResolver] to find a matching function defined in the [Header] aggregation function catalog.
* Current catalog [ConnectorMetadata]. Error if missing from the session.
*/
internal fun resolveAgg(agg: Agg.Unresolved, args: List<Rex>) = fnResolver.resolveAgg(agg, args)
private val catalog: ConnectorMetadata = session.catalogs[session.currentCatalog]
?: error("Session is missing ConnectorMetadata for current catalog ${session.currentCatalog}")

/**
* Fetch global object metadata from the given [BindingPath].
*
* @param catalog Current catalog
* @param path Global identifier path
* @return
* A [PathResolver] for database objects.
*/
fun catalogs(): List<Catalog> = catalogs.map { it.build() }
private val objects: PathResolverObj = PathResolverObj(catalog, session)

/**
* This function looks up a global [BindingPath], returning a global reference expression.
*
* Convert any remaining binding names (tail) to a path expression.
*
* @param path
* @param locals
* @param strategy
* @return
*/
public fun resolve(path: BindingPath, locals: TypeEnv, strategy: ResolutionStrategy): Rex? = when (strategy) {
ResolutionStrategy.LOCAL -> locals.resolve(path) ?: catalog.lookup
ResolutionStrategy.GLOBAL -> global(path) ?: locals.resolve(path)
}


/**
* TODO optimization, check known globals before calling out to connector again
*
* @param catalog
* @param originalPath
* @param catalogPath
* @return
*/
private fun getGlobalType(
catalog: BindingName?,
originalPath: BindingPath,
catalogPath: BindingPath,
): ResolvedVar? {
return catalog?.let { cat ->
getObjectHandle(cat, catalogPath)?.let { handle ->
getObjectDescriptor(handle).let { type ->
val depth = calculateMatched(originalPath, catalogPath, handle.second.absolutePath)
val (catalogIndex, valueIndex) = getOrAddCatalogValue(
handle.first,
handle.second.absolutePath.steps,
type
)
// Return resolution metadata
ResolvedVar(type, catalogIndex, depth, valueIndex)
}
}
}
}


// /**
// * TODO
// *
// * @param fn
// * @return
// */
// @OptIn(FnExperimental::class)
// public fun resolve(fn: Fn.Unresolved, args: List<Rex>): Rex? {
// TODO()
// }

/**
* Attempt to resolve a [BindingPath] in the global + local type environments.
*/
fun resolve(path: BindingPath, locals: TypeEnv, strategy: ResolutionStrategy): Rex? {
return when (strategy) {
ResolutionStrategy.LOCAL -> locals.resolve(path) ?: resolveGlobalBind(path)
ResolutionStrategy.GLOBAL -> resolveGlobalBind(path) ?: locals.resolve(path)
}
}

/**
* Logic is as follows:
* 1. If Current Catalog and Schema are set, create a Path to the object and attempt to grab handle and schema.
* a. If not found, just try to find the object in the catalog.
* 2. If Current Catalog is not set:
* a. Loop through all catalogs and try to find the object.
*
* TODO: Add global bindings
* TODO: Replace paths with global variable references if found
*/
private fun resolveGlobalBind(path: BindingPath): Rex? {
val currentCatalog = session.currentCatalog?.let { BindingName(it, BindingCase.SENSITIVE) }
val currentCatalogPath = BindingPath(session.currentDirectory.map { BindingName(it, BindingCase.SENSITIVE) })
val absoluteCatalogPath = BindingPath(currentCatalogPath.steps + path.steps)
val resolvedVar = when (path.steps.size) {
0 -> null
1 -> getGlobalType(currentCatalog, path, absoluteCatalogPath)
2 -> getGlobalType(currentCatalog, path, path) ?: getGlobalType(currentCatalog, path, absoluteCatalogPath)
else -> {
val inferredCatalog = path.steps[0]
val newPath = BindingPath(path.steps.subList(1, path.steps.size))
getGlobalType(inferredCatalog, path, newPath)
?: getGlobalType(currentCatalog, path, path)
?: getGlobalType(currentCatalog, path, absoluteCatalogPath)
}
} ?: return null
// rewrite as path expression for any remaining steps.
val root = rex(resolvedVar.type, rexOpGlobal(catalogSymbolRef(resolvedVar.ordinal, resolvedVar.position)))
val tail = path.steps.drop(resolvedVar.depth)
fun resolveObj(path: BindingPath): Rex? {
val match = objects.lookup(path) ?: return null
// Insert into symbols, producing a reference
val ref = symbols.insert(match.item)
// Rewrite as an untyped path expression.
val root = rex(StaticType.ANY, rexOpGlobal(ref))
val tail = path.steps.drop(match.depth)
return if (tail.isEmpty()) root else root.toPath(tail)
}

/**
* Logic for determining how many BindingNames were “matched” by the ConnectorMetadata
* 1. Matched = RelativePath - Not Found
* 2. Not Found = Input CatalogPath - Output CatalogPath
* 3. Matched = RelativePath - (Input CatalogPath - Output CatalogPath)
* 4. Matched = RelativePath + Output CatalogPath - Input CatalogPath
*/
private fun calculateMatched(
originalPath: BindingPath,
inputCatalogPath: BindingPath,
outputCatalogPath: ConnectorObjectPath,
): Int {
return originalPath.steps.size + outputCatalogPath.steps.size - inputCatalogPath.steps.size
}

@OptIn(PartiQLValueExperimental::class)
private fun Rex.toPath(steps: List<BindingName>): Rex = steps.fold(this) { curr, step ->
val op = when (step.bindingCase) {
BindingCase.SENSITIVE -> rexOpPathKey(curr, rex(StaticType.STRING, rexOpLit(stringValue(step.name))))
BindingCase.INSENSITIVE -> rexOpPathSymbol(curr, step.name)
}
rex(StaticType.ANY, op)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.partiql.spi.connector.ConnectorHandle
* @property catalog The resolved entity's catalog name.
* @property handle The resolved entity's catalog path and type information.
*/
internal data class PathEntry<T>(
internal data class PathItem<T>(
@JvmField val catalog: String,
@JvmField val handle: ConnectorHandle<T>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.partiql.planner.internal

/**
* Result of searching a path in a catalog.
*
* @param T
* @property depth The depth/level of the path match.
* @property item The metadata for the matched catalog item.
*/
internal data class PathMatch<T>(
@JvmField val depth: Int,
@JvmField val item: PathItem<T>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -32,59 +32,93 @@ internal abstract class PathResolver<T>(
) {

/**
*
* The session's current directory represented as [BindingName] steps.
*/
private val schema = session.currentDirectory.map { it.toBindingName() }

/**
* A [PathResolver] should override this one method for which [ConnectorMetadata] API to call.
*
* @param path The absolute path within a catalog.
* @param path The catalog absolute path.
* @return
*/
abstract fun get(metadata: ConnectorMetadata, path: BindingPath): ConnectorHandle<T>?

/**
* Resolution rules for a given name.
* Lookup a `path` following the scoping rules in the class javadoc.
*
* Returns a pair of the
*
* @param path This represents the exact path
* @return
*/
internal fun resolve(path: BindingPath): PathEntry<T>? {
internal fun lookup(path: BindingPath): PathMatch<T>? {
val n = path.steps.size
val absPath = BindingPath(schema + path.steps)
return when (n) {
0 -> null
1 -> get(absPath)
2 -> get(absPath) ?: get(path)
else -> get(absPath) ?: get(path) ?: lookup(path)
1 -> get(absPath, n)
2 -> get(absPath, n) ?: get(path, n)
else -> get(absPath, n) ?: get(path, n) ?: search(path, n)
}
}

/**
* This gets the path in the current catalog.
*
* @param path
* @param absPath Catalog absolute path.
* @param n Original path step size, used in depth calculation.
* @return
*/
private fun get(path: BindingPath): PathEntry<T>? =
get(catalog, path)?.let { PathEntry(session.currentCatalog, it) }
private fun get(absPath: BindingPath, n: Int): PathMatch<T>? {
val handle = get(catalog, absPath) ?: return null
val depth = depth(n, absPath.steps.size, handle.path.size)
val item = PathItem(session.currentCatalog, handle)
return PathMatch(depth, item)
}

/**
* This looks for an absolute path in the current system, using the session to lookup catalogs.
* This searches with a system absolute path, using the session to lookup catalogs.
*
* @param path
* @param path System absolute path.
* @param n Original path step size, used in depth calculation.
*/
private fun lookup(path: BindingPath): PathEntry<T>? {
val head = path.steps.first()
val tail = BindingPath(path.steps.drop(1))
for ((catalog, metadata) in session.catalogs) {
if (head.matches(catalog)) {
return get(metadata, tail)?.let { PathEntry(catalog, it) }
private fun search(path: BindingPath, n: Int): PathMatch<T>? {
var match: Map.Entry<String, ConnectorMetadata>? = null
val first: BindingName = path.steps.first()
for (catalog in session.catalogs) {
if (first.matches(catalog.key)) {
if (match != null) {
// TODO root was already matched, emit ambiguous error
return null
}
match = catalog
}
}
return null
if (match == null) {
return null
}
// Lookup in the unambiguously matched catalog, calculating the depth matched.
val absPath = BindingPath(path.steps.drop(1))
val catalog = match.key
val metadata = match.value
val handle = get(metadata, absPath) ?: return null
val depth = depth(n, absPath.steps.size, handle.path.size)
val item = PathItem(catalog, handle)
return PathMatch(depth, item)
}

private fun String.toBindingName() = BindingName(this, BindingCase.SENSITIVE)

/**
* Logic for determining how many BindingNames were “matched” by the ConnectorMetadata
*
* 1. Matched = RelativePath - Not Found
* 2. Not Found = Input CatalogPath - Output CatalogPath
* 3. Matched = RelativePath - (Input CatalogPath - Output CatalogPath)
* 4. Matched = RelativePath + Output CatalogPath - Input CatalogPath
*
* Ask johqunn@ as this is too clever for me.
*/
private fun depth(original: Int, input: Int, output: Int): Int = original + output - input
}
Loading

0 comments on commit 77760dc

Please sign in to comment.