diff --git a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogTableTest.java b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogTableTest.java index 8d9f6d1d8760..090e38fd8b40 100644 --- a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogTableTest.java +++ b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/CatalogTableTest.java @@ -83,11 +83,14 @@ import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import java.util.function.Supplier; +import org.apache.ignite.internal.catalog.commands.AlterTableAddColumnCommand; import org.apache.ignite.internal.catalog.commands.AlterTableAlterColumnCommand; import org.apache.ignite.internal.catalog.commands.AlterTableAlterColumnCommandBuilder; +import org.apache.ignite.internal.catalog.commands.AlterTableDropColumnCommand; import org.apache.ignite.internal.catalog.commands.AlterTableSetPropertyCommand; import org.apache.ignite.internal.catalog.commands.CatalogUtils; import org.apache.ignite.internal.catalog.commands.ColumnParams; @@ -513,6 +516,79 @@ public void testDropColumnWithIndexColumns() { ); } + @Test + public void testDropMultipleColumns() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogTableDescriptor tableBefore = actualTable(TABLE_NAME); + int colCountBefore = tableBefore.columns().size(); + + tryApplyAndExpectApplied(dropColumnParams(TABLE_NAME, "VAL", "DEC", "STR")); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + + assertNull(table.column("VAL")); + assertNull(table.column("DEC")); + assertNull(table.column("STR")); + assertEquals(colCountBefore - 3, table.columns().size()); + } + + @Test + public void testDropMultipleColumnsWithMissingColumn() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + assertThat( + manager.execute(dropColumnParams(TABLE_NAME, "VAL", "fake")), + willThrowFast(CatalogValidationException.class, "Column with name 'fake' not found in table 'PUBLIC.test_table'.") + ); + + // Validate no column was dropped. + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table.column("VAL")); + } + + @Test + public void testDropMultipleColumnsWithPrimaryKeyColumn() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + assertThat( + manager.execute(dropColumnParams(TABLE_NAME, "VAL", "ID")), + willThrowFast(CatalogValidationException.class, "Deleting column `ID` belonging to primary key is not allowed") + ); + + // Validate no column was dropped. + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table.column("VAL")); + assertNotNull(table.column("ID")); + } + + @Test + public void testDropMultipleColumnsWithIndexColumn() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + tryApplyAndExpectApplied(simpleIndex()); + + assertThat( + manager.execute(dropColumnParams(TABLE_NAME, "VAL", "DEC")), + willThrowFast( + CatalogValidationException.class, + "Deleting column 'VAL' used by index(es) [myIndex], it is not allowed" + ) + ); + + // Validate no column was dropped. + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table.column("VAL")); + assertNotNull(table.column("DEC")); + } + + @Test + public void testDropMultipleColumnsFromNonExistingTable() { + assertThat( + manager.execute(dropColumnParams(TABLE_NAME, "VAL", "DEC")), + willThrowFast(CatalogValidationException.class, "Table with name 'PUBLIC.test_table' not found.") + ); + } + @Test public void testAddColumnWithNotExistingTable() { assertThat(manager.execute(addColumnParams(TABLE_NAME, columnParams("key", INT32, true))), @@ -1534,6 +1610,347 @@ private TestColumnTypeParams(ColumnType type, @Nullable Integer precision, @Null } } + @Test + public void testAddMultipleColumnsAssignsSequentialIds() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogTableDescriptor tableBefore = actualTable(TABLE_NAME); + assertNotNull(tableBefore); + int maxId = tableBefore.columns().stream().mapToInt(CatalogTableColumnDescriptor::id).max().orElse(0); + + tryApplyAndExpectApplied(addColumnParams(TABLE_NAME, + columnParams("COL_A", INT32, true), + columnParams("COL_B", STRING, 100, true), + columnParams("COL_C", DECIMAL, true, DFLT_TEST_PRECISION, 2) + )); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table); + assertEquals(maxId + 1, table.column("COL_A").id()); + assertEquals(maxId + 2, table.column("COL_B").id()); + assertEquals(maxId + 3, table.column("COL_C").id()); + } + + @Test + public void testAddMultipleColumnsPreservesProperties() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + tryApplyAndExpectApplied(addColumnParams(TABLE_NAME, + columnParamsBuilder("COL_INT", INT32, true).defaultValue(constant(42)).build(), + columnParamsBuilder("COL_STR", STRING).length(200).defaultValue(constant("hello")).build(), + columnParams("COL_DEC", DECIMAL, true, 15, 5) + )); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table); + + CatalogTableColumnDescriptor colInt = table.column("COL_INT"); + assertNotNull(colInt); + assertEquals(INT32, colInt.type()); + assertTrue(colInt.nullable()); + assertEquals(constant(42), colInt.defaultValue()); + + CatalogTableColumnDescriptor colStr = table.column("COL_STR"); + assertNotNull(colStr); + assertEquals(STRING, colStr.type()); + assertEquals(200, colStr.length()); + assertEquals(constant("hello"), colStr.defaultValue()); + + CatalogTableColumnDescriptor colDec = table.column("COL_DEC"); + assertNotNull(colDec); + assertEquals(DECIMAL, colDec.type()); + assertTrue(colDec.nullable()); + assertEquals(15, colDec.precision()); + assertEquals(5, colDec.scale()); + } + + @Test + public void testAddMultipleColumnsPreservesOrder() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + tryApplyAndExpectApplied(addColumnParams(TABLE_NAME, + columnParams("COL_A", INT32, true), + columnParams("COL_B", INT32, true), + columnParams("COL_C", INT32, true) + )); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table); + List columns = table.columns(); + + // simpleTable creates 6 columns, new ones should be at indices 6, 7, 8 + assertEquals("COL_A", columns.get(6).name()); + assertEquals("COL_B", columns.get(7).name()); + assertEquals("COL_C", columns.get(8).name()); + + assertEquals(6, table.columnIndex("COL_A")); + assertEquals(7, table.columnIndex("COL_B")); + assertEquals(8, table.columnIndex("COL_C")); + } + + @Test + public void testAddMultipleColumnsIncrementsTableVersionOnce() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogTableDescriptor tableBefore = actualTable(TABLE_NAME); + assertNotNull(tableBefore); + assertEquals(1, tableBefore.latestSchemaVersion()); + + tryApplyAndExpectApplied(addColumnParams(TABLE_NAME, + columnParams("COL_A", INT32, true), + columnParams("COL_B", INT32, true), + columnParams("COL_C", INT32, true) + )); + + CatalogTableDescriptor tableAfter = actualTable(TABLE_NAME); + assertNotNull(tableAfter); + assertEquals(2, tableAfter.latestSchemaVersion()); + } + + @Test + public void testAddMultipleColumnsTimeTravelVisibility() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + long timestampBeforeAdd = clock.nowLong(); + + tryApplyAndExpectApplied(addColumnParams(TABLE_NAME, + columnParams("COL_A", INT32, true), + columnParams("COL_B", INT32, true) + )); + + // At the old timestamp, the new columns should not be visible. + CatalogTableDescriptor oldTable = manager.activeCatalog(timestampBeforeAdd).table(SCHEMA_NAME, TABLE_NAME); + assertNotNull(oldTable); + assertNull(oldTable.column("COL_A")); + assertNull(oldTable.column("COL_B")); + + // At the latest timestamp, the new columns should be visible. + CatalogTableDescriptor newTable = actualTable(TABLE_NAME); + assertNotNull(newTable); + assertNotNull(newTable.column("COL_A")); + assertNotNull(newTable.column("COL_B")); + } + + @Test + public void testAddMultipleColumnsFiresEventWithAllColumns() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + var fireEventFuture = new CompletableFuture(); + + manager.listen(CatalogEvent.TABLE_ALTER, fromConsumer(fireEventFuture, (AddColumnEventParameters parameters) -> { + List descriptors = parameters.descriptors(); + + assertThat(descriptors, hasSize(2)); + assertEquals("COL_A", descriptors.get(0).name()); + assertEquals("COL_B", descriptors.get(1).name()); + })); + + tryApplyAndExpectApplied(addColumnParams(TABLE_NAME, + columnParams("COL_A", INT32, true), + columnParams("COL_B", INT32, true) + )); + + assertThat(fireEventFuture, willCompleteSuccessfully()); + } + + @Test + public void testAddSingleColumnIfNotExistsColumnExists() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableAddColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(List.of(columnParams("VAL", INT32, true))) + .ifColumnNotExists(true) + .build(); + + tryApplyAndExpectNotApplied(command); + } + + @Test + public void testAddSingleColumnIfNotExistsColumnDoesNotExist() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableAddColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(List.of(columnParams(NEW_COLUMN_NAME, INT32, true))) + .ifColumnNotExists(true) + .build(); + + tryApplyAndExpectApplied(command); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table); + assertNotNull(table.column(NEW_COLUMN_NAME)); + } + + @Test + public void testAddMultipleColumnsIfNotExistsAllNew() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableAddColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(List.of( + columnParams(NEW_COLUMN_NAME, INT32, true), + columnParams(NEW_COLUMN_NAME_2, INT32, true) + )) + .ifColumnNotExists(true) + .build(); + + tryApplyAndExpectApplied(command); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table); + assertNotNull(table.column(NEW_COLUMN_NAME)); + assertNotNull(table.column(NEW_COLUMN_NAME_2)); + } + + @Test + public void testAddMultipleColumnsIfNotExistsPartialOverlap() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableAddColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(List.of( + columnParams("VAL", INT32, true), + columnParams(NEW_COLUMN_NAME, INT32, true) + )) + .ifColumnNotExists(true) + .build(); + + tryApplyAndExpectApplied(command); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table); + assertNotNull(table.column(NEW_COLUMN_NAME)); + // VAL should still have its original type (INT32) and be unchanged. + assertNotNull(table.column("VAL")); + } + + @Test + public void testAddMultipleColumnsIfNotExistsAllExist() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableAddColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(List.of( + columnParams("VAL", INT32, true), + columnParams("STR", STRING, 100, true) + )) + .ifColumnNotExists(true) + .build(); + + tryApplyAndExpectNotApplied(command); + } + + @Test + public void testAddMultipleColumnsFailsAtomicallyWhenOneAlreadyExists() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + // Try to add columns where "VAL" already exists — without ifColumnNotExists, this should fail. + assertThat( + manager.execute(addColumnParams(TABLE_NAME, + columnParams(NEW_COLUMN_NAME, INT32, true), + columnParams("VAL", INT32, true) + )), + willThrow(CatalogValidationException.class) + ); + + // Verify NEWCOL was NOT added (atomic rollback). + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNotNull(table); + assertNull(table.column(NEW_COLUMN_NAME)); + } + + @Test + public void testDropMultipleColumnsIfExistsAllExist() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableDropColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(Set.of("VAL", "DEC")) + .ifColumnExists(true) + .build(); + + tryApplyAndExpectApplied(command); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNull(table.column("VAL")); + assertNull(table.column("DEC")); + } + + @Test + public void testDropMultipleColumnsIfExistsPartialOverlap() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableDropColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(Set.of("VAL", "fake")) + .ifColumnExists(true) + .build(); + + tryApplyAndExpectApplied(command); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNull(table.column("VAL")); + } + + @Test + public void testDropMultipleColumnsIfExistsNoneExist() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableDropColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(Set.of("fake1", "fake2")) + .ifColumnExists(true) + .build(); + + tryApplyAndExpectNotApplied(command); + } + + @Test + public void testDropSingleColumnIfExists() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableDropColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(Set.of("fake")) + .ifColumnExists(true) + .build(); + + tryApplyAndExpectNotApplied(command); + + // Verify table is unchanged. + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertEquals(6, table.columns().size()); + } + + @Test + public void testDropSingleColumnIfExistsColumnExists() { + tryApplyAndExpectApplied(simpleTable(TABLE_NAME)); + + CatalogCommand command = AlterTableDropColumnCommand.builder() + .schemaName(SCHEMA_NAME) + .tableName(TABLE_NAME) + .columns(Set.of("VAL")) + .ifColumnExists(true) + .build(); + + tryApplyAndExpectApplied(command); + + CatalogTableDescriptor table = actualTable(TABLE_NAME); + assertNull(table.column("VAL")); + assertEquals(5, table.columns().size()); + } + private @Nullable CatalogTableDescriptor actualTable(String tableName) { return manager.activeCatalog(clock.nowLong()).table(SCHEMA_NAME, tableName); } diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java index 3eb7745d6fac..3b3914d47d89 100644 --- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java +++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/SqlDdlParserTest.java @@ -796,6 +796,87 @@ public void tablePropertiesParsingErrors(String stmt, String error) { () -> parse(stmt)); } + @ParameterizedTest + @CsvSource(delimiter = ';', value = { + // ADD COLUMN missing column definition + "alter table t add column;" + + " Failed to parse query: Encountered \"\" at line 1, column 24", + "alter table t add;" + + " Failed to parse query: Encountered \"\" at line 1, column 17", + "alter table t add column if not exists;" + + " Failed to parse query: Encountered \"\" at line 1, column 38", + "alter table t add if not exists;" + + " Failed to parse query: Encountered \"\" at line 1, column 31", + + // ADD COLUMN with wrong IF EXISTS (should be IF NOT EXISTS) + "alter table t add column if exists c int;" + + " Failed to parse query: Encountered \"exists\" at line 1, column 29", + "alter table if exists t add column if exists c int;" + + " Failed to parse query: Encountered \"exists\" at line 1, column 39", + + // DROP COLUMN missing column name + "alter table t drop column;" + + " Failed to parse query: Encountered \"\" at line 1, column 25", + "alter table t drop;" + + " Failed to parse query: Encountered \"\" at line 1, column 18", + "alter table t drop column if exists;" + + " Failed to parse query: Encountered \"\" at line 1, column 35", + "alter table t drop if exists;" + + " Failed to parse query: Encountered \"\" at line 1, column 28", + + // DROP COLUMN with wrong IF NOT EXISTS (should be IF EXISTS) + "alter table t drop column if not exists c;" + + " Failed to parse query: Encountered \"not\" at line 1, column 30", + "alter table if exists t drop column if not exists c;" + + " Failed to parse query: Encountered \"not\" at line 1, column 40", + + // ADD single column with reserved word as name + "alter table t add column select int;" + + " Failed to parse query: Encountered \"select\" at line 1, column 26", + "alter table t add select int;" + + " Failed to parse query: Encountered \"select\" at line 1, column 19", + "alter table t add column if not exists select int;" + + " Failed to parse query: Encountered \"select\" at line 1, column 40", + + // ADD multiple columns with reserved word as name + "alter table t add column (select int, b varchar);" + + " Failed to parse query: Encountered \"select\" at line 1, column 27", + "alter table t add column (a int, select varchar);" + + " Failed to parse query: Encountered \"select\" at line 1, column 34", + "alter table t add column if not exists (select int, b varchar);" + + " Failed to parse query: Encountered \"select\" at line 1, column 41", + "alter table t add (select int, b varchar);" + + " Failed to parse query: Encountered \"select\" at line 1, column 20", + + // ADD column with missing type + "alter table t add column a;" + + " Failed to parse query: Encountered \"\" at line 1, column 26", + "alter table t add column (a, b varchar);" + + " Failed to parse query: Encountered \",\" at line 1, column 28", + "alter table t add column (a int, b);" + + " Failed to parse query: Encountered \")\" at line 1, column 35", + + // DROP column with reserved word as name + "alter table t drop column select;" + + " Failed to parse query: Encountered \"select\" at line 1, column 27", + "alter table t drop select;" + + " Failed to parse query: Encountered \"select\" at line 1, column 20", + "alter table t drop column if exists select;" + + " Failed to parse query: Encountered \"select\" at line 1, column 37", + + // DROP multiple columns with reserved word as name + "alter table t drop column (select, b);" + + " Failed to parse query: Encountered \"select\" at line 1, column 28", + "alter table t drop column (a, select);" + + " Failed to parse query: Encountered \"select\" at line 1, column 31", + }) + public void alterTableAddDropColumnParsingErrors(String stmt, String error) { + assertThrowsSqlException( + Sql.STMT_PARSE_ERR, + error, + () -> parse(stmt)); + } + @Test public void createIndexSimpleCase() { var query = "create index my_index on my_table (col)"; @@ -1169,6 +1250,113 @@ public void alterTableAddColumnNotNull() { expectUnparsed(addColumn, "ALTER TABLE \"T\" ADD COLUMN \"C\" INTEGER NOT NULL"); } + @Test + public void alterTableAddMultipleColumns() { + SqlNode sqlNode = parse("ALTER TABLE t ADD COLUMN (a INT, b VARCHAR, c DECIMAL(10, 2))"); + + IgniteSqlAlterTableAddColumn addColumn = assertInstanceOf(IgniteSqlAlterTableAddColumn.class, sqlNode); + + assertThat(addColumn.name.names, is(List.of("T"))); + assertThat(addColumn.ifColumnNotExists(), is(false)); + assertThat(addColumn.columns().size(), is(3)); + + SqlColumnDeclaration col0 = (SqlColumnDeclaration) addColumn.columns().get(0); + SqlColumnDeclaration col1 = (SqlColumnDeclaration) addColumn.columns().get(1); + SqlColumnDeclaration col2 = (SqlColumnDeclaration) addColumn.columns().get(2); + + expectColumnBasic(col0, "A", ColumnStrategy.NULLABLE, "INTEGER", true); + expectColumnBasic(col1, "B", ColumnStrategy.NULLABLE, "VARCHAR", true); + expectColumnBasic(col2, "C", ColumnStrategy.NULLABLE, "DECIMAL", true); + + expectUnparsed(addColumn, "ALTER TABLE \"T\" ADD COLUMN \"A\" INTEGER, \"B\" VARCHAR, \"C\" DECIMAL(10, 2)"); + } + + @Test + public void alterTableAddMultipleColumnsIfNotExists() { + SqlNode sqlNode = parse("ALTER TABLE t ADD COLUMN IF NOT EXISTS (a INT, b VARCHAR NOT NULL)"); + + IgniteSqlAlterTableAddColumn addColumn = assertInstanceOf(IgniteSqlAlterTableAddColumn.class, sqlNode); + + assertThat(addColumn.name.names, is(List.of("T"))); + assertThat(addColumn.ifColumnNotExists(), is(true)); + assertThat(addColumn.columns().size(), is(2)); + + SqlColumnDeclaration col0 = (SqlColumnDeclaration) addColumn.columns().get(0); + SqlColumnDeclaration col1 = (SqlColumnDeclaration) addColumn.columns().get(1); + + expectColumnBasic(col0, "A", ColumnStrategy.NULLABLE, "INTEGER", true); + expectColumnBasic(col1, "B", ColumnStrategy.NOT_NULLABLE, "VARCHAR", false); + + expectUnparsed(addColumn, "ALTER TABLE \"T\" ADD COLUMN IF NOT EXISTS \"A\" INTEGER, \"B\" VARCHAR NOT NULL"); + } + + @Test + public void alterTableAddMultipleColumnsWithDefaults() { + SqlNode sqlNode = parse("ALTER TABLE t ADD COLUMN (a INT DEFAULT 1, b VARCHAR DEFAULT 'hello')"); + + IgniteSqlAlterTableAddColumn addColumn = assertInstanceOf(IgniteSqlAlterTableAddColumn.class, sqlNode); + + assertThat(addColumn.columns().size(), is(2)); + + SqlColumnDeclaration col0 = (SqlColumnDeclaration) addColumn.columns().get(0); + SqlColumnDeclaration col1 = (SqlColumnDeclaration) addColumn.columns().get(1); + + assertNotNull(col0.expression); + assertNotNull(col1.expression); + + expectUnparsed(addColumn, "ALTER TABLE \"T\" ADD COLUMN \"A\" INTEGER DEFAULT (1), \"B\" VARCHAR DEFAULT ('hello')"); + } + + @Test + public void alterTableAddMultipleColumnsWithoutColumnKeyword() { + SqlNode sqlNode = parse("ALTER TABLE t ADD (a INT, b VARCHAR)"); + + IgniteSqlAlterTableAddColumn addColumn = assertInstanceOf(IgniteSqlAlterTableAddColumn.class, sqlNode); + + assertThat(addColumn.name.names, is(List.of("T"))); + assertThat(addColumn.columns().size(), is(2)); + + SqlColumnDeclaration col0 = (SqlColumnDeclaration) addColumn.columns().get(0); + SqlColumnDeclaration col1 = (SqlColumnDeclaration) addColumn.columns().get(1); + + expectColumnBasic(col0, "A", ColumnStrategy.NULLABLE, "INTEGER", true); + expectColumnBasic(col1, "B", ColumnStrategy.NULLABLE, "VARCHAR", true); + + expectUnparsed(addColumn, "ALTER TABLE \"T\" ADD COLUMN \"A\" INTEGER, \"B\" VARCHAR"); + } + + @Test + public void alterTableAddMultipleColumnsWithoutColumnKeywordIfNotExists() { + SqlNode sqlNode = parse("ALTER TABLE t ADD IF NOT EXISTS (a INT, b VARCHAR)"); + + IgniteSqlAlterTableAddColumn addColumn = assertInstanceOf(IgniteSqlAlterTableAddColumn.class, sqlNode); + + assertThat(addColumn.name.names, is(List.of("T"))); + assertThat(addColumn.ifColumnNotExists(), is(true)); + assertThat(addColumn.columns().size(), is(2)); + + SqlColumnDeclaration col0 = (SqlColumnDeclaration) addColumn.columns().get(0); + SqlColumnDeclaration col1 = (SqlColumnDeclaration) addColumn.columns().get(1); + + expectColumnBasic(col0, "A", ColumnStrategy.NULLABLE, "INTEGER", true); + expectColumnBasic(col1, "B", ColumnStrategy.NULLABLE, "VARCHAR", true); + + expectUnparsed(addColumn, "ALTER TABLE \"T\" ADD COLUMN IF NOT EXISTS \"A\" INTEGER, \"B\" VARCHAR"); + } + + @Test + public void alterTableAddMultipleColumnsIfTableExists() { + SqlNode sqlNode = parse("ALTER TABLE IF EXISTS t ADD COLUMN (a INT, b VARCHAR)"); + + IgniteSqlAlterTableAddColumn addColumn = assertInstanceOf(IgniteSqlAlterTableAddColumn.class, sqlNode); + + assertThat(addColumn.name.names, is(List.of("T"))); + assertThat(addColumn.ifExists(), is(true)); + assertThat(addColumn.columns().size(), is(2)); + + expectUnparsed(addColumn, "ALTER TABLE IF EXISTS \"T\" ADD COLUMN \"A\" INTEGER, \"B\" VARCHAR"); + } + @Test public void alterTableDropColumn() { SqlNode sqlNode = parse("ALTER TABLE t DROP COLUMN c");