Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds parsing and modeling for INSERT INTO <table name> #1621

Draft
wants to merge 2 commits into
base: prep-v0_14_10
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Thank you to all who have contributed!
-->

## [0.14.10]

### Experimental Changes
- **BREAKING**: For the _experimental_ `org.partiql.lang.domains`, the DML targets now use the newly created `TableName`,
and `IdentifierChain`. With this, we have added parsing support for qualified identifiers in certain DML operations,
closing [#1595](https://github.com/partiql/partiql-lang-kotlin/issues/1595).

## [0.14.9]

### Changed
Expand Down Expand Up @@ -1127,7 +1134,8 @@ breaking changes if migrating from v0.9.2. The breaking changes accidentally int
### Added
Initial alpha release of PartiQL.

[Unreleased]: https://github.com/partiql/partiql-lang-kotlin/compare/v0.14.9...HEAD
[Unreleased]: https://github.com/partiql/partiql-lang-kotlin/compare/v0.14.10...HEAD
[0.14.10]: https://github.com/partiql/partiql-lang-kotlin/compare/v0.14.9...v0.14.10
[0.14.9]: https://github.com/partiql/partiql-lang-kotlin/compare/v0.14.8...v0.14.9
[0.14.8]: https://github.com/partiql/partiql-lang-kotlin/compare/v0.14.7...v0.14.8
[0.14.7]: https://github.com/partiql/partiql-lang-kotlin/compare/v0.14.6...v0.14.7
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This project is published to [Maven Central](https://search.maven.org/artifact/o

| Group ID | Artifact ID | Recommended Version |
|---------------|-----------------------|---------------------|
| `org.partiql` | `partiql-lang-kotlin` | `0.14.9` |
| `org.partiql` | `partiql-lang-kotlin` | `0.14.10` |


For Maven builds, add the following to your `pom.xml`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import org.partiql.lang.compiler.PartiQLCompilerAsync;
import org.partiql.lang.compiler.PartiQLCompilerAsyncBuilder;
import org.partiql.lang.compiler.PartiQLCompilerPipelineAsync;
import org.partiql.lang.eval.BindingId;
import org.partiql.lang.eval.BindingName;
import org.partiql.lang.eval.Bindings;
import org.partiql.lang.eval.EvaluationSession;
import org.partiql.lang.eval.ExprValue;
Expand Down Expand Up @@ -63,15 +65,29 @@ public void run() {
.globals(globalVariables)
.build();

final GlobalVariableResolver globalVariableResolver = bindingName -> {
ExprValue value = session.getGlobals().get(bindingName);

if (value != null) {
return new GlobalResolutionResult.GlobalVariable(bindingName.getName());
}
else {
final GlobalVariableResolver globalVariableResolver = new GlobalVariableResolver() {
@NotNull
@Override
public GlobalResolutionResult resolveGlobal(@NotNull BindingId bindingId) {
// In this example, we don't allow for qualified identifiers.
if (!bindingId.hasQualifier()) {
return resolveGlobal(bindingId.getIdentifier());
}
return GlobalResolutionResult.Undefined.INSTANCE;
}

@NotNull
@Override
public GlobalResolutionResult resolveGlobal(@NotNull BindingName bindingName) {
ExprValue value = session.getGlobals().get(bindingName);

if (value != null) {
return new GlobalResolutionResult.GlobalVariable(bindingName.getName());
}
else {
return GlobalResolutionResult.Undefined.INSTANCE;
}
}
};

final EvaluatorOptions evaluatorOptions = new EvaluatorOptions.Builder()
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group=org.partiql
version=0.14.9
version=0.14.10

ossrhUsername=EMPTY
ossrhPassword=EMPTY
Expand Down
28 changes: 24 additions & 4 deletions partiql-ast/src/main/kotlin/org/partiql/ast/helpers/ToLegacyAst.kt
Original file line number Diff line number Diff line change
Expand Up @@ -992,14 +992,34 @@ private class AstTranslator(val metas: Map<String, MetaContainer>) : AstBaseVisi
super.visitStatementDML(node, ctx) as PartiqlAst.Statement

override fun visitStatementDMLInsert(node: Statement.DML.Insert, ctx: Ctx) = translate(node) { metas ->
val target = visitIdentifier(node.target, ctx)
val target = tableName(node.target)
val asAlias = node.asAlias?.symbol
val values = visitExpr(node.values, ctx)
val conflictAction = node.onConflict?.let { visitOnConflictAction(it.action, ctx) }
val op = insert(target, asAlias, values, conflictAction)
dml(dmlOpList(op), null, null, null, metas)
}

private fun tableName(id: Identifier): PartiqlAst.TableName = translate(id) { metas ->
val identifierChain = when (id) {
is Identifier.Symbol -> identifierChain(identifier(id), emptyList())
is Identifier.Qualified -> {
val root = identifier(id.root)
val steps = id.steps.map { identifier(it) }
val (head, qualifier) = when (id.steps.isEmpty()) {
true -> root to emptyList()
false -> steps.last() to listOf(root) + steps.dropLast(1)
}
identifierChain(head, qualifier)
}
}
tableName(identifierChain)
}

private fun identifier(id: Identifier.Symbol): PartiqlAst.Identifier = translate(id) { metas ->
identifier(id.symbol, id.caseSensitivity.toLegacyCaseSensitivity())
}

override fun visitStatementDMLInsertLegacy(
node: Statement.DML.InsertLegacy,
ctx: Ctx,
Expand All @@ -1016,7 +1036,7 @@ private class AstTranslator(val metas: Map<String, MetaContainer>) : AstBaseVisi
}

override fun visitStatementDMLUpsert(node: Statement.DML.Upsert, ctx: Ctx) = translate(node) { metas ->
val target = visitIdentifier(node.target, ctx)
val target = tableName(node.target)
val asAlias = node.asAlias?.symbol
val values = visitExpr(node.values, ctx)
val conflictAction = doUpdate(excluded())
Expand All @@ -1026,7 +1046,7 @@ private class AstTranslator(val metas: Map<String, MetaContainer>) : AstBaseVisi
}

override fun visitStatementDMLReplace(node: Statement.DML.Replace, ctx: Ctx) = translate(node) { metas ->
val target = visitIdentifier(node.target, ctx)
val target = tableName(node.target)
val asAlias = node.asAlias?.symbol
val values = visitExpr(node.values, ctx)
val conflictAction = doReplace(excluded())
Expand Down Expand Up @@ -1122,7 +1142,7 @@ private class AstTranslator(val metas: Map<String, MetaContainer>) : AstBaseVisi
node: Statement.DML.BatchLegacy.Op.Insert,
ctx: Ctx,
) = translate(node) { metas ->
val target = visitIdentifier(node.target, ctx)
val target = tableName(node.target)
val asAlias = node.asAlias?.symbol
val values = visitExpr(node.values, ctx)
val conflictAction = node.onConflict?.let { visitOnConflictAction(it.action, ctx) }
Expand Down
14 changes: 12 additions & 2 deletions partiql-ast/src/main/pig/partiql.ion
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ may then be further optimized by selecting better implementations of each operat
(sum dml_op
// See the following RFC for more details:
// https://github.com/partiql/partiql-docs/blob/main/RFCs/0011-partiql-insert.md
(insert target::expr as_alias::(? symbol) values::expr conflict_action::(? conflict_action))
(insert target::table_name as_alias::(? symbol) values::expr conflict_action::(? conflict_action))

// `INSERT INTO <expr> VALUE <expr> [AT <expr>]` [ON CONFLICT WHERE <expr> DO NOTHING]`
(insert_value target::expr value::expr index::(? expr) on_conflict::(? on_conflict))
Expand Down Expand Up @@ -524,8 +524,13 @@ may then be further optimized by selecting better implementations of each operat
(all_old)
)

// This refers to the <table name> EBNF rule in SQL:1999.
(product table_name id::identifier_chain)
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved

// `identifier` can be used for names that need to be looked up with a notion of case-sensitivity.

// This is a generalization of a qualified identifier in the SQL EBNF that allows for namespaces beyond the
// typical (optional) catalog-schema namespace.
// For both `create_index` and `create_table`, there is no notion of case-sensitivity
// for table identifiers since they are *defining* new identifiers. However, for `drop_index` and
// `drop_table` *do* have the notion of case sensitivity since they are referring to existing names.
Expand All @@ -534,6 +539,11 @@ may then be further optimized by selecting better implementations of each operat
// an element of a type. (Even though in the Kotlin code each varaint is its own type.) Hence, we
// define an `identifier` type above which can be used without opening up an element's domain to all of
// `expr`.
// For how this maps to syntax, consider `INSERT INTO cat1.schema1.table1`. In this scenario, `table1` is the
// head. The qualifier is `cat1.schema1`.
(product identifier_chain head::identifier qualifier::(* identifier 0))
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved

// This refers to a regular/delimited identifier in the SQL EBNF.
(product identifier name::symbol case::case_sensitivity)

// Represents `<expr> = <expr>` in a DML SET operation. Note that in this case, `=` is representing
Expand Down Expand Up @@ -853,7 +863,7 @@ may then be further optimized by selecting better implementations of each operat

// The "target" of a DML operation, i.e. the table targeted for manipulation with INSERT, UPDATE, etc.
// This is a discrete type so it can be permuted in later domains to affect every use.
(record dml_target (identifier identifier))
(record dml_target (identifier table_name))

// An assignment within a SET clause.
(record set_assignment
Expand Down
64 changes: 64 additions & 0 deletions partiql-lang/src/main/kotlin/org/partiql/lang/eval/Bindings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,63 @@ fun PartiqlAst.CaseSensitivity.toBindingCase(): BindingCase = when (this) {
is PartiqlAst.CaseSensitivity.CaseSensitive -> BindingCase.SENSITIVE
}

/**
* Represents a namespaced global binding.
*/
class BindingId(
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved
private val qualifier: List<BindingName>,
private val id: BindingName
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved
) : Iterable<BindingName> {
/**
* For input such as `SELECT a FROM catalog1."schema1".table1`, the identifier is `table1`.
*/
fun getIdentifier(): BindingName = id

/**
* For input such as `SELECT a FROM catalog1."schema1".table1`, the qualifier is `catalog1."schema1"`
*/
fun getQualifier(): List<BindingName> = qualifier

/**
* Returns whether the id is qualified.
*/
fun hasQualifier(): Boolean = qualifier.isNotEmpty()

/**
* Returns a collection of the parts of the identifier.
*/
fun getParts(): List<BindingName> {
return qualifier + id
}
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved

override fun iterator(): Iterator<BindingName> {
return getParts().iterator()
}

override fun toString(): String {
return when (hasQualifier()) {
true -> "${qualifier.joinToString(".")}.$id"
false -> id.toString()
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is BindingId) return false

if (qualifier != other.qualifier) return false
if (id != other.id) return false

return true
}

override fun hashCode(): Int {
var result = qualifier.hashCode()
result = 31 * result + id.hashCode()
return result
}
johnedquinn marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Encapsulates the data necessary to perform a binding lookup.
*/
Expand All @@ -65,6 +122,13 @@ data class BindingName(val name: String, val bindingCase: BindingCase) {
* Compares [name] to [otherName] using the rules specified by [bindingCase].
*/
fun isEquivalentTo(otherName: String?) = otherName != null && name.isBindingNameEquivalent(otherName, bindingCase)

override fun toString(): String {
return when (bindingCase) {
BindingCase.SENSITIVE -> "\"$name\""
BindingCase.INSENSITIVE -> "$name"
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.partiql.lang.planner

import org.partiql.lang.eval.BindingCase
import org.partiql.lang.eval.BindingId
import org.partiql.lang.eval.BindingName

/**
Expand All @@ -17,6 +18,21 @@ sealed class GlobalResolutionResult {
*/
data class GlobalVariable(val uniqueId: String) : GlobalResolutionResult()
Copy link
Member

@dlurton dlurton Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A change I've been wanting to make forever is to make uniqueId here an IonElement so we can put arbitrary Ion data here instead of having to "abuse" this field by putting Ion-text here instead. I'll leave a note in the main PIG domain where we need to change this as well.... EDIT: github won't let me comment there--to do this we would also need to change partiql_logical_resolved.expr.global_id.unique_id to the ion type.

Since we're breaking this API anyway, now might be a good time to do that as well...


/**
* A success case. Refers to a variable that is contained within a namespace (AKA catalog/schema).
*/
abstract class NamespacedVariable(
val uniqueId: String
) : GlobalResolutionResult() {
/**
* When attempting to resolve a qualified identifier, say `FROM cat1.schema1.table1.attr1.attr2`, the [GlobalVariableResolver]
* _may_ resolve only the first few steps of the identifier, say `cat1.schema1.table1`. Therefore, it must
* return the resolved [uniqueId] (say `cat1:::schema1:::table1`) and the remaining steps `attr1.attr2` in order for the planner
* to convert these to path expressions.
*/
abstract fun getRemainingSteps(): List<BindingName>
Comment on lines +27 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this might be a little too forward thinking... i.e. trying to design for a future we know little about. Or is there more to this than I am aware of?

In my case, all parts of a table identifier will be completely resolved or we will abort w/undefined table error. This might make sense in the event that we have only part of the schema... but that is not in my foreseeable future. I suggest dropping this.

If this is dropped, I don't think we need a distinction between a GlobalVariable and a NamespacedVariable either... GlobalVariable will still be fine for our case... Even though we're kind of using it in a way not entirely consistent with the name. Maybe, "ResolvedVariable" is a better name?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking again about this... was the intention to handle the scenario when some of the parts of the qualified identifier could be resolved but others not? I guess I'm not 100% sure I understand the intent here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking again about this... was the intention to handle the scenario when some of the parts of the qualified identifier could be resolved but others not?

Yes, that is the intention. In the INSERT INTO <table name> use-case, you wouldn't allow remainingSteps to have anything. But, in a query such as the one mentioned in the Javadocs, it is useful to know what is actually a global vs what is a path expression. This allows for the engine-implementer to enforce the accuracy of the path expression and for the global-variable-resolver implementer to only take care of returning "global" variables.

This is something that has been added to v1.

}

/** A failure case, indicates that resolution did not match any variable. */
object Undefined : GlobalResolutionResult()
}
Expand Down Expand Up @@ -56,6 +72,18 @@ fun interface GlobalVariableResolver {
*/
fun resolveGlobal(bindingName: BindingName): GlobalResolutionResult
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can drop this--it is redundant when you have resolveGlobal(BindingId). Or, more accurately, drop this function and move its documentation (with modification) to the other overload. Then, remove the default implementation of resolveGlobal(BindingId) so we're forced to implement it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For backwards compatibility purposes, when adding new APIs, we've been adding default implementations that "may" utilize existing implemented methods, so that users who don't want to update their code don't have to. That being said, you're the only customer here, so it's up to you.


/**
* Resolves a potentially qualified [BindingId] in the database environment.
* By default, if the [bindingId] does not have a qualified identifier passed to it, it will resolve using just
* the unqualified identifier. If it is qualified, it will return a [GlobalResolutionResult.Undefined].
*/
fun resolveGlobal(bindingId: BindingId): GlobalResolutionResult {
if (!bindingId.hasQualifier()) {
return resolveGlobal(bindingId.getIdentifier())
}
return GlobalResolutionResult.Undefined
}

companion object {

val EMPTY = GlobalVariableResolver { GlobalResolutionResult.Undefined }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package org.partiql.lang.planner

import org.partiql.errors.ProblemDetails
import org.partiql.errors.ProblemSeverity
import org.partiql.lang.eval.BindingCase
import org.partiql.lang.eval.BindingId
import org.partiql.lang.eval.BindingName

/**
* Contains detailed information about errors that may occur during query planning.
Expand Down Expand Up @@ -32,15 +35,29 @@ sealed class PlanningProblemDetails(
}
)

data class UndefinedDmlTarget(val variableName: String, val caseSensitive: Boolean) :
PlanningProblemDetails(
ProblemSeverity.ERROR,
{
"Data manipulation target table '$variableName' is undefined. " +
"Hint: this must be a name in the global scope. " +
quotationHint(caseSensitive)
}
data class UndefinedDmlTarget(val id: BindingId) : PlanningProblemDetails(
ProblemSeverity.ERROR,
{
"Data manipulation target table '$id' is undefined. Hint: this must be a name in the global scope. " + quotationHint(id.getIdentifier().bindingCase == BindingCase.SENSITIVE)
}
) {

val variableName = id.getIdentifier().name
val caseSensitive = id.getIdentifier().bindingCase == BindingCase.SENSITIVE

constructor(variableName: String, caseSensitive: Boolean) : this(
BindingId(
emptyList(),
BindingName(
variableName,
when (caseSensitive) {
true -> BindingCase.SENSITIVE
false -> BindingCase.INSENSITIVE
}
)
)
)
}

data class VariablePreviouslyDefined(val variableName: String) :
PlanningProblemDetails(
Expand Down
Loading
Loading