diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d03136604..8abf031315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,15 +28,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adds support for SQL's CURRENT_USER in the AST, EvaluatingCompiler, experimental planner implementation, and Schema Inferencer. - Adds the AST node `session_attribute`. - Adds the function `EvaluationSession.Builder::user()` to add the CURRENT_USER to the EvaluationSession +- Adds support for parsing and planning of `INSERT INTO .. AS ... ON CONFLICT DO [UPDATE|REPLACE] EXCLUDED WHERE ` +- Adds the `statement.dml` and `dml_operation` node to the experimental PartiQL Physical Plan. ### Changed +- **Breaking**: Adds a new property `as_alias` to the `insert` AST node. +- **Breaking**: Adds new property `condition` to the AST nodes of `do_replace` and `do_update` +- **Breaking**: Adds `target_alias` property to the `dml_insert`, `dml_replace`, and `dml_update` nodes within the + Logical and Logical Resolved plans +- **Breaking**: Adds `condition` property to the `dml_replace` and `dml_update` nodes within the + Logical and Logical Resolved plans + ### Deprecated ### Fixed +- Parsing INSERT statements with aliases no longer loses the original table name. Closes #1043. + ### Removed +- **Breaking**: Removes node `statement.dml_query` from the experimental PartiQL Physical Plan. Please see the added + `statement.dml` and `dml_operation` nodes. + ### Security ## [0.9.3] - 2023-04-12 diff --git a/partiql-lang/src/main/antlr/PartiQL.g4 b/partiql-lang/src/main/antlr/PartiQL.g4 index 5d4144b12b..cd6286000d 100644 --- a/partiql-lang/src/main/antlr/PartiQL.g4 +++ b/partiql-lang/src/main/antlr/PartiQL.g4 @@ -161,6 +161,8 @@ removeCommand insertCommandReturning : INSERT INTO pathSimple VALUE value=expr ( AT pos=expr )? onConflictClause? returningClause?; +// TODO: Update grammar to disallow the mixing and matching of `insert`, `insertLegacy`, `onConflict`, and `onConflictLegacy` +// See https://github.com/partiql/partiql-lang-kotlin/issues/1063 for more details. insertCommand : INSERT INTO pathSimple VALUE value=expr ( AT pos=expr )? onConflictClause? # InsertLegacy // See the Grammar at https://github.com/partiql/partiql-docs/blob/main/RFCs/0011-partiql-insert.md#2-proposed-grammar-and-semantics @@ -197,7 +199,7 @@ conflictAction [ WHERE ] */ doReplace - : EXCLUDED; + : EXCLUDED ( WHERE condition=expr )?; // :TODO add the rest of the grammar /* @@ -207,7 +209,7 @@ doReplace [ WHERE ] */ doUpdate - : EXCLUDED; + : EXCLUDED ( WHERE condition=expr )?; // :TODO add the rest of the grammar updateClause diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/compiler/PartiQLCompilerDefault.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/compiler/PartiQLCompilerDefault.kt index 7aed33a34a..fa55daaa6b 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/compiler/PartiQLCompilerDefault.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/compiler/PartiQLCompilerDefault.kt @@ -20,12 +20,8 @@ import org.partiql.lang.domains.PartiqlLogical import org.partiql.lang.domains.PartiqlLogicalResolved import org.partiql.lang.domains.PartiqlPhysical import org.partiql.lang.errors.PartiQLException -import org.partiql.lang.eval.BindingCase -import org.partiql.lang.eval.BindingName -import org.partiql.lang.eval.Bindings import org.partiql.lang.eval.ExprFunction import org.partiql.lang.eval.ExprValue -import org.partiql.lang.eval.Expression import org.partiql.lang.eval.PartiQLResult import org.partiql.lang.eval.PartiQLStatement import org.partiql.lang.eval.builtins.storedprocedure.StoredProcedure @@ -35,10 +31,6 @@ import org.partiql.lang.eval.physical.PhysicalPlanCompilerImpl import org.partiql.lang.eval.physical.PhysicalPlanThunk import org.partiql.lang.eval.physical.operators.RelationalOperatorFactory import org.partiql.lang.eval.physical.operators.RelationalOperatorFactoryKey -import org.partiql.lang.planner.DML_COMMAND_FIELD_ACTION -import org.partiql.lang.planner.DML_COMMAND_FIELD_ROWS -import org.partiql.lang.planner.DML_COMMAND_FIELD_TARGET_UNIQUE_ID -import org.partiql.lang.planner.DmlAction import org.partiql.lang.planner.EvaluatorOptions import org.partiql.lang.planner.PartiQLPlanner import org.partiql.lang.types.TypedOpParameter @@ -71,18 +63,20 @@ internal class PartiQLCompilerDefault( } override fun compile(statement: PartiqlPhysical.Plan): PartiQLStatement { - val expression = exprConverter.compile(statement) return when (statement.stmt) { - is PartiqlPhysical.Statement.DmlQuery -> PartiQLStatement { expression.eval(it).toDML() } + is PartiqlPhysical.Statement.Dml -> compileDml(statement.stmt, statement.locals.size) is PartiqlPhysical.Statement.Exec, - is PartiqlPhysical.Statement.Query -> PartiQLStatement { expression.eval(it).toValue() } + is PartiqlPhysical.Statement.Query -> { + val expression = exprConverter.compile(statement) + PartiQLStatement { expression.eval(it).toValue() } + } is PartiqlPhysical.Statement.Explain -> throw PartiQLException("Unable to compile EXPLAIN without details.") } } override fun compile(statement: PartiqlPhysical.Plan, details: PartiQLPlanner.PlanningDetails): PartiQLStatement { return when (statement.stmt) { - is PartiqlPhysical.Statement.DmlQuery, + is PartiqlPhysical.Statement.Dml -> compileDml(statement.stmt, statement.locals.size) is PartiqlPhysical.Statement.Exec, is PartiqlPhysical.Statement.Query -> compile(statement) is PartiqlPhysical.Statement.Explain -> PartiQLStatement { compileExplain(statement.stmt, details) } @@ -100,6 +94,18 @@ internal class PartiQLCompilerDefault( PHYSICAL_TRANSFORMED } + private fun compileDml(dml: PartiqlPhysical.Statement.Dml, localsSize: Int): PartiQLStatement { + val rows = exprConverter.compile(dml.rows, localsSize) + return PartiQLStatement { session -> + when (dml.operation) { + is PartiqlPhysical.DmlOperation.DmlReplace -> PartiQLResult.Replace(dml.uniqueId.text, rows.eval(session)) + is PartiqlPhysical.DmlOperation.DmlInsert -> PartiQLResult.Insert(dml.uniqueId.text, rows.eval(session)) + is PartiqlPhysical.DmlOperation.DmlDelete -> PartiQLResult.Delete(dml.uniqueId.text, rows.eval(session)) + is PartiqlPhysical.DmlOperation.DmlUpdate -> TODO("DML Update compilation not supported yet.") + } + } + } + private fun compileExplain(statement: PartiqlPhysical.Statement.Explain, details: PartiQLPlanner.PlanningDetails): PartiQLResult.Explain.Domain { return when (statement.target) { is PartiqlPhysical.ExplainTarget.Domain -> compileExplainDomain(statement.target, details) @@ -152,44 +158,5 @@ internal class PartiQLCompilerDefault( } } - /** - * The physical expr converter is EvaluatingCompiler with s/Ast/Physical and `bindingsToValues -> Thunk` - * so it returns the [Expression] rather than a [PartiQLStatement]. This method parses a DML Command from the result. - * - * { - * 'action': , - * 'target_unique_id': - * 'rows': - * } - * - * Later refactors will rework the Compiler to use [PartiQLStatement], but this is an acceptable workaround for now. - */ - private fun ExprValue.toDML(): PartiQLResult { - val action = bindings string DML_COMMAND_FIELD_ACTION - val target = bindings string DML_COMMAND_FIELD_TARGET_UNIQUE_ID - val rows = bindings seq DML_COMMAND_FIELD_ROWS - return when (DmlAction.safeValueOf(action)) { - DmlAction.INSERT -> PartiQLResult.Insert(target, rows) - DmlAction.DELETE -> PartiQLResult.Delete(target, rows) - DmlAction.REPLACE -> PartiQLResult.Replace(target, rows) - null -> error("Unknown DML Action `$action`") - } - } - private fun ExprValue.toValue(): PartiQLResult = PartiQLResult.Value(this) - - private infix fun Bindings.string(field: String): String { - return this[BindingName(field, BindingCase.SENSITIVE)]?.scalar?.stringValue() ?: missing(field) - } - - private infix fun Bindings.seq(field: String): Iterable { - val v = this[BindingName(field, BindingCase.SENSITIVE)] ?: missing(field) - if (!v.type.isSequence) { - error("DML command struct '$DML_COMMAND_FIELD_ROWS' field must be a bag or list") - } - return v.asIterable() - } - - private fun missing(field: String): Nothing = - error("Field `$field` missing from DML command struct or has incorrect Ion type") } diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/eval/physical/PhysicalPlanCompilerImpl.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/eval/physical/PhysicalPlanCompilerImpl.kt index 40b5a24299..5d5fa2780e 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/eval/physical/PhysicalPlanCompilerImpl.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/eval/physical/PhysicalPlanCompilerImpl.kt @@ -173,6 +173,27 @@ internal class PhysicalPlanCompilerImpl( } } + /** + * Compiles a [PartiqlPhysical.Expr] tree to an [Expression]. + * + * Checks [Thread.interrupted] before every expression and sub-expression is compiled + * and throws [InterruptedException] if [Thread.interrupted] it has been set in the + * hope that long-running compilations may be aborted by the caller. + */ + internal fun compile(expr: PartiqlPhysical.Expr, localsSize: Int): Expression { + val thunk = compileAstExpr(expr) + + return object : Expression { + override fun eval(session: EvaluationSession): ExprValue { + val env = EvaluatorState( + session = session, + registers = Array(localsSize) { ExprValue.missingValue } + ) + return thunk(env) + } + } + } + override fun convert(expr: PartiqlPhysical.Expr): PhysicalPlanThunk = this.compileAstExpr(expr) /** @@ -183,8 +204,8 @@ internal class PhysicalPlanCompilerImpl( private fun compileAstStatement(ast: PartiqlPhysical.Statement): PhysicalPlanThunk { return when (ast) { is PartiqlPhysical.Statement.Query -> compileAstExpr(ast.expr) - is PartiqlPhysical.Statement.DmlQuery -> compileAstExpr(ast.expr) is PartiqlPhysical.Statement.Exec -> compileExec(ast) + is PartiqlPhysical.Statement.Dml, is PartiqlPhysical.Statement.Explain -> { val value = ExprValue.newBoolean(true) thunkFactory.thunkEnv(emptyMetaContainer()) { value } diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/planner/QueryPlan.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/planner/QueryPlan.kt index 26edee1e61..98d7af6007 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/planner/QueryPlan.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/planner/QueryPlan.kt @@ -5,7 +5,6 @@ import org.partiql.lang.eval.BindingName import org.partiql.lang.eval.Bindings import org.partiql.lang.eval.EvaluationSession import org.partiql.lang.eval.ExprValue -import org.partiql.lang.eval.ExprValueType /** A query plan that has been compiled and is ready to be evaluated. */ fun interface QueryPlan { @@ -29,7 +28,7 @@ sealed class QueryResult { * * The primary benefit of this class is that it ensures that the [rows] property is evaluated lazily. It also * provides a cleaner API that is easier to work with for PartiQL embedders. Without this, the user would have to - * consume the [ExprValue] directly and use code similar to that in [toDmlCommand] or convert it to Ion. Neither + * consume the [ExprValue] directly and convert it to Ion. Neither * option is particularly developer friendly, efficient or maintainable. * * This is currently only factored to support `INSERT INTO` and `DELETE FROM` as `UPDATE` and `FROM ... UPDATE` is @@ -67,54 +66,5 @@ enum class DmlAction { } } -internal const val DML_COMMAND_FIELD_ACTION = "action" -internal const val DML_COMMAND_FIELD_TARGET_UNIQUE_ID = "target_unique_id" -internal const val DML_COMMAND_FIELD_ROWS = "rows" - private operator fun Bindings.get(fieldName: String): ExprValue? = this[BindingName(fieldName, BindingCase.SENSITIVE)] - -private fun errMissing(fieldName: String): Nothing = - error("'$fieldName' missing from DML command struct or has incorrect Ion type") - -/** - * Converts an [ExprValue] which is the result of a DML query to an instance of [DmlCommand]. - * - * Format of a such an [ExprValue]: - * - * ``` - * { - * 'action': , - * 'target_unique_id': - * 'rows': - * } - * ``` - * - * Where: - * - `` is either `insert` or `delete` - * - `` is a string or symbol containing the unique identifier of the table to be effected - * by the DML statement. - * - `` is a bag or list containing the rows (structs) effected by the DML statement. - * - When `` is `insert` this is the rows to be inserted. - * - When `` is `delete` this is the rows to be deleted. Non-primary key fields may be elided, but the - * default behavior is to include all fields because PartiQL does not yet know about primary keys. - */ -internal fun ExprValue.toDmlCommand(): QueryResult.DmlCommand { - require(this.type == ExprValueType.STRUCT) { "'row' must be a struct" } - - val actionString = this.bindings[DML_COMMAND_FIELD_ACTION]?.scalar?.stringValue()?.toUpperCase() - ?: errMissing(DML_COMMAND_FIELD_ACTION) - - val dmlAction = DmlAction.safeValueOf(actionString) - ?: error("Unknown DmlAction in DML command struct: '$actionString'") - - val targetUniqueId = this.bindings[DML_COMMAND_FIELD_TARGET_UNIQUE_ID]?.scalar?.stringValue() - ?: errMissing(DML_COMMAND_FIELD_TARGET_UNIQUE_ID) - - val rows = this.bindings[DML_COMMAND_FIELD_ROWS] ?: errMissing(DML_COMMAND_FIELD_ROWS) - if (!rows.type.isSequence) { - error("DML command struct '$DML_COMMAND_FIELD_ROWS' field must be a bag or list") - } - - return QueryResult.DmlCommand(dmlAction, targetUniqueId, rows) -} diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/AstToLogicalVisitorTransform.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/AstToLogicalVisitorTransform.kt index fe9e7755ba..f252fc61a0 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/AstToLogicalVisitorTransform.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/AstToLogicalVisitorTransform.kt @@ -363,46 +363,34 @@ internal class AstToLogicalVisitorTransform( } } - when (val conflictAction = dmlOp.conflictAction) { - null -> { - PartiqlLogical.build { - dml( - target = dmlOp.target.toDmlTargetId(), - operation = dmlInsert(), - rows = transformExpr(dmlOp.values), - metas = node.metas - ) - } - } - is PartiqlAst.ConflictAction.DoReplace -> { - when (conflictAction.value) { - PartiqlAst.OnConflictValue.Excluded() -> PartiqlLogical.build { - dml( - target = dmlOp.target.toDmlTargetId(), - operation = dmlReplace(), - rows = transformExpr(dmlOp.values), - metas = node.metas - ) - } else -> TODO("Only `DO REPLACE EXCLUDED` is supported in logical plan at the moment.") - } + val target = dmlOp.target.toDmlTargetId() + val alias = dmlOp.asAlias?.let { + PartiqlLogical.VarDecl(it) + } ?: PartiqlLogical.VarDecl(target.name) + + val operation = when (val conflictAction = dmlOp.conflictAction) { + null -> PartiqlLogical.DmlOperation.DmlInsert(targetAlias = alias) + is PartiqlAst.ConflictAction.DoReplace -> when (conflictAction.value) { + is PartiqlAst.OnConflictValue.Excluded -> PartiqlLogical.DmlOperation.DmlReplace( + targetAlias = alias, + condition = conflictAction.condition?.let { transformExpr(conflictAction.condition) } + ) } - is PartiqlAst.ConflictAction.DoUpdate -> { - when (conflictAction.value) { - PartiqlAst.OnConflictValue.Excluded() -> PartiqlLogical.build { - dml( - target = dmlOp.target.toDmlTargetId(), - operation = dmlUpdate(), - rows = transformExpr(dmlOp.values), - metas = node.metas - ) - } - else -> TODO("Only `DO UPDATE EXCLUDED` is supported in logical plan at the moment.") - } + is PartiqlAst.ConflictAction.DoUpdate -> when (conflictAction.value) { + is PartiqlAst.OnConflictValue.Excluded -> PartiqlLogical.DmlOperation.DmlUpdate( + targetAlias = alias, + condition = conflictAction.condition?.let { transformExpr(conflictAction.condition) } + ) } - is PartiqlAst.ConflictAction.DoNothing -> TODO( - "`ON CONFLICT DO NOTHING` is not supported in logical plan yet." - ) + is PartiqlAst.ConflictAction.DoNothing -> TODO("`ON CONFLICT DO NOTHING` is not supported in logical plan yet.") } + + PartiqlLogical.Statement.Dml( + target = target, + operation = operation, + rows = transformExpr(dmlOp.values), + metas = node.metas + ) } // INSERT single row with VALUE is disallowed. (This variation of INSERT might be removed in a future // release of PartiQL.) diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/LogicalResolvedToDefaultPhysicalVisitorTransform.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/LogicalResolvedToDefaultPhysicalVisitorTransform.kt index 4bafd7bcd9..98a16fabf7 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/LogicalResolvedToDefaultPhysicalVisitorTransform.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/LogicalResolvedToDefaultPhysicalVisitorTransform.kt @@ -5,10 +5,6 @@ import org.partiql.lang.domains.PartiqlLogicalResolved import org.partiql.lang.domains.PartiqlLogicalResolvedToPartiqlPhysicalVisitorTransform import org.partiql.lang.domains.PartiqlPhysical import org.partiql.lang.errors.ProblemHandler -import org.partiql.lang.planner.DML_COMMAND_FIELD_ACTION -import org.partiql.lang.planner.DML_COMMAND_FIELD_ROWS -import org.partiql.lang.planner.DML_COMMAND_FIELD_TARGET_UNIQUE_ID -import org.partiql.lang.planner.DmlAction /** * Transforms an instance of [PartiqlLogicalResolved.Statement] to [PartiqlPhysical.Statement], @@ -26,6 +22,14 @@ internal fun PartiqlPhysical.Builder.structField(name: String, value: String) = internal fun PartiqlPhysical.Builder.structField(name: String, value: PartiqlPhysical.Expr) = structField(lit(ionSymbol(name)), value) +internal fun structField(name: String, value: PartiqlPhysical.Expr) = + PartiqlPhysical.StructPart.StructField(PartiqlPhysical.Expr.Lit(ionSymbol(name).asAnyElement()), value) + +internal fun structField(name: String, value: String) = PartiqlPhysical.StructPart.StructField( + PartiqlPhysical.Expr.Lit(ionSymbol(name).asAnyElement()), + PartiqlPhysical.Expr.Lit(ionSymbol(value).asAnyElement()) +) + internal class LogicalResolvedToDefaultPhysicalVisitorTransform( val problemHandler: ProblemHandler ) : PartiqlLogicalResolvedToPartiqlPhysicalVisitorTransform() { @@ -164,28 +168,6 @@ internal class LogicalResolvedToDefaultPhysicalVisitorTransform( } } - override fun transformStatementDml(node: PartiqlLogicalResolved.Statement.Dml): PartiqlPhysical.Statement { - val action = when (node.operation) { - is PartiqlLogicalResolved.DmlOperation.DmlInsert -> DmlAction.INSERT - is PartiqlLogicalResolved.DmlOperation.DmlDelete -> DmlAction.DELETE - is PartiqlLogicalResolved.DmlOperation.DmlReplace -> DmlAction.REPLACE - is PartiqlLogicalResolved.DmlOperation.DmlUpdate -> - TODO("DmlUpdate physical transform is not supported yet") - }.name.toLowerCase() - - return PartiqlPhysical.build { - dmlQuery( - expr = struct( - structField(DML_COMMAND_FIELD_ACTION, action), - structField(DML_COMMAND_FIELD_TARGET_UNIQUE_ID, lit(node.uniqueId.toIonElement())), - structField(DML_COMMAND_FIELD_ROWS, transformExpr(node.rows)), - metas = node.metas - ), - metas = node.metas - ) - } - } - override fun transformStatementQuery(node: PartiqlLogicalResolved.Statement.Query): PartiqlPhysical.Statement = PartiqlPhysical.build { query(transformExpr(node.expr), node.metas) } } diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransform.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransform.kt index d5fe2b3299..b15ce4e270 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransform.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransform.kt @@ -378,6 +378,24 @@ internal data class LogicalToLogicalResolvedVisitorTransform( } } + override fun transformDmlOperationDmlInsert(node: PartiqlLogical.DmlOperation.DmlInsert): PartiqlLogicalResolved.DmlOperation { + return withInputScope(this.inputScope.concatenate(node.targetAlias)) { + super.transformDmlOperationDmlInsert(node) + } + } + + override fun transformDmlOperationDmlReplace(node: PartiqlLogical.DmlOperation.DmlReplace): PartiqlLogicalResolved.DmlOperation { + return withInputScope(this.inputScope.concatenate(node.targetAlias)) { + super.transformDmlOperationDmlReplace(node) + } + } + + override fun transformDmlOperationDmlUpdate(node: PartiqlLogical.DmlOperation.DmlUpdate): PartiqlLogicalResolved.DmlOperation { + return withInputScope(this.inputScope.concatenate(node.targetAlias)) { + super.transformDmlOperationDmlUpdate(node) + } + } + /** * Returns a list of variables accessible from the current scope which contain variables that may contain * an unqualified variable, in the order that they should be searched. diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/syntax/PartiQLVisitor.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/syntax/PartiQLVisitor.kt index 7edaf38f9f..8f7870ea4f 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/syntax/PartiQLVisitor.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/syntax/PartiQLVisitor.kt @@ -288,23 +288,19 @@ internal class PartiQLVisitor(val customTypes: List = listOf(), priv } override fun visitInsert(ctx: PartiQLParser.InsertContext) = PartiqlAst.build { - val metas = ctx.INSERT().getSourceMetaContainer() - val asIdent = ctx.asIdent() - // Based on the RFC, if alias exists the table must be hidden behind the alias, see: - // https://github.com/partiql/partiql-docs/blob/main/RFCs/0011-partiql-insert.md#41-insert-parameters - val target = if (asIdent != null) visitAsIdent(asIdent) else visitSymbolPrimitive(ctx.symbolPrimitive()) - val conflictAction = visitOrNull(ctx.onConflictClause(), PartiqlAst.ConflictAction::class) - insert(target, visit(ctx.value, PartiqlAst.Expr::class), conflictAction, metas) + insert( + target = visitSymbolPrimitive(ctx.symbolPrimitive()), + asAlias = visitOrNull(ctx.asIdent(), PartiqlAst.Expr.Id::class)?.name?.text, + values = visit(ctx.value, PartiqlAst.Expr::class), + conflictAction = visitOrNull(ctx.onConflictClause(), PartiqlAst.ConflictAction::class), + metas = ctx.INSERT().getSourceMetaContainer() + ) } - // TODO move from experimental; pending: https://github.com/partiql/partiql-docs/issues/27 override fun visitReplaceCommand(ctx: PartiQLParser.ReplaceCommandContext) = PartiqlAst.build { - val asIdent = ctx.asIdent() - // Based on the RFC, if alias exists the table must be hidden behind the alias, see: - // https://github.com/partiql/partiql-docs/blob/main/RFCs/0011-partiql-insert.md#41-insert-parameters - val target = if (asIdent != null) visitAsIdent(asIdent) else visitSymbolPrimitive(ctx.symbolPrimitive()) insert( - target = target, + target = visitSymbolPrimitive(ctx.symbolPrimitive()), + asAlias = visitOrNull(ctx.asIdent(), PartiqlAst.Expr.Id::class)?.name?.text, values = visit(ctx.value, PartiqlAst.Expr::class), conflictAction = doReplace(excluded()), metas = ctx.REPLACE().getSourceMetaContainer() @@ -313,12 +309,9 @@ internal class PartiQLVisitor(val customTypes: List = listOf(), priv // Based on https://github.com/partiql/partiql-docs/blob/main/RFCs/0011-partiql-insert.md override fun visitUpsertCommand(ctx: PartiQLParser.UpsertCommandContext) = PartiqlAst.build { - val asIdent = ctx.asIdent() - // Based on the RFC, if alias exists the table must be hidden behind the alias, see: - // https://github.com/partiql/partiql-docs/blob/main/RFCs/0011-partiql-insert.md#41-insert-parameters - val target = if (asIdent != null) visitAsIdent(asIdent) else visitSymbolPrimitive(ctx.symbolPrimitive()) insert( - target = target, + target = visitSymbolPrimitive(ctx.symbolPrimitive()), + asAlias = visitOrNull(ctx.asIdent(), PartiqlAst.Expr.Id::class)?.name?.text, values = visit(ctx.value, PartiqlAst.Expr::class), conflictAction = doUpdate(excluded()), metas = ctx.UPSERT().getSourceMetaContainer() @@ -387,7 +380,8 @@ internal class PartiQLVisitor(val customTypes: List = listOf(), priv ctx.EXCLUDED() != null -> excluded() else -> TODO("DO REPLACE doesn't support values other than `EXCLUDED` yet.") } - doReplace(value) + val condition = visitOrNull(ctx.condition, PartiqlAst.Expr::class) + doReplace(value, condition) } override fun visitDoUpdate(ctx: PartiQLParser.DoUpdateContext) = PartiqlAst.build { @@ -395,7 +389,8 @@ internal class PartiQLVisitor(val customTypes: List = listOf(), priv ctx.EXCLUDED() != null -> excluded() else -> TODO("DO UPDATE doesn't support values other than `EXCLUDED` yet.") } - doUpdate(value) + val condition = visitOrNull(ctx.condition, PartiqlAst.Expr::class) + doUpdate(value, condition) } override fun visitPathSimple(ctx: PartiQLParser.PathSimpleContext) = PartiqlAst.build { diff --git a/partiql-lang/src/main/pig/partiql.ion b/partiql-lang/src/main/pig/partiql.ion index 1d21b26685..8c0489faeb 100644 --- a/partiql-lang/src/main/pig/partiql.ion +++ b/partiql-lang/src/main/pig/partiql.ion @@ -404,7 +404,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 values::expr conflict_action::(? conflict_action)) + (insert target::expr as_alias::(? symbol) values::expr conflict_action::(? conflict_action)) // `INSERT INTO VALUE [AT ]` [ON CONFLICT WHERE DO NOTHING]` (insert_value target::expr value::expr index::(? expr) on_conflict::(? on_conflict)) @@ -424,8 +424,8 @@ may then be further optimized by selecting better implementations of each operat // `CONFLICT_ACTION ` (sum conflict_action - (do_replace value::on_conflict_value) - (do_update value::on_conflict_value) + (do_replace value::on_conflict_value condition::(? expr)) + (do_update value::on_conflict_value condition::(? expr)) (do_nothing)) (sum on_conflict_value @@ -764,10 +764,10 @@ may then be further optimized by selecting better implementations of each operat (include // Indicates kind of DML operation. (sum dml_operation - (dml_insert) + (dml_insert target_alias::var_decl) (dml_delete) - (dml_replace) - (dml_update) + (dml_replace target_alias::var_decl condition::(? expr)) + (dml_update target_alias::var_decl condition::(? expr)) ) ) @@ -903,15 +903,6 @@ may then be further optimized by selecting better implementations of each operat (product impl name::symbol static_args::(* ion 0)) ) - // drop DML stuff added in partiql_logical (these should be rewritten into queries). - (exclude dml_operation) - (with statement - (exclude dml) - (include - // Include the dml_query variant, so that we know how to interpret the results of the query. - (dml_query expr::expr)) - ) - // Every variant of bexpr changes by adding an `impl` element in the physical algebra, so let's replace it // entirely. (exclude bexpr) diff --git a/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/AstToLogicalVisitorTransformTests.kt b/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/AstToLogicalVisitorTransformTests.kt index 8403258d73..807581b507 100644 --- a/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/AstToLogicalVisitorTransformTests.kt +++ b/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/AstToLogicalVisitorTransformTests.kt @@ -733,7 +733,7 @@ class AstToLogicalVisitorTransformTests { PartiqlLogical.build { dml( identifier("foo", caseInsensitive()), - dmlInsert(), + dmlInsert(varDecl("foo")), bag(lit(ionInt(1))) ) } @@ -744,7 +744,7 @@ class AstToLogicalVisitorTransformTests { PartiqlLogical.build { dml( identifier("foo", caseInsensitive()), - dmlInsert(), + dmlInsert(varDecl("foo")), bindingsToValues( struct(structFields(id("x", caseInsensitive(), unqualified()))), scan(lit(ionInt(1)), varDecl("x")) @@ -757,7 +757,31 @@ class AstToLogicalVisitorTransformTests { PartiqlLogical.build { dml( identifier("foo", caseInsensitive()), - dmlReplace(), + dmlReplace(varDecl("foo")), + bindingsToValues( + struct(structFields(id("x", caseInsensitive(), unqualified()))), + scan(lit(ionInt(1)), varDecl("x")) + ) + ) + } + ), + TestCase( + "INSERT INTO foo SELECT x.* FROM 1 AS x ON CONFLICT DO REPLACE EXCLUDED WHERE foo.id > 2", + PartiqlLogical.build { + dml( + identifier("foo", caseInsensitive()), + dmlReplace( + targetAlias = varDecl("foo"), + condition = gt( + listOf( + path( + id("foo", caseInsensitive(), unqualified()), + listOf(pathExpr(lit(ionString("id")), caseInsensitive())) + ), + lit(ionInt(2)) + ) + ) + ), bindingsToValues( struct(structFields(id("x", caseInsensitive(), unqualified()))), scan(lit(ionInt(1)), varDecl("x")) @@ -770,8 +794,36 @@ class AstToLogicalVisitorTransformTests { PartiqlLogical.build { PartiqlLogical.build { dml( - identifier("f", caseInsensitive()), - dmlReplace(), + identifier("foo", caseInsensitive()), + dmlReplace(varDecl("f")), + bag( + struct( + structField(lit(ionString("id")), lit(ionInt(1))), + structField(lit(ionString("name")), lit(ionString("bob"))) + ) + ) + ) + } + } + ), + TestCase( + "INSERT INTO foo AS f <<{'id': 1, 'name':'bob'}>> ON CONFLICT DO REPLACE EXCLUDED WHERE f.id > 2", + PartiqlLogical.build { + PartiqlLogical.build { + dml( + identifier("foo", caseInsensitive()), + dmlReplace( + varDecl("f"), + condition = gt( + listOf( + path( + id("f", caseInsensitive(), unqualified()), + listOf(pathExpr(lit(ionString("id")), caseInsensitive())) + ), + lit(ionInt(2)) + ) + ) + ), bag( struct( structField(lit(ionString("id")), lit(ionInt(1))), @@ -787,7 +839,7 @@ class AstToLogicalVisitorTransformTests { PartiqlLogical.build { dml( identifier("foo", caseInsensitive()), - dmlUpdate(), + dmlUpdate(varDecl("foo")), bindingsToValues( struct(structFields(id("x", caseInsensitive(), unqualified()))), scan(lit(ionInt(1)), varDecl("x")) @@ -800,8 +852,36 @@ class AstToLogicalVisitorTransformTests { PartiqlLogical.build { PartiqlLogical.build { dml( - identifier("f", caseInsensitive()), - dmlUpdate(), + identifier("foo", caseInsensitive()), + dmlUpdate(varDecl("f")), + bag( + struct( + structField(lit(ionString("id")), lit(ionInt(1))), + structField(lit(ionString("name")), lit(ionString("bob"))) + ) + ) + ) + } + } + ), + TestCase( + "INSERT INTO foo AS f <<{'id': 1, 'name':'bob'}>> ON CONFLICT DO UPDATE EXCLUDED WHERE f.id > 2", + PartiqlLogical.build { + PartiqlLogical.build { + dml( + identifier("foo", caseInsensitive()), + dmlUpdate( + varDecl("f"), + condition = gt( + listOf( + path( + id("f", caseInsensitive(), unqualified()), + listOf(pathExpr(lit(ionString("id")), caseInsensitive())) + ), + lit(ionInt(2)) + ) + ) + ), bag( struct( structField(lit(ionString("id")), lit(ionInt(1))), @@ -817,8 +897,8 @@ class AstToLogicalVisitorTransformTests { PartiqlLogical.build { PartiqlLogical.build { dml( - identifier("f", caseInsensitive()), - dmlReplace(), + identifier("foo", caseInsensitive()), + dmlReplace(varDecl("f")), bag( struct( structField(lit(ionString("id")), lit(ionInt(1))), @@ -834,8 +914,8 @@ class AstToLogicalVisitorTransformTests { PartiqlLogical.build { PartiqlLogical.build { dml( - identifier("f", caseInsensitive()), - dmlUpdate(), + identifier("foo", caseInsensitive()), + dmlUpdate(varDecl("f")), bindingsToValues( struct(structFields(id("x", caseInsensitive(), unqualified()))), scan(lit(ionInt(1)), varDecl("x")) diff --git a/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass.kt b/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass.kt index 2170c45110..8d66037f36 100644 --- a/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass.kt +++ b/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass.kt @@ -3,6 +3,7 @@ package org.partiql.lang.planner.transforms import com.amazon.ionelement.api.ionBool import com.amazon.ionelement.api.ionInt +import com.amazon.ionelement.api.ionString import com.amazon.ionelement.api.ionSymbol import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.params.ParameterizedTest @@ -10,9 +11,6 @@ import org.junit.jupiter.params.provider.ArgumentsSource import org.partiql.lang.domains.PartiqlLogicalResolved import org.partiql.lang.domains.PartiqlPhysical import org.partiql.lang.errors.ProblemCollector -import org.partiql.lang.planner.DML_COMMAND_FIELD_ACTION -import org.partiql.lang.planner.DML_COMMAND_FIELD_ROWS -import org.partiql.lang.planner.DML_COMMAND_FIELD_TARGET_UNIQUE_ID import org.partiql.lang.util.ArgumentsProviderBase import kotlin.test.fail @@ -232,17 +230,15 @@ class LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass { PartiqlLogicalResolved.build { dml( uniqueId = "foo", - operation = dmlInsert(), + operation = dmlInsert(varDecl(0)), rows = bag(lit(ionInt(1))) ) }, PartiqlPhysical.build { - dmlQuery( - struct( - structField(DML_COMMAND_FIELD_ACTION, "insert"), - structField(DML_COMMAND_FIELD_TARGET_UNIQUE_ID, lit(ionSymbol("foo"))), - structField(DML_COMMAND_FIELD_ROWS, bag(lit(ionInt(1)))) - ) + dml( + uniqueId = "foo", + operation = dmlInsert(varDecl(0)), + rows = bag(lit(ionInt(1))) ) } ), @@ -251,7 +247,7 @@ class LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass { PartiqlLogicalResolved.build { dml( uniqueId = "foo", - operation = dmlInsert(), + operation = dmlInsert(varDecl(0)), rows = bindingsToValues( struct(structFields(localId(0))), scan(lit(ionInt(1)), varDecl(0)) @@ -259,31 +255,22 @@ class LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass { ) }, PartiqlPhysical.build { - dmlQuery( - struct( - structField(DML_COMMAND_FIELD_ACTION, "insert"), - structField(DML_COMMAND_FIELD_TARGET_UNIQUE_ID, lit(ionSymbol("foo"))), - structField( - DML_COMMAND_FIELD_ROWS, - bindingsToValues( - struct(structFields(localId(0))), - scan( - i = DEFAULT_IMPL, - expr = lit(ionInt(1)), - asDecl = varDecl(0) - ) - ) - ) + dml( + uniqueId = "foo", + operation = dmlInsert(varDecl(0)), + rows = bindingsToValues( + struct(structFields(localId(0))), + scan(DEFAULT_IMPL, lit(ionInt(1)), varDecl(0)) ) ) } ), DmlTestCase( - // INSERT INTO foo SELECT x.* FROM 1 AS x ON CONFLICT DO REPLACE EXCLUDED + // INSERT INTO foo [AS f] SELECT x.* FROM 1 AS x ON CONFLICT DO REPLACE EXCLUDED PartiqlLogicalResolved.build { dml( uniqueId = "foo", - operation = dmlReplace(), + operation = dmlReplace(varDecl(0)), rows = bindingsToValues( struct(structFields(localId(0))), scan(lit(ionInt(1)), varDecl(0)) @@ -291,21 +278,57 @@ class LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass { ) }, PartiqlPhysical.build { - dmlQuery( - struct( - structField(DML_COMMAND_FIELD_ACTION, "replace"), - structField(DML_COMMAND_FIELD_TARGET_UNIQUE_ID, lit(ionSymbol("foo"))), - structField( - DML_COMMAND_FIELD_ROWS, - bindingsToValues( - struct(structFields(localId(0))), - scan( - i = DEFAULT_IMPL, - expr = lit(ionInt(1)), - asDecl = varDecl(0) - ) + dml( + uniqueId = "foo", + operation = dmlReplace(varDecl(0)), + rows = bindingsToValues( + struct(structFields(localId(0))), + scan(DEFAULT_IMPL, lit(ionInt(1)), varDecl(0)) + ) + ) + } + ), + DmlTestCase( + // INSERT INTO foo [AS f] SELECT x.* FROM 1 AS x ON CONFLICT DO REPLACE EXCLUDED WHERE foo.id > 1 + PartiqlLogicalResolved.build { + dml( + uniqueId = "foo", + operation = dmlReplace( + targetAlias = varDecl(0), + condition = gt( + listOf( + path( + localId(0), + listOf(pathExpr(lit(ionString("id")), caseInsensitive())) + ), + lit(ionInt(1)) ) ) + ), + rows = bindingsToValues( + struct(structFields(localId(0))), + scan(lit(ionInt(1)), varDecl(0)) + ) + ) + }, + PartiqlPhysical.build { + dml( + uniqueId = "foo", + operation = dmlReplace( + targetAlias = varDecl(0), + condition = gt( + listOf( + path( + localId(0), + listOf(pathExpr(lit(ionString("id")), caseInsensitive())) + ), + lit(ionInt(1)) + ) + ) + ), + rows = bindingsToValues( + struct(structFields(localId(0))), + scan(DEFAULT_IMPL, lit(ionInt(1)), varDecl(0)) ) ) } @@ -323,21 +346,12 @@ class LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass { ) }, PartiqlPhysical.build { - dmlQuery( - struct( - structField(DML_COMMAND_FIELD_ACTION, "delete"), - structField(DML_COMMAND_FIELD_TARGET_UNIQUE_ID, lit(ionSymbol("foo"))), - structField( - DML_COMMAND_FIELD_ROWS, - bindingsToValues( - localId(0), - scan( - i = DEFAULT_IMPL, - expr = globalId("y"), - asDecl = varDecl(0) - ) - ) - ) + dml( + uniqueId = "foo", + operation = dmlDelete(), + rows = bindingsToValues( + localId(0), + scan(DEFAULT_IMPL, globalId("y"), varDecl(0)) ) ) } @@ -359,25 +373,16 @@ class LogicalResolvedToDefaultPartiQLPhysicalVisitorTransformTestsPass { ) }, PartiqlPhysical.build { - dmlQuery( - struct( - structField(DML_COMMAND_FIELD_ACTION, "delete"), - structField(DML_COMMAND_FIELD_TARGET_UNIQUE_ID, lit(ionSymbol("y"))), - structField( - DML_COMMAND_FIELD_ROWS, - bindingsToValues( - localId(0), - // this logical plan is same as previous but includes this filter - filter( - i = DEFAULT_IMPL, - eq(lit(ionInt(1)), lit(ionInt(1))), - scan( - i = DEFAULT_IMPL, - expr = globalId("y"), - asDecl = varDecl(0) - ) - ) - ) + dml( + uniqueId = "y", + operation = dmlDelete(), + rows = bindingsToValues( + localId(0), + // this logical plan is same as previous but includes this filter + filter( + DEFAULT_IMPL, + eq(lit(ionInt(1)), lit(ionInt(1))), + scan(DEFAULT_IMPL, globalId("y"), varDecl(0)) ) ) ) diff --git a/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransformTests.kt b/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransformTests.kt index 83e0892b7f..3af61eeef9 100644 --- a/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransformTests.kt +++ b/partiql-lang/src/test/kotlin/org/partiql/lang/planner/transforms/LogicalToLogicalResolvedVisitorTransformTests.kt @@ -862,4 +862,60 @@ class LogicalToLogicalResolvedVisitorTransformTests { ), ) } + + @ParameterizedTest + @ArgumentsSource(DmlStatements::class) + fun `dml statements`(tc: TestCase) = runTestCase(tc) + class DmlStatements : ArgumentsProviderBase() { + override fun getParameters() = listOf( + TestCase( + "INSERT INTO foo << {'a': 1} >> ON CONFLICT DO REPLACE EXCLUDED WHERE foo.id > 2", + Expectation.Success( + ResolvedId(1, 70) { localId(0) }, + ).withLocals(localVariable("foo", 0)) + ), + TestCase( + "INSERT INTO foo AS f << {'a': 1} >> ON CONFLICT DO REPLACE EXCLUDED WHERE f.id > 2", + Expectation.Success( + ResolvedId(1, 75) { localId(0) }, + ).withLocals(localVariable("f", 0)) + ), + TestCase( + "INSERT INTO foo AS f << {'a': 1} >> ON CONFLICT DO REPLACE EXCLUDED WHERE foo.id > 2", + Expectation.Success( + ResolvedId(1, 75) { globalId("fake_uid_for_foo") }, + ).withLocals(localVariable("f", 0)) + ), + TestCase( + "INSERT INTO foo << {'a': 1} >> ON CONFLICT DO REPLACE EXCLUDED WHERE f.id > 2", + Expectation.Problems( + problem(1, 70, PlanningProblemDetails.UndefinedVariable("f", false)) + ) + ), + TestCase( + "INSERT INTO foo << {'a': 1} >> ON CONFLICT DO UPDATE EXCLUDED WHERE foo.id > 2", + Expectation.Success( + ResolvedId(1, 69) { localId(0) }, + ).withLocals(localVariable("foo", 0)) + ), + TestCase( + "INSERT INTO foo AS f << {'a': 1} >> ON CONFLICT DO UPDATE EXCLUDED WHERE f.id > 2", + Expectation.Success( + ResolvedId(1, 74) { localId(0) }, + ).withLocals(localVariable("f", 0)) + ), + TestCase( + "INSERT INTO foo << {'a': 1} >> ON CONFLICT DO UPDATE EXCLUDED WHERE f.id > 2", + Expectation.Problems( + problem(1, 69, PlanningProblemDetails.UndefinedVariable("f", false)) + ) + ), + TestCase( + "INSERT INTO foo AS f << {'a': 1} >> ON CONFLICT DO UPDATE EXCLUDED WHERE foo.id > 2", + Expectation.Success( + ResolvedId(1, 74) { globalId("fake_uid_for_foo") }, + ).withLocals(localVariable("f", 0)) + ), + ) + } } diff --git a/partiql-lang/src/test/kotlin/org/partiql/lang/syntax/PartiQLParserTest.kt b/partiql-lang/src/test/kotlin/org/partiql/lang/syntax/PartiQLParserTest.kt index 640dc9bf84..55d025efaf 100644 --- a/partiql-lang/src/test/kotlin/org/partiql/lang/syntax/PartiQLParserTest.kt +++ b/partiql-lang/src/test/kotlin/org/partiql/lang/syntax/PartiQLParserTest.kt @@ -21,10 +21,15 @@ import com.amazon.ionelement.api.ionString import com.amazon.ionelement.api.loadSingleElement import org.junit.Ignore import org.junit.Test +import org.partiql.lang.ION import org.partiql.lang.ast.SourceLocationMeta import org.partiql.lang.ast.sourceLocation import org.partiql.lang.domains.PartiqlAst import org.partiql.lang.domains.id +import org.partiql.lang.errors.ErrorCode +import org.partiql.lang.errors.Property +import org.partiql.lang.syntax.antlr.PartiQLParser +import org.partiql.lang.util.getAntlrDisplayString import kotlin.concurrent.thread /** @@ -1750,6 +1755,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (bag (list (lit 1) @@ -1848,6 +1854,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (select (project (project_list @@ -1927,6 +1934,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (bag (list (lit 1) @@ -2201,6 +2209,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (select (project (project_list @@ -2226,6 +2235,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (bag (list (lit 1) @@ -2234,7 +2244,40 @@ class PartiQLParserTest : PartiQLParserTestBase() { (lit 3) (lit 4))) (do_replace - (excluded)))))) + (excluded) + null + ))))) + """ + ) + + @Test + fun insertWithOnConflictReplaceExcludedWithLiteralValueAndCondition() = assertExpression( + source = "INSERT into foo VALUES (1, 2), (3, 4) ON CONFLICT DO REPLACE EXCLUDED WHERE foo.id > 2", + expectedPigAst = """ + (dml + (operations + (dml_op_list + (insert + (id foo (case_insensitive) (unqualified)) + null + (bag + (list + (lit 1) + (lit 2)) + (list + (lit 3) + (lit 4))) + (do_replace + (excluded) + (gt + (path + (id foo (case_insensitive) (unqualified)) + (path_expr + (lit "id") + (case_insensitive))) + (lit 2) + ) + ))))) """ ) @@ -2246,7 +2289,33 @@ class PartiQLParserTest : PartiQLParserTestBase() { (operations (dml_op_list (insert - (id f (case_insensitive) (unqualified)) + (id foo (case_insensitive) (unqualified)) + f + (bag + (struct + (expr_pair + (lit "id") + (lit 1)) + (expr_pair + (lit "name") + (lit "bob")))) + (do_replace + (excluded) + null + ))))) + """ + ) + + @Test + fun insertWithOnConflictReplaceExcludedWithLiteralValueWithAliasAndCondition() = assertExpression( + source = "INSERT into foo AS f <<{'id': 1, 'name':'bob'}>> ON CONFLICT DO REPLACE EXCLUDED WHERE f.id > 2", + expectedPigAst = """ + (dml + (operations + (dml_op_list + (insert + (id foo (case_insensitive) (unqualified)) + f (bag (struct (expr_pair @@ -2256,7 +2325,16 @@ class PartiQLParserTest : PartiQLParserTestBase() { (lit "name") (lit "bob")))) (do_replace - (excluded)))))) + (excluded) + (gt + (path + (id f (case_insensitive) (unqualified)) + (path_expr + (lit "id") + (case_insensitive))) + (lit 2) + ) + ))))) """ ) @@ -2269,6 +2347,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (select (project (project_list @@ -2293,7 +2372,9 @@ class PartiQLParserTest : PartiQLParserTestBase() { null null))) (do_replace - (excluded)))))) + (excluded) + null + ))))) """ ) @@ -2306,6 +2387,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (bag (list (lit 1) @@ -2314,7 +2396,40 @@ class PartiQLParserTest : PartiQLParserTestBase() { (lit 3) (lit 4))) (do_update - (excluded)))))) + (excluded) + null + ))))) + """ + ) + + @Test + fun insertWithOnConflictUpdateExcludedWithLiteralValueAndCondition() = assertExpression( + source = "INSERT into foo VALUES (1, 2), (3, 4) ON CONFLICT DO UPDATE EXCLUDED WHERE foo.id > 2", + expectedPigAst = """ + (dml + (operations + (dml_op_list + (insert + (id foo (case_insensitive) (unqualified)) + null + (bag + (list + (lit 1) + (lit 2)) + (list + (lit 3) + (lit 4))) + (do_update + (excluded) + (gt + (path + (id foo (case_insensitive) (unqualified)) + (path_expr + (lit "id") + (case_insensitive))) + (lit 2) + ) + ))))) """ ) @@ -2326,7 +2441,8 @@ class PartiQLParserTest : PartiQLParserTestBase() { (operations (dml_op_list (insert - (id f (case_insensitive) (unqualified)) + (id foo (case_insensitive) (unqualified)) + f (bag (struct (expr_pair @@ -2336,7 +2452,41 @@ class PartiQLParserTest : PartiQLParserTestBase() { (lit "name") (lit "bob")))) (do_update - (excluded)))))) + (excluded) + null + ))))) + """ + ) + + @Test + fun insertWithOnConflictUpdateExcludedWithLiteralValueWithAliasAndCondition() = assertExpression( + source = "INSERT into foo AS f <<{'id': 1, 'name':'bob'}>> ON CONFLICT DO UPDATE EXCLUDED WHERE f.id > 2", + expectedPigAst = """ + (dml + (operations + (dml_op_list + (insert + (id foo (case_insensitive) (unqualified)) + f + (bag + (struct + (expr_pair + (lit "id") + (lit 1)) + (expr_pair + (lit "name") + (lit "bob")))) + (do_update + (excluded) + (gt + (path + (id f (case_insensitive) (unqualified)) + (path_expr + (lit "id") + (case_insensitive))) + (lit 2) + ) + ))))) """ ) @@ -2349,6 +2499,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (select (project (project_list @@ -2373,7 +2524,9 @@ class PartiQLParserTest : PartiQLParserTestBase() { null null))) (do_update - (excluded)))))) + (excluded) + null + ))))) """ ) @@ -2386,6 +2539,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (bag (struct (expr_pair @@ -2398,6 +2552,18 @@ class PartiQLParserTest : PartiQLParserTestBase() { """ ) + @Test + fun attemptConditionWithInsertDoNothing() = checkInputThrowingParserException( + "INSERT into foo <<{'id': 1, 'name':'bob'}>> ON CONFLICT DO NOTHING WHERE foo.id > 2", + ErrorCode.PARSE_UNEXPECTED_TOKEN, + expectErrorContextValues = mapOf( + Property.LINE_NUMBER to 1L, + Property.COLUMN_NUMBER to 68L, + Property.TOKEN_DESCRIPTION to PartiQLParser.WHERE.getAntlrDisplayString(), + Property.TOKEN_VALUE to ION.newSymbol("where") + ) + ) + @Test fun insertWithOnConflictDoNothingWithLiteralValueWithAlias() = assertExpression( source = "INSERT into foo AS f <<{'id': 1, 'name':'bob'}>> ON CONFLICT DO NOTHING", @@ -2406,7 +2572,8 @@ class PartiQLParserTest : PartiQLParserTestBase() { (operations (dml_op_list (insert - (id f (case_insensitive) (unqualified)) + (id foo (case_insensitive) (unqualified)) + f (bag (struct (expr_pair @@ -2428,6 +2595,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (select (project (project_list @@ -2464,6 +2632,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (bag (struct (expr_pair @@ -2473,7 +2642,9 @@ class PartiQLParserTest : PartiQLParserTestBase() { (lit "name") (lit "bob")))) (do_replace - (excluded)))))) + (excluded) + null + ))))) """ ) @@ -2485,7 +2656,8 @@ class PartiQLParserTest : PartiQLParserTestBase() { (operations (dml_op_list (insert - (id f (case_insensitive) (unqualified)) + (id foo (case_insensitive) (unqualified)) + f (bag (struct (expr_pair @@ -2495,7 +2667,9 @@ class PartiQLParserTest : PartiQLParserTestBase() { (lit "name") (lit "bob")))) (do_replace - (excluded)))))) + (excluded) + null + ))))) """ ) @@ -2508,6 +2682,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (bag (struct (expr_pair @@ -2517,7 +2692,9 @@ class PartiQLParserTest : PartiQLParserTestBase() { (lit "name") (lit "bob")))) (do_update - (excluded)))))) + (excluded) + null + ))))) """ ) @@ -2529,7 +2706,8 @@ class PartiQLParserTest : PartiQLParserTestBase() { (operations (dml_op_list (insert - (id f (case_insensitive) (unqualified)) + (id foo (case_insensitive) (unqualified)) + f (bag (struct (expr_pair @@ -2539,7 +2717,9 @@ class PartiQLParserTest : PartiQLParserTestBase() { (lit "name") (lit "bob")))) (do_update - (excluded)))))) + (excluded) + null + ))))) """ ) @@ -2552,6 +2732,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (select (project (project_list @@ -2576,7 +2757,9 @@ class PartiQLParserTest : PartiQLParserTestBase() { null null))) (do_replace - (excluded)))))) + (excluded) + null + ))))) """ ) @@ -2589,6 +2772,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id foo (case_insensitive) (unqualified)) + null (select (project (project_list @@ -2613,7 +2797,10 @@ class PartiQLParserTest : PartiQLParserTestBase() { null null))) (do_update - (excluded)))))) + (excluded) + null + ) + )))) """ ) @@ -3159,6 +3346,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id k (case_insensitive) (unqualified)) + null (bag (lit 1)) null))) @@ -3184,6 +3372,7 @@ class PartiQLParserTest : PartiQLParserTestBase() { (dml_op_list (insert (id k (case_insensitive) (unqualified)) + null (bag (lit 1)) null)))