diff --git a/sqlparser/lib/src/analysis/steps/linting_visitor.dart b/sqlparser/lib/src/analysis/steps/linting_visitor.dart index 8fa725b0..8258e1f2 100644 --- a/sqlparser/lib/src/analysis/steps/linting_visitor.dart +++ b/sqlparser/lib/src/analysis/steps/linting_visitor.dart @@ -388,9 +388,8 @@ class LintingVisitor extends RecursiveVisitor { visitChildren(e, arg); } - @override - void visitSetComponent(SetComponent e, void arg) { - final target = e.column.resolvedColumn; + void _checkForGeneratedColumn(Reference column) { + final target = column.resolvedColumn; if (target is TableColumn && target.isGenerated) { context.reportError( @@ -398,7 +397,43 @@ class LintingVisitor extends RecursiveVisitor { type: AnalysisErrorType.writeToGeneratedColumn, message: 'This column is generated, and generated columns cannot be ' 'updated explicitly.', - relevantNode: e.column, + relevantNode: column, + ), + ); + } + } + + @override + void visitSingleColumnSetComponent(SingleColumnSetComponent e, void arg) { + _checkForGeneratedColumn(e.column); + visitChildren(e, arg); + } + + @override + void visitMultiColumnSetComponent(MultiColumnSetComponent e, void arg) { + for (final column in e.columns) { + _checkForGeneratedColumn(column); + } + + if (e.rowValue is Tuple && + e.columns.length != (e.rowValue as Tuple).expressions.length) { + context.reportError( + AnalysisError( + type: AnalysisErrorType.cteColumnCountMismatch, + message: + 'Length of column-name-list must match length of row values.', + relevantNode: e.rowValue, + ), + ); + } else if (e.rowValue is SubQuery && + e.columns.length != + (e.rowValue as SubQuery).select.resolvedColumns?.length) { + context.reportError( + AnalysisError( + type: AnalysisErrorType.cteColumnCountMismatch, + message: + 'Length of column-name-list must match length of columns returned by SubQuery.', + relevantNode: e.rowValue, ), ); } @@ -494,6 +529,8 @@ class LintingVisitor extends RecursiveVisitor { // In expressions are tricky. The rhs can always be a row value, but the // lhs can only be a row value if the rhs is a subquery isAllowed = e == parent.inside || parent.inside is SubQuery; + } else if (parent is SetComponent) { + isAllowed = true; } if (!isAllowed) { diff --git a/sqlparser/lib/src/analysis/steps/reference_resolver.dart b/sqlparser/lib/src/analysis/steps/reference_resolver.dart index 9cdba948..313d8fd1 100644 --- a/sqlparser/lib/src/analysis/steps/reference_resolver.dart +++ b/sqlparser/lib/src/analysis/steps/reference_resolver.dart @@ -113,7 +113,9 @@ class ReferenceResolver if (table != null) { // Resolve the set components against the primary table for (final set in e.set) { - _resolveReferenceInTable(set.column, table); + for (final column in set.columns) { + _resolveReferenceInTable(column, table); + } } } diff --git a/sqlparser/lib/src/analysis/types/resolving_visitor.dart b/sqlparser/lib/src/analysis/types/resolving_visitor.dart index 1c0454f9..c3e843bd 100644 --- a/sqlparser/lib/src/analysis/types/resolving_visitor.dart +++ b/sqlparser/lib/src/analysis/types/resolving_visitor.dart @@ -131,12 +131,33 @@ class TypeResolver extends RecursiveVisitor { } @override - void visitSetComponent(SetComponent e, TypeExpectation arg) { + void visitSingleColumnSetComponent( + SingleColumnSetComponent e, TypeExpectation arg) { visit(e.column, const NoTypeExpectation()); _lazyCopy(e.expression, e.column); visit(e.expression, const NoTypeExpectation()); } + @override + void visitMultiColumnSetComponent( + MultiColumnSetComponent e, TypeExpectation arg) { + visitList(e.columns, const NoTypeExpectation()); + + final targets = e.resolvedTargetColumns ?? const []; + for (final column in targets) { + _handleColumn(column, e); + } + + final expectations = targets.map((r) { + if (r != null && session.graph.knowsType(r)) { + return ExactTypeExpectation(session.typeOf(r)!); + } + return const NoTypeExpectation(); + }).toList(); + + visit(e.rowValue, SelectTypeExpectation(expectations)); + } + @override void visitGroupBy(GroupBy e, TypeExpectation arg) { visitList(e.by, const NoTypeExpectation()); diff --git a/sqlparser/lib/src/ast/statements/update.dart b/sqlparser/lib/src/ast/statements/update.dart index 1cf41e81..2d4e47c9 100644 --- a/sqlparser/lib/src/ast/statements/update.dart +++ b/sqlparser/lib/src/ast/statements/update.dart @@ -78,15 +78,22 @@ class UpdateStatement extends CrudStatement } } -class SetComponent extends AstNode { +abstract class SetComponent extends AstNode { + List get columns; +} + +class SingleColumnSetComponent extends SetComponent { Reference column; Expression expression; - SetComponent({required this.column, required this.expression}); + @override + List get columns => [column]; + + SingleColumnSetComponent({required this.column, required this.expression}); @override R accept(AstVisitor visitor, A arg) { - return visitor.visitSetComponent(this, arg); + return visitor.visitSingleColumnSetComponent(this, arg); } @override @@ -98,3 +105,31 @@ class SetComponent extends AstNode { @override Iterable get childNodes => [column, expression]; } + +class MultiColumnSetComponent extends SetComponent { + @override + List columns; + // Will be either Tuple or SubQuery + Expression rowValue; + + List? get resolvedTargetColumns { + if (columns.isEmpty) return null; + return columns.map((c) => c.resolvedColumn).toList(); + } + + MultiColumnSetComponent({required this.columns, required this.rowValue}); + + @override + R accept(AstVisitor visitor, A arg) { + return visitor.visitMultiColumnSetComponent(this, arg); + } + + @override + void transformChildren(Transformer transformer, A arg) { + columns = transformer.transformChildren(columns, this, arg); + rowValue = transformer.transformChild(rowValue, this, arg); + } + + @override + Iterable get childNodes => [...columns, rowValue]; +} diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart index 3e0f4232..ae7922bc 100644 --- a/sqlparser/lib/src/ast/visitor.dart +++ b/sqlparser/lib/src/ast/visitor.dart @@ -43,7 +43,8 @@ abstract class AstVisitor { R visitDoNothing(DoNothing e, A arg); R visitDoUpdate(DoUpdate e, A arg); - R visitSetComponent(SetComponent e, A arg); + R visitSingleColumnSetComponent(SingleColumnSetComponent e, A arg); + R visitMultiColumnSetComponent(MultiColumnSetComponent e, A arg); R visitValuesSource(ValuesSource e, A arg); R visitSelectInsertSource(SelectInsertSource e, A arg); @@ -315,7 +316,16 @@ class RecursiveVisitor implements AstVisitor { } @override - R? visitSetComponent(SetComponent e, A arg) { + R? visitSingleColumnSetComponent(SingleColumnSetComponent e, A arg) { + return defaultSetComponent(e, arg); + } + + @override + R? visitMultiColumnSetComponent(MultiColumnSetComponent e, A arg) { + return defaultSetComponent(e, arg); + } + + R? defaultSetComponent(SetComponent e, A arg) { return defaultNode(e, arg); } diff --git a/sqlparser/lib/src/reader/parser.dart b/sqlparser/lib/src/reader/parser.dart index 51f74f84..63d85a75 100644 --- a/sqlparser/lib/src/reader/parser.dart +++ b/sqlparser/lib/src/reader/parser.dart @@ -1030,8 +1030,10 @@ class Parser { } /// Parses a [Tuple]. If [orSubQuery] is set (defaults to false), a [SubQuery] - /// (in brackets) will be accepted as well. - Expression _consumeTuple({bool orSubQuery = false}) { + /// (in brackets) will be accepted as well. If parsing a [Tuple], [usedAsRowValue] is + /// passed into the [Tuple] constructor. + Expression _consumeTuple( + {bool orSubQuery = false, bool usedAsRowValue = false}) { final firstToken = _consume(TokenType.leftParen, 'Expected opening parenthesis for tuple'); final expressions = []; @@ -1049,7 +1051,8 @@ class Parser { _consume( TokenType.rightParen, 'Expected right parenthesis to close tuple'); - return Tuple(expressions: expressions)..setSpan(firstToken, _previous); + return Tuple(expressions: expressions, usedAsRowValue: usedAsRowValue) + ..setSpan(firstToken, _previous); } else { _consume(TokenType.rightParen, 'Expected right parenthesis to finish subquery'); @@ -1686,19 +1689,47 @@ class Parser { )..setSpan(withClause?.first ?? updateToken, _previous); } + SingleColumnSetComponent _singleColumnSetComponent() { + final columnName = _consumeIdentifier('Expected a column name to set'); + final reference = Reference(columnName: columnName.identifier) + ..setSpan(columnName, columnName); + _consume(TokenType.equal, 'Expected = after the column name'); + final expr = expression(); + + return SingleColumnSetComponent(column: reference, expression: expr) + ..setSpan(columnName, _previous); + } + + MultiColumnSetComponent _multiColumnSetComponent() { + final first = _consume( + TokenType.leftParen, 'Expected opening parenthesis before column list'); + + final targetColumns = []; + do { + final columnName = _consumeIdentifier('Expected a column'); + targetColumns.add(Reference(columnName: columnName.identifier) + ..setSpan(columnName, columnName)); + } while (_matchOne(TokenType.comma)); + + _consume( + TokenType.rightParen, 'Expected closing parenthesis after column list'); + _consume(TokenType.equal, 'Expected = after the column name list'); + final tupleOrSubQuery = + _consumeTuple(orSubQuery: true, usedAsRowValue: true); + + return MultiColumnSetComponent( + columns: targetColumns, rowValue: tupleOrSubQuery) + ..setSpan(first, _previous); + } + List _setComponents() { final set = []; do { - final columnName = - _consume(TokenType.identifier, 'Expected a column name to set') - as IdentifierToken; - final reference = Reference(columnName: columnName.identifier) - ..setSpan(columnName, columnName); - _consume(TokenType.equal, 'Expected = after the column name'); - final expr = expression(); - - set.add(SetComponent(column: reference, expression: expr) - ..setSpan(columnName, _previous)); + if (_check(TokenType.leftParen)) { + set.add(_multiColumnSetComponent()); + } else { + set.add(_singleColumnSetComponent()); + } } while (_matchOne(TokenType.comma)); return set; diff --git a/sqlparser/lib/src/utils/ast_equality.dart b/sqlparser/lib/src/utils/ast_equality.dart index 10efa09e..984d843e 100644 --- a/sqlparser/lib/src/utils/ast_equality.dart +++ b/sqlparser/lib/src/utils/ast_equality.dart @@ -608,8 +608,14 @@ class EqualityEnforcingVisitor implements AstVisitor { } @override - void visitSetComponent(SetComponent e, void arg) { - _currentAs(e); + void visitSingleColumnSetComponent(SingleColumnSetComponent e, void arg) { + _currentAs(e); + _checkChildren(e); + } + + @override + void visitMultiColumnSetComponent(MultiColumnSetComponent e, void arg) { + _currentAs(e); _checkChildren(e); } diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index 229a9211..7c171d58 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -1062,12 +1062,21 @@ class NodeSqlBuilder extends AstVisitor { } @override - void visitSetComponent(SetComponent e, void arg) { + void visitSingleColumnSetComponent(SingleColumnSetComponent e, void arg) { visit(e.column, arg); symbol('=', spaceBefore: true, spaceAfter: true); visit(e.expression, arg); } + @override + void visitMultiColumnSetComponent(MultiColumnSetComponent e, void arg) { + symbol('(', spaceBefore: true); + _join(e.columns, ','); + symbol(')'); + symbol('=', spaceBefore: true, spaceAfter: true); + visit(e.rowValue, arg); + } + @override void visitStarFunctionParameter(StarFunctionParameter e, void arg) { symbol('*', spaceAfter: true); diff --git a/sqlparser/test/analysis/common_table_expressions_test.dart b/sqlparser/test/analysis/common_table_expressions_test.dart index 5f713aed..49539924 100644 --- a/sqlparser/test/analysis/common_table_expressions_test.dart +++ b/sqlparser/test/analysis/common_table_expressions_test.dart @@ -89,12 +89,25 @@ void main() { expect(result.errors, isEmpty); }); + test('update statements with column-name-list', () { + final result = engine.analyze( + 'WITH x AS (SELECT * FROM demo) UPDATE demo ' + 'SET (id, content) = (x.id, x.content) FROM x WHERE demo.id = x.id;'); + expect(result.errors, isEmpty); + }); + test('insert statements', () { final result = engine.analyze( 'WITH x AS (SELECT * FROM demo) INSERT INTO demo SELECT * FROM x;'); expect(result.errors, isEmpty); }); + test('insert statements with upsert using column-name-list', () { + final result = engine.analyze( + 'WITH x AS (SELECT * FROM demo) INSERT INTO demo SELECT * FROM x ON CONFLICT(content) DO UPDATE SET (id, content) = (0, \'hello\');'); + expect(result.errors, isEmpty); + }); + test('delete statements', () { final result = engine.analyze( 'WITH x AS (SELECT * FROM demo) DELETE FROM demo WHERE id IN (SELECT id FROM x);'); diff --git a/sqlparser/test/analysis/errors/update_column_name_list_count_mismatch_test.dart b/sqlparser/test/analysis/errors/update_column_name_list_count_mismatch_test.dart new file mode 100644 index 00000000..ab54d129 --- /dev/null +++ b/sqlparser/test/analysis/errors/update_column_name_list_count_mismatch_test.dart @@ -0,0 +1,72 @@ +import 'package:sqlparser/sqlparser.dart'; +import 'package:test/test.dart'; + +import '../data.dart'; +import 'utils.dart'; + +void main() { + late SqlEngine engine; + + setUp(() { + engine = SqlEngine(); + engine.registerTableFromSql(''' + CREATE TABLE foo ( + a INTEGER NOT NULL, + b INTEGER NOT NULL + ); + '''); + }); + + group("using column-name-list and tuple in update", () { + test('reports error if they have different sizes', () { + engine + .analyze("UPDATE foo SET (a, b) = (1);") + .expectError('(1)', type: AnalysisErrorType.cteColumnCountMismatch); + engine + .analyze("UPDATE foo SET (a) = (1,2);") + .expectError('(1,2)', type: AnalysisErrorType.cteColumnCountMismatch); + }); + test('reports no error if they have same sizes', () { + engine.analyze("UPDATE foo SET (a, b) = (1,2);").expectNoError(); + }); + }); + + group("using column-name-list and values in update", () { + test('reports error if they have different sizes', () { + engine.analyze("UPDATE foo SET (a, b) = (VALUES(1));").expectError( + "(VALUES(1))", + type: AnalysisErrorType.cteColumnCountMismatch); + engine.analyze("UPDATE foo SET (a) = (VALUES(1,2));").expectError( + "(VALUES(1,2))", + type: AnalysisErrorType.cteColumnCountMismatch); + }); + + test('reports no error if they have same sizes', () { + engine.analyze("UPDATE foo SET (a, b) = (VALUES(1,2));").expectNoError(); + }); + }); + + group("using column-name-list and subquery in update", () { + test('reports error if they have different sizes', () { + engine.analyze("UPDATE foo SET (a, b) = (SELECT 1);").expectError( + '(SELECT 1)', + type: AnalysisErrorType.cteColumnCountMismatch); + engine.analyze("UPDATE foo SET (a) = (SELECT 1,2);").expectError( + '(SELECT 1,2)', + type: AnalysisErrorType.cteColumnCountMismatch); + engine + .analyze( + "UPDATE foo SET (a, b) = (SELECT b FROM foo as f WHERE f.a=a);") + .expectError('(SELECT b FROM foo as f WHERE f.a=a)', + type: AnalysisErrorType.cteColumnCountMismatch); + }); + + test('reports no error if they have same sizes', () { + engine.analyze("UPDATE foo SET (a, b) = (SELECT 1,2);").expectNoError(); + engine + .analyze( + "UPDATE foo SET (a, b) = (SELECT b, a FROM foo as f WHERE f.a=a);") + .expectNoError(); + }); + }); +} diff --git a/sqlparser/test/analysis/errors/write_to_generated_column.dart b/sqlparser/test/analysis/errors/write_to_generated_column.dart index 0aee2f39..e30e339b 100644 --- a/sqlparser/test/analysis/errors/write_to_generated_column.dart +++ b/sqlparser/test/analysis/errors/write_to_generated_column.dart @@ -23,6 +23,13 @@ void main() { .expectError('g', type: AnalysisErrorType.writeToGeneratedColumn); }); + test('reports error when updating generated column with column-name-list', + () { + engine + .analyze("UPDATE a SET (ok, g) = ('new', 'old');") + .expectError('g', type: AnalysisErrorType.writeToGeneratedColumn); + }); + test('reports error when inserting generated column', () { engine .analyze('INSERT INTO a (ok, g) VALUES (?, ?)') diff --git a/sqlparser/test/analysis/types2/resolver_test.dart b/sqlparser/test/analysis/types2/resolver_test.dart index c114b57b..4c93161a 100644 --- a/sqlparser/test/analysis/types2/resolver_test.dart +++ b/sqlparser/test/analysis/types2/resolver_test.dart @@ -14,13 +14,18 @@ void main() { return TypeResolver(TypeInferenceSession(context))..run(context.root); } - ResolvedType? resolveFirstVariable(String sql, + Iterable resolveVariableTypes(String sql, {AnalyzeStatementOptions? options}) { final resolver = obtainResolver(sql, options: options); final session = resolver.session; - final variable = - session.context.root.allDescendants.whereType().first; - return session.typeOf(variable); + return session.context.root.allDescendants + .whereType() + .map((variable) => session.typeOf(variable)); + } + + ResolvedType? resolveFirstVariable(String sql, + {AnalyzeStatementOptions? options}) { + return resolveVariableTypes(sql, options: options).first; } ResolvedType? resolveResultColumn(String sql) { @@ -292,6 +297,32 @@ WITH RECURSIVE expect(type, const ResolvedType(type: BasicType.int)); }); + test('handles multi column set components in updates', () { + final variableTypes = + resolveVariableTypes('UPDATE demo SET (id, content) = (?, ?)'); + expect(variableTypes.first, const ResolvedType(type: BasicType.int)); + expect( + variableTypes.elementAt(1), const ResolvedType(type: BasicType.text)); + }); + + test('handles multi column set components in updates with select subquery', + () { + final variableTypes = + resolveVariableTypes('UPDATE demo SET (id, content) = (SELECT ?,?)'); + expect(variableTypes.first, const ResolvedType(type: BasicType.int)); + expect( + variableTypes.elementAt(1), const ResolvedType(type: BasicType.text)); + }); + + test('handles multi column set components in updates with values subquery', + () { + final variableTypes = + resolveVariableTypes('UPDATE demo SET (id, content) = (VALUES(?,?))'); + expect(variableTypes.first, const ResolvedType(type: BasicType.int)); + expect( + variableTypes.elementAt(1), const ResolvedType(type: BasicType.text)); + }); + test('infers offsets in frame specs', () { final type = resolveFirstVariable('SELECT SUM(id) OVER (ROWS ? PRECEDING)'); expect(type, const ResolvedType(type: BasicType.int)); diff --git a/sqlparser/test/parser/create_trigger_test.dart b/sqlparser/test/parser/create_trigger_test.dart index bbfe16ad..fb00bf9b 100644 --- a/sqlparser/test/parser/create_trigger_test.dart +++ b/sqlparser/test/parser/create_trigger_test.dart @@ -5,7 +5,7 @@ import 'utils.dart'; final _block = Block([ UpdateStatement(table: TableReference('tbl'), set: [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'foo'), expression: Reference(columnName: 'bar'), ), diff --git a/sqlparser/test/parser/drift_file_test.dart b/sqlparser/test/parser/drift_file_test.dart index 9421d087..397eceb8 100644 --- a/sqlparser/test/parser/drift_file_test.dart +++ b/sqlparser/test/parser/drift_file_test.dart @@ -151,7 +151,7 @@ END; UpdateStatement( table: TableReference('foo'), set: [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'bar'), expression: Reference(columnName: 'baz'), ), diff --git a/sqlparser/test/parser/insert_test.dart b/sqlparser/test/parser/insert_test.dart index 68ca8509..c0237639 100644 --- a/sqlparser/test/parser/insert_test.dart +++ b/sqlparser/test/parser/insert_test.dart @@ -133,7 +133,7 @@ void main() { UpsertClauseEntry( action: DoUpdate( [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'foo'), expression: NumericLiteral(2), ), @@ -158,7 +158,7 @@ void main() { UpsertClauseEntry( action: DoUpdate( [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'foo'), expression: NumericLiteral(2), ), @@ -189,7 +189,7 @@ void main() { onColumns: [IndexedColumn(Reference(columnName: 'bar'))], action: DoUpdate( [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'x'), expression: NumericLiteral(2), ), diff --git a/sqlparser/test/parser/multiple_statements_test.dart b/sqlparser/test/parser/multiple_statements_test.dart index c43aedaa..8b47b39b 100644 --- a/sqlparser/test/parser/multiple_statements_test.dart +++ b/sqlparser/test/parser/multiple_statements_test.dart @@ -18,7 +18,7 @@ void main() { UpdateStatement( table: TableReference('tbl'), set: [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'a'), expression: Reference(columnName: 'b'), ), diff --git a/sqlparser/test/parser/update_test.dart b/sqlparser/test/parser/update_test.dart index 01086373..591033de 100644 --- a/sqlparser/test/parser/update_test.dart +++ b/sqlparser/test/parser/update_test.dart @@ -8,11 +8,11 @@ final Map testCases = { or: FailureMode.rollback, table: TableReference('tbl'), set: [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'a'), expression: NullLiteral(), ), - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'b'), expression: Reference(columnName: 'c'), ) @@ -26,6 +26,57 @@ void main() { testAll(testCases); }); + test('parses updates with column-name-list and subquery', () { + testStatement( + ''' + UPDATE foo + SET (a, b) = (SELECT b, a FROM bar AS b WHERE b.id=foo.id); + ''', + UpdateStatement(table: TableReference('foo'), set: [ + MultiColumnSetComponent( + columns: [Reference(columnName: 'a'), Reference(columnName: 'b')], + rowValue: SubQuery( + select: SelectStatement( + columns: [ + ExpressionResultColumn( + expression: Reference(columnName: 'b'), + ), + ExpressionResultColumn( + expression: Reference(columnName: 'a'), + ), + ], + from: TableReference('bar', as: 'b'), + where: BinaryExpression( + Reference(entityName: 'b', columnName: 'id'), + token(TokenType.equal), + Reference(entityName: 'foo', columnName: 'id'), + ), + ), + ), + ) + ]), + ); + }); + + test('parses updates with column-name-list and scalar rowValues', () { + testStatement( + ''' + UPDATE foo + SET (a, b) = (b, 3+4); + ''', + UpdateStatement(table: TableReference('foo'), set: [ + MultiColumnSetComponent( + columns: [Reference(columnName: 'a'), Reference(columnName: 'b')], + rowValue: Tuple(expressions: [ + Reference(columnName: "b"), + BinaryExpression( + NumericLiteral(3), token(TokenType.plus), NumericLiteral(4)), + ], usedAsRowValue: true), + ) + ]), + ); + }); + test('parses updates with FROM clause', () { testStatement( ''' @@ -38,7 +89,7 @@ void main() { UpdateStatement( table: TableReference('inventory'), set: [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'quantity'), expression: BinaryExpression( Reference(columnName: 'quantity'), @@ -85,7 +136,7 @@ void main() { UpdateStatement( table: TableReference('tbl'), set: [ - SetComponent( + SingleColumnSetComponent( column: Reference(columnName: 'foo'), expression: Reference(columnName: 'bar'), ), diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index 9c221cbf..4bc75a67 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -407,6 +407,11 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3; 'ON CONFLICT DO UPDATE SET a = b, c = d WHERE d < a;'); }); + test('upsert - update with column-name-list', () { + testFormat('INSERT INTO foo VALUES (1, 2, 3) ' + 'ON CONFLICT DO UPDATE SET (a, c) = (b, d) WHERE d < a;'); + }); + test('upsert - multiple clauses', () { testFormat('INSERT INTO foo VALUES (1, 2, 3) ' 'ON CONFLICT DO NOTHING ' @@ -419,6 +424,14 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3; testFormat('UPDATE foo SET bar = baz WHERE 1;'); }); + test('with column-name-list', () { + testFormat('UPDATE foo SET (bar, baz) = (baz, bar) WHERE 1;'); + }); + + test('with column-name-list and subquery', () { + testFormat('UPDATE foo SET (bar, baz) = (SELECT 1,2) WHERE 1;'); + }); + test('with returning', () { testFormat('UPDATE foo SET bar = baz RETURNING *'); });