diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Catalog.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Catalog.kt index 8d2c3033a..bf98dbcde 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Catalog.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Catalog.kt @@ -16,35 +16,37 @@ public interface Catalog { /** * Get a table by name. - * - * @param name The case-sensitive [Table] name. - * @return The [Table] or null if not found. */ - public fun getTable(name: Name): Table? = null + public fun getTable(session: Session, name: Name): Table? = null + + /** + * Get a table by identifier; note that identifiers may be case-insensitive. + */ + public fun getTable(session: Session, identifier: Identifier): Table? = null /** * List top-level tables. */ - public fun listTables(): Collection = listTables(Namespace.empty()) + public fun listTables(session: Session): Collection = listTables(session, Namespace.root()) /** * List all tables under this namespace. * * @param namespace */ - public fun listTables(namespace: Namespace): Collection = emptyList() + public fun listTables(session: Session, namespace: Namespace): Collection = emptyList() /** * List top-level namespaces from the catalog. */ - public fun listNamespaces(): Collection = listNamespaces(Namespace.empty()) + public fun listNamespaces(session: Session): Collection = listNamespaces(session, Namespace.root()) /** * List all child namespaces from the namespace. * * @param namespace */ - public fun listNamespaces(namespace: Namespace): Collection = emptyList() + public fun listNamespaces(session: Session, namespace: Namespace): Collection = emptyList() /** * Get a routine's variants by name. @@ -52,5 +54,5 @@ public interface Catalog { * @param name The case-sensitive [Routine] name. * @return A collection of all [Routine]s in the current namespace with this name. */ - public fun getRoutines(name: Name): Collection = emptyList() + public fun getRoutines(session: Session, name: Name): Collection = emptyList() } diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Catalogs.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Catalogs.kt new file mode 100644 index 000000000..537998e85 --- /dev/null +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Catalogs.kt @@ -0,0 +1,29 @@ +package org.partiql.planner.catalog + +/** + * Catalogs is used to provide the default catalog and possibly others by name. + */ +public interface Catalogs { + + /** + * Returns the default catalog. Required. + */ + public fun default(): Catalog + + /** + * Returns a catalog by name (single identifier). + */ + public fun get(name: String, ignoreCase: Boolean = false): Catalog? { + val default = default() + return if (name.equals(default.getName(), ignoreCase)) { + default + } else { + null + } + } + + /** + * Returns a list of all available catalogs. + */ + public fun list(): Collection = listOf(default()) +} diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Identifier.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Identifier.kt new file mode 100644 index 000000000..ade423191 --- /dev/null +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Identifier.kt @@ -0,0 +1,173 @@ +package org.partiql.planner.catalog + +/** + * Represents an SQL identifier (possibly qualified). + * + * @property qualifier If + * @property identifier + */ +public class Identifier private constructor( + private val qualifier: Array, + private val identifier: Part, +) { + + /** + * Returns the unqualified name part. + */ + public fun getIdentifier(): Part = identifier + + /** + * Returns the name's namespace. + */ + public fun getQualifier(): Array = qualifier + + /** + * Returns true if the namespace is non-empty. + */ + public fun hasQualifier(): Boolean = qualifier.isNotEmpty() + + /** + * Compares one identifier to another, possibly ignoring case. + */ + public fun matches(other: Identifier, ignoreCase: Boolean = false): Boolean { + // + if (this.qualifier.size != other.qualifier.size) { + return false + } + // Compare identifier + if (ignoreCase && !(this.identifier.matches(other.identifier))) { + return false + } else if (this.identifier != other.identifier) { + return false + } + for (i in this.qualifier.indices) { + val lhs = this.qualifier[i] + val rhs = other.qualifier[i] + if (ignoreCase && !lhs.matches(rhs)) { + return false + } else if (lhs != rhs) { + return false + } + } + return true + } + + /** + * Compares the case-preserved text of two identifiers — that is case-sensitive equality. + */ + public override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + other as Identifier + return (this.identifier == other.identifier && this.qualifier.contentEquals(other.qualifier)) + } + + /** + * The hashCode() is case-sensitive — java.util.Arrays.hashCode + */ + public override fun hashCode(): Int { + var result = 1 + result = 31 * result + qualifier.hashCode() + result = 31 * result + identifier.hashCode() + return result + } + + /** + * Return the SQL representation of this identifier. + */ + public override fun toString(): String = buildString { + if (qualifier.isNotEmpty()) { + append(qualifier.joinToString(".")) + append(".") + } + append(identifier) + } + + /** + * Represents an SQL identifier part which is either regular (unquoted) or delimited (double-quoted). + * + * @property text The case-preserved identifier text. + * @property regular True if the identifier should be treated as an SQL regular identifier. + */ + public class Part private constructor( + private val text: String, + private val regular: Boolean, + ) { + + /** + * Compares two identifiers, ignoring case iff at least one identifier is non-delimited. + */ + public fun matches(other: Part): Boolean { + return this.text.equals(other.text, ignoreCase = (this.regular || other.regular)) + } + + /** + * Compares the case-preserved text of two identifiers — that is case-sensitive equality. + */ + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + return this.text == (other as Part).text + } + + /** + * Returns the hashcode of the identifier's case-preserved text. + */ + override fun hashCode(): Int { + return this.text.hashCode() + } + + /** + * Return the identifier as a SQL string. + */ + override fun toString(): String = when (regular) { + true -> "\"${text}\"" + false -> text + } + + public companion object { + + @JvmStatic + public fun regular(text: String): Part = Part(text, true) + + @JvmStatic + public fun delimited(text: String): Part = Part(text, false) + } + } + + public companion object { + + @JvmStatic + public fun regular(text: String): Identifier = Identifier(emptyArray(), Part.regular(text)) + + @JvmStatic + public fun delimited(text: String): Identifier = Identifier(emptyArray(), Part.delimited(text)) + + @JvmStatic + public fun of(part: Part): Identifier = Identifier(emptyArray(), part) + + @JvmStatic + public fun of(vararg parts: Part): Identifier = TODO() + + @JvmStatic + public fun of(parts: Collection): Identifier = TODO() + + @JvmStatic + public fun of(vararg parts: String): Identifier { + TODO() + } + + @JvmStatic + public fun of(parts: Collection): Identifier { + TODO() + } + } +} diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Name.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Name.kt index 347801804..596b97c4a 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Name.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Name.kt @@ -1,28 +1,79 @@ package org.partiql.planner.catalog /** - * Thin wrapper over a list of strings. + * A reference to a named object in a catalog; case-preserved. */ -public data class Name( +public class Name( private val namespace: Namespace, private val name: String, ) { + /** + * Returns the unqualified name part. + */ + public fun getName(): String = name + + /** + * Returns the name's namespace. + */ public fun getNamespace(): Namespace = namespace + /** + * Returns true if the namespace is non-empty. + */ public fun hasNamespace(): Boolean = !namespace.isEmpty() - public fun getName(): String = name + /** + * Compares two names including their namespaces and symbols. + */ + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + other as Name + return (this.name == other.name) && (this.namespace == other.namespace) + } + + /** + * The hashCode() is case-sensitive. + */ + override fun hashCode(): Int { + var result = 1 + result = 31 * result + namespace.hashCode() + result = 31 * result + name.hashCode() + return result + } + + /** + * Return the SQL name representation of this name — all parts delimited. + */ + override fun toString(): String { + val parts = mutableListOf() + parts.addAll(namespace.getLevels()) + parts.add(name) + return Identifier.of(parts).toString() + } public companion object { + /** + * Construct a name from a string. + */ + @JvmStatic + public fun of(vararg names: String): Name = of(names.toList()) + + /** + * Construct a name from a collection of strings. + */ @JvmStatic - public fun of(vararg names: String): Name { - assert(names.size > 1) { "Cannot create an empty" } - return Name( - namespace = Namespace.of(*names.drop(1).toTypedArray()), - name = names.last(), - ) + public fun of(names: Collection): Name { + assert(names.size > 1) { "Cannot create an empty name" } + val namespace = Namespace.of(names.drop(1)) + val name = names.last() + return Name(namespace, name) } } } diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Namespace.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Namespace.kt index fbbb603d0..932f4e99c 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Namespace.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Namespace.kt @@ -4,7 +4,7 @@ import java.util.Spliterator import java.util.function.Consumer /** - * A reference to a namespace within a catalog. + * A reference to a namespace within a catalog; case-preserved. * * Related * - Iceberg — https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/catalog/Namespace.java @@ -49,28 +49,33 @@ public class Namespace private constructor( if (other == null || javaClass != other.javaClass) { return false } - val namespace = other as Namespace - return levels.contentEquals(namespace.levels) + return levels.contentEquals((other as Namespace).levels) } + /** + * The hashCode() is case-sensitive — java.util.Arrays.hashCode + */ public override fun hashCode(): Int { return levels.contentHashCode() } + /** + * Return the SQL identifier representation of this namespace. + */ public override fun toString(): String { - return levels.joinToString(".") + return Identifier.of(*levels).toString() } public companion object { - private val EMPTY = Namespace(emptyArray()) + private val ROOT = Namespace(emptyArray()) - public fun empty(): Namespace = EMPTY + public fun root(): Namespace = ROOT @JvmStatic public fun of(vararg levels: String): Namespace { if (levels.isEmpty()) { - return empty() + return root() } return Namespace(arrayOf(*levels)) } @@ -78,7 +83,7 @@ public class Namespace private constructor( @JvmStatic public fun of(levels: Collection): Namespace { if (levels.isEmpty()) { - return empty() + return root() } return Namespace(levels.toTypedArray()) } diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Path.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Path.kt new file mode 100644 index 000000000..4dcd05ec1 --- /dev/null +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Path.kt @@ -0,0 +1,42 @@ +package org.partiql.planner.catalog + +import java.util.Spliterator +import java.util.function.Consumer + +/** + * The routine resolution path, accessible via PATH. + */ +public class Path private constructor(private val namespaces: List) : Iterable { + + public companion object { + + @JvmStatic + public fun of(vararg namespaces: Namespace): Path = Path(namespaces.toList()) + } + + public fun getLength(): Int { + return namespaces.size + } + + public fun isEmpty(): Boolean { + return namespaces.isEmpty() + } + + public operator fun get(index: Int): Namespace { + return namespaces[index] + } + + override fun forEach(action: Consumer?) { + namespaces.forEach(action) + } + + override fun iterator(): Iterator { + return namespaces.iterator() + } + + override fun spliterator(): Spliterator { + return namespaces.spliterator() + } + + override fun toString(): String = "PATH = (${namespaces.joinToString()})" +} diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Session.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Session.kt new file mode 100644 index 000000000..bc79f2ef8 --- /dev/null +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Session.kt @@ -0,0 +1,64 @@ +package org.partiql.planner.catalog + +/** + * Session is used for authorization and name resolution. + */ +public interface Session { + + public companion object { + + private val EMPTY = object : Session { + override fun getIdentity(): String = "unknown" + override fun getNamespace(): Namespace = Namespace.root() + } + + @JvmStatic + public fun empty(): Session = EMPTY + + @JvmStatic + public fun builder(): Builder = Builder() + } + + /** + * Returns the caller identity as a string; accessible via CURRENT_USER. + */ + public fun getIdentity(): String + + /** + * Returns the current [Namespace]; accessible via the NAMESPACE session variable. + */ + public fun getNamespace(): Namespace + + /** + * Returns the current [Path]; accessible via the PATH and CURRENT_PATH session variables. + * + * Default implementation returns the current namespace. + */ + public fun getPath(): Path = Path.of(getNamespace()) + + /** + * Arbitrary session properties that may be used in planning or custom plan passes. + */ + public fun getProperties(): Map = emptyMap() + + /** + * Java-style session builder. + */ + public class Builder { + + private var identity: String = "unknown" + private var namespace: Namespace = Namespace.root() + private var properties: MutableMap = mutableMapOf() + + public fun identity(identity: String): Builder = this.apply { this.identity = identity } + + public fun namespace(namespace: Namespace): Builder = this.apply { this.namespace = namespace } + + public fun property(name: String, value: String): Builder = this.apply { this.properties[name] = value } + + public fun build(): Session = object : Session { + override fun getIdentity(): String = identity + override fun getNamespace(): Namespace = namespace + } + } +} diff --git a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Table.kt b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Table.kt index 738017a17..909fde62f 100644 --- a/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Table.kt +++ b/partiql-planner/src/main/kotlin/org/partiql/planner/catalog/Table.kt @@ -10,7 +10,7 @@ public interface Table { /** * The table's name. */ - public fun getName(): String + public fun getName(): Name /** * The table's schema.