Repository: questdb/questdb
Pull request #5675: fix(sql): fixed error reported when trying to add an already existing column with IF NOT EXISTS
Author: login: glasstiger
Created: 2025-05-14T16:27:03Z | Merged: 2025-05-16T20:38:24Z

When running a SQL like the below:
```
ALTER TABLE tab ADD COLUMN IF NOT EXISTS col1 LONG;
```
and `col1` already exists, we expect the statement to succeed.
However, it results in the following error instead:
```
statements containing multiple transactions, such as 'alter table add column col1, col2' are currently not supported for WAL tables [table=tab, oldStructureVersion=2, newStructureVersion=2]
```

The error message is slightly misleading.
The code in `WalWriter` checks that structure version increases by 1 and only by 1.
In this case structure version did not increase because the column already existed, and the add column operation became no-op.
No structure version increase should be treated separately, or the error message should be rephrased to account for it.

Also, when the column to be added is already present in the table, and the `IF NOT EXISTS` clause is added to the `ALTER` statement, we do not have to do anything.
It is enough to compile the SQL to an empty instruction, no need to actually run an alter operation which does nothing.

---------------------------------------

The PR also fixes another existing bug.
When `IF NOT EXISTS` is specified we also need to check that the type is the same, not enough to check the name only.
`IF NOT EXISTS` should make the SQL pass only if both, the column name and the type are the same.


Diff:
diff --git a/core/rust/qdb-core/src/error.rs b/core/rust/qdb-core/src/error.rs
index 62e361e5adee..4d7da11016bf 100644
--- a/core/rust/qdb-core/src/error.rs
+++ b/core/rust/qdb-core/src/error.rs
@@ -225,7 +225,7 @@ mod tests {
 
     #[test]
     pub fn test_io_error() {
-        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "io_error");
+        let io_err = std::io::Error::other("io_error");
         let mut err = CoreErrorReason::Io(Arc::new(io_err)).into_err();
         err.add_context("context_msg");
         let result: Result<(), _> = Err(err);
@@ -252,7 +252,7 @@ mod tests {
 
     #[test]
     fn io_err_into_coreerror() {
-        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "io_error");
+        let io_err = std::io::Error::other("io_error");
         let err: CoreError = io_err.into();
         assert_eq!(err.to_string(), "io_error");
     }
@@ -265,7 +265,7 @@ mod tests {
 
     #[test]
     pub fn test_no_context() {
-        let io_err = std::io::Error::new(std::io::ErrorKind::Other, "io_error");
+        let io_err = std::io::Error::other("io_error");
         let err = CoreErrorReason::Io(Arc::new(io_err)).into_err();
         assert_eq!(err.to_string(), "io_error");
     }
diff --git a/core/src/main/java/io/questdb/cairo/wal/WalWriter.java b/core/src/main/java/io/questdb/cairo/wal/WalWriter.java
index a92cb6bdbaaa..09755757dc52 100644
--- a/core/src/main/java/io/questdb/cairo/wal/WalWriter.java
+++ b/core/src/main/java/io/questdb/cairo/wal/WalWriter.java
@@ -831,8 +831,9 @@ private long applyStructural(AlterOperation alterOp) {
                 if (metaValidatorSvc.structureVersion != getColumnStructureVersion() + 1) {
                     retry = false;
                     throw CairoException.nonCritical()
-                            .put("statements containing multiple transactions, such as 'alter table add column col1, col2'" +
-                                    " are currently not supported for WAL tables [table=").put(tableToken.getTableName())
+                            .put("statement is either no-op,")
+                            .put(" or contains multiple transactions, such as 'alter table add column col1, col2',")
+                            .put(" and currently not supported for WAL tables [table=").put(tableToken.getTableName())
                             .put(", oldStructureVersion=").put(getColumnStructureVersion())
                             .put(", newStructureVersion=").put(metaValidatorSvc.structureVersion).put(']');
                 }
diff --git a/core/src/main/java/io/questdb/griffin/CompiledQueryImpl.java b/core/src/main/java/io/questdb/griffin/CompiledQueryImpl.java
index 42209c688dc2..a734a2e25693 100644
--- a/core/src/main/java/io/questdb/griffin/CompiledQueryImpl.java
+++ b/core/src/main/java/io/questdb/griffin/CompiledQueryImpl.java
@@ -49,6 +49,7 @@ public class CompiledQueryImpl implements CompiledQuery, Mutable {
     // number of rows either returned by SELECT operation or affected by UPDATE or INSERT
     private long affectedRowsCount;
     private AlterOperation alterOp;
+    private boolean done;
     private InsertOperation insertOp;
     private boolean isExecutedAtParseTime;
     private Operation operation;
@@ -61,14 +62,12 @@ public class CompiledQueryImpl implements CompiledQuery, Mutable {
     private UpdateOperation updateOp;
 
     public CompiledQueryImpl(CairoEngine engine) {
-        // type inference fails on java 8 if <UpdateOperation> is removed
         updateOperationDispatcher = new OperationDispatcher<>(engine, "sync 'UPDATE' execution") {
             @Override
             protected long apply(UpdateOperation operation, TableWriterAPI writerAPI) {
                 return writerAPI.apply(operation);
             }
         };
-        // type inference fails on java 8 if <AlterOperation> is removed
         alterOperationDispatcher = new OperationDispatcher<>(engine, "Alter table execute") {
             @Override
             protected long apply(AlterOperation operation, TableWriterAPI writerAPI) {
@@ -92,6 +91,11 @@ public void clear() {
         this.statementName = null;
         this.operation = null;
         this.isExecutedAtParseTime = false;
+        this.done = false;
+    }
+
+    public void done() {
+        this.done = true;
     }
 
     @Override
@@ -105,6 +109,10 @@ public OperationFuture execute(
             SCSequence eventSubSeq,
             boolean closeOnDone
     ) throws SqlException {
+        if (done) {
+            return doneFuture.of(0);
+        }
+
         switch (type) {
             case INSERT:
                 return insertOp.execute(sqlExecutionContext);
diff --git a/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java b/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java
index 9bced6a36b7e..2f845eec98a7 100644
--- a/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java
+++ b/core/src/main/java/io/questdb/griffin/SqlCompilerImpl.java
@@ -693,7 +693,22 @@ private void alterTableAddColumn(
                     tok = SqlUtil.fetchNext(lexer);
                     if (tok != null && isExistsKeyword(tok)) {
                         tok = SqlUtil.fetchNext(lexer); // captured column name
-                        if (tableMetadata.getColumnIndexQuiet(tok) != -1) {
+                        final int columnIndex = tableMetadata.getColumnIndexQuiet(tok);
+                        if (columnIndex != -1) {
+                            tok = expectToken(lexer, "column type");
+                            final int columnType = ColumnType.typeOf(tok);
+                            if (columnType == -1) {
+                                throw SqlException.$(lexer.lastTokenPosition(), "unrecognized column type: ").put(tok);
+                            }
+                            final int existingType = tableMetadata.getColumnType(columnIndex);
+                            if (existingType != columnType) {
+                                throw SqlException
+                                        .$(lexer.lastTokenPosition(), "column already exists with a different column type [current type=")
+                                        .put(ColumnType.nameOf(existingType))
+                                        .put(", requested type=")
+                                        .put(ColumnType.nameOf(columnType))
+                                        .put(']');
+                            }
                             break;
                         }
                     } else {
@@ -734,6 +749,10 @@ private void alterTableAddColumn(
 
         addColumnSuffix(securityContext, null, tableToken, alterOperationBuilder);
         compiledQuery.ofAlter(alterOperationBuilder.build());
+        if (alterOperationBuilder.getExtraStrInfo().size() == 0) {
+            // there is no column to add, set the done flag to avoid execution of the query
+            compiledQuery.done();
+        }
     }
 
     private void alterTableChangeColumnType(
diff --git a/core/src/test/java/io/questdb/test/cairo/wal/WalTableFailureTest.java b/core/src/test/java/io/questdb/test/cairo/wal/WalTableFailureTest.java
index 6ba6a548416f..192496e3a154 100644
--- a/core/src/test/java/io/questdb/test/cairo/wal/WalTableFailureTest.java
+++ b/core/src/test/java/io/questdb/test/cairo/wal/WalTableFailureTest.java
@@ -1483,9 +1483,10 @@ public void testWalTableMultiColumnAddNotSupported() throws Exception {
                 Assert.fail();
             } catch (CairoException ex) {
                 TestUtils.assertContains(
-                        ex.getFlyweightMessage(),
-                        "statements containing multiple transactions, such as 'alter table add column col1, col2'" +
-                                " are currently not supported for WAL tables"
+                        ex.getFlyweightMessage(), "statement is either no-op," +
+                                " or contains multiple transactions, such as 'alter table add column col1, col2'," +
+                                " and currently not supported for WAL tables" +
+                                " [table=testWalTableMultiColumnAddNotSupported, oldStructureVersion=0, newStructureVersion=2]"
                 );
             }
 
diff --git a/core/src/test/java/io/questdb/test/griffin/AlterTableAddColumnTest.java b/core/src/test/java/io/questdb/test/griffin/AlterTableAddColumnTest.java
index 681d13f893ed..ef8c9f617b3f 100644
--- a/core/src/test/java/io/questdb/test/griffin/AlterTableAddColumnTest.java
+++ b/core/src/test/java/io/questdb/test/griffin/AlterTableAddColumnTest.java
@@ -26,6 +26,7 @@
 
 import io.questdb.cairo.CairoConfiguration;
 import io.questdb.cairo.CairoEngine;
+import io.questdb.cairo.CairoException;
 import io.questdb.cairo.EntryUnavailableException;
 import io.questdb.cairo.SymbolMapReader;
 import io.questdb.cairo.TableReader;
@@ -35,16 +36,35 @@
 import io.questdb.griffin.SqlException;
 import io.questdb.test.AbstractCairoTest;
 import io.questdb.test.cairo.DefaultTestCairoConfiguration;
-import io.questdb.test.tools.TestUtils;
 import org.junit.Assert;
+import org.junit.Assume;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static io.questdb.test.tools.TestUtils.assertContains;
+
+@RunWith(Parameterized.class)
 public class AlterTableAddColumnTest extends AbstractCairoTest {
+    private final boolean isWal;
+
+    public AlterTableAddColumnTest(WalMode walMode) {
+        isWal = (WalMode.WITH_WAL == walMode);
+    }
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][]{
+                {WalMode.WITH_WAL}, {WalMode.NO_WAL}
+        });
+    }
 
     @Test
     public void testAdd2ColumnsWithoutUsingColumnKeywordAndUsingNotNullKeyword() throws Exception {
@@ -52,21 +72,48 @@ public void testAdd2ColumnsWithoutUsingColumnKeywordAndUsingNotNullKeyword() thr
                 () -> {
                     createX();
 
-                    execute("alter table x add mycol int not null, mycol2 int");
+                    try {
+                        execute("alter table x add mycol int not null, mycol2 int");
+                        if (isWal) {
+                            Assert.fail("Exception expected");
+                        }
+                    } catch (CairoException e) {
+                        if (!isWal) {
+                            throw e;
+                        }
+                        assertContains(e.getFlyweightMessage(), "statement is either no-op," +
+                                " or contains multiple transactions, such as 'alter table add column col1, col2'," +
+                                " and currently not supported for WAL tables [table=x, oldStructureVersion=0, newStructureVersion=2]");
+                    }
+
+                    final String originalColumns =
+                            "column\ttype\tindexed\tindexBlockCapacity\tsymbolCached\tsymbolCapacity\tdesignated\tupsertKey\n" +
+                                    "i\tINT\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "sym\tSYMBOL\tfalse\t0\ttrue\t128\tfalse\tfalse\n" +
+                                    "amt\tDOUBLE\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "timestamp\tTIMESTAMP\tfalse\t0\tfalse\t0\ttrue\tfalse\n" +
+                                    "b\tBOOLEAN\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "c\tSTRING\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "d\tDOUBLE\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "e\tFLOAT\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "f\tSHORT\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "g\tDATE\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "ik\tSYMBOL\tfalse\t0\ttrue\t128\tfalse\tfalse\n" +
+                                    "j\tLONG\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "k\tTIMESTAMP\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "l\tBYTE\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "m\tBINARY\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "n\tSTRING\tfalse\t0\tfalse\t0\tfalse\tfalse\n";
 
                     assertQueryNoLeakCheck(
-                            "c\tmycol\tmycol2\n" +
-                                    "XYZ\tnull\tnull\n" +
-                                    "ABC\tnull\tnull\n" +
-                                    "ABC\tnull\tnull\n" +
-                                    "XYZ\tnull\tnull\n" +
-                                    "\tnull\tnull\n" +
-                                    "CDE\tnull\tnull\n" +
-                                    "CDE\tnull\tnull\n" +
-                                    "ABC\tnull\tnull\n" +
-                                    "\tnull\tnull\n" +
-                                    "XYZ\tnull\tnull\n",
-                            "select c, mycol, mycol2 from x"
+                            isWal
+                                    ? originalColumns
+                                    : originalColumns +
+                                    "mycol\tINT\tfalse\t256\tfalse\t0\tfalse\tfalse\n" +
+                                    "mycol2\tINT\tfalse\t256\tfalse\t0\tfalse\tfalse\n",
+                            "show columns from x",
+                            null,
+                            false
                     );
                 }
         );
@@ -94,14 +141,15 @@ public void testAddBadSyntax() throws Exception {
 
     @Test
     public void testAddBusyTable() throws Exception {
+        Assume.assumeFalse(isWal);
+
         assertMemoryLeak(() -> {
             CountDownLatch allHaltLatch = new CountDownLatch(1);
             try {
                 createX();
                 AtomicInteger errorCounter = new AtomicInteger();
 
-                // start a thread that would lock table we
-                // about to alter
+                // start a thread that would lock table we about to alter
                 CyclicBarrier startBarrier = new CyclicBarrier(2);
                 CountDownLatch haltLatch = new CountDownLatch(1);
                 new Thread(() -> {
@@ -127,7 +175,7 @@ public void testAddBusyTable() throws Exception {
                     haltLatch.countDown();
                 }
             } catch (EntryUnavailableException e) {
-                TestUtils.assertContains(e.getFlyweightMessage(), "table busy");
+                assertContains(e.getFlyweightMessage(), "table busy");
             }
 
             Assert.assertTrue(allHaltLatch.await(2, TimeUnit.SECONDS));
@@ -141,6 +189,7 @@ public void testAddColumn() throws Exception {
                     createX();
 
                     execute("alter table x add column mycol int");
+                    drainWalQueue();
 
                     assertQueryNoLeakCheck(
                             "c\tmycol\n" +
@@ -189,7 +238,7 @@ public void testAddColumnUpperCase() throws Exception {
                         execute("alter table x add column D int", sqlExecutionContext);
                         Assert.fail();
                     } catch (SqlException e) {
-                        TestUtils.assertContains(e.getFlyweightMessage(), "column 'D' already exists");
+                        assertContains(e.getFlyweightMessage(), "column 'D' already exists");
                     }
                 }
         );
@@ -202,6 +251,7 @@ public void testAddColumnWithQuotedColumnName() throws Exception {
                     createX();
 
                     execute("alter table x add \"mycol\" int not null");
+                    drainWalQueue();
 
                     assertQueryNoLeakCheck(
                             "c\tmycol\n" +
@@ -228,6 +278,7 @@ public void testAddColumnWithQuotedColumnNameV2() throws Exception {
                     createX();
 
                     execute("alter table x add column \"mycol\" int not null");
+                    drainWalQueue();
 
                     assertQueryNoLeakCheck(
                             "c\tmycol\n" +
@@ -254,6 +305,7 @@ public void testAddColumnWithSpaceInName() throws Exception {
                     createX();
 
                     execute("alter table x add \"spa ce\" string");
+                    drainWalQueue();
 
                     assertQueryNoLeakCheck(
                             "c\tspa ce\n" +
@@ -280,6 +332,7 @@ public void testAddColumnWithoutUsingColumnKeyword() throws Exception {
                     createX();
 
                     execute("alter table x add mycol int");
+                    drainWalQueue();
 
                     assertQueryNoLeakCheck(
                             "c\tmycol\n" +
@@ -306,6 +359,7 @@ public void testAddColumnWithoutUsingColumnKeywordAndUsingNotNullKeyword() throw
                     createX();
 
                     execute("alter table x add mycol int not null");
+                    drainWalQueue();
 
                     assertQueryNoLeakCheck(
                             "c\tmycol\n" +
@@ -332,6 +386,7 @@ public void testAddColumnWithoutUsingColumnKeywordAndUsingNullKeyword() throws E
                     createX();
 
                     execute("alter table x add mycol int null");
+                    drainWalQueue();
 
                     assertQueryNoLeakCheck(
                             "c\tmycol\n" +
@@ -361,6 +416,9 @@ public void testAddDuplicateColumnIfNotExists() throws Exception {
         createX();
         execute("alter table x add column a int");
         execute("alter table x add column if not exists a int");
+
+        assertException("alter table x add column if not exists a hohoho", 41, "unrecognized column type: hohoho");
+        assertException("alter table x add column if not exists a long", 41, "column already exists with a different column type [current type=INT, requested type=LONG]");
     }
 
     @Test
@@ -401,6 +459,7 @@ public boolean getDefaultSymbolCacheFlag() {
                     try (CairoEngine engine = new CairoEngine(configuration)) {
                         try (SqlCompiler compiler = engine.getSqlCompiler()) {
                             execute(compiler, "alter table x add column meh symbol cache");
+                            drainWalQueue(engine);
 
                             try (TableReader reader = getReader("x")) {
                                 SymbolMapReader smr = reader.getSymbolMapReader(16);
@@ -425,6 +484,7 @@ public void testAddSymbolCapacity() throws Exception {
                     createX();
 
                     execute("alter table x add column meh symbol capacity 2048");
+                    drainWalQueue();
 
                     try (TableReader reader = getReader("x")) {
                         SymbolMapReader smr = reader.getSymbolMapReader(16);
@@ -481,6 +541,7 @@ public void testAddSymbolIndex() throws Exception {
                     createX();
 
                     execute("alter table x add column meh symbol index");
+                    drainWalQueue();
 
                     try (TableReader reader = getReader("x")) {
                         SymbolMapReader smr = reader.getSymbolMapReader(16);
@@ -501,6 +562,7 @@ public void testAddSymbolIndexCapacity() throws Exception {
                     createX();
 
                     execute("alter table x add column meh symbol index capacity 9000");
+                    drainWalQueue();
 
                     try (TableReader reader = getReader("x")) {
                         SymbolMapReader smr = reader.getSymbolMapReader(16);
@@ -527,6 +589,7 @@ public void testAddSymbolNoCache() throws Exception {
                     createX();
 
                     execute("alter table x add column meh symbol nocache");
+                    drainWalQueue();
 
                     try (TableReader reader = getReader("x")) {
                         SymbolMapReader smr = reader.getSymbolMapReader(16);
@@ -548,6 +611,7 @@ public void testAddSymbolWithStatementEndingWithSemicolon_DoesntThrowException()
                     engine.clear();
 
                     execute("alter table x add column meh symbol;");
+                    drainWalQueue();
 
                     try (TableReader reader = getReader("x")) {
                         SymbolMapReader smr = reader.getSymbolMapReader(16);
@@ -578,6 +642,8 @@ public boolean getDefaultSymbolCacheFlag() {
                     try (CairoEngine engine = new CairoEngine(configuration)) {
                         try (SqlCompiler compiler = engine.getSqlCompiler()) {
                             execute(compiler, "alter table x add column meh symbol", sqlExecutionContext);
+                            drainWalQueue(engine);
+
                             try (TableReader reader = getReader("x")) {
                                 SymbolMapReader smr = reader.getSymbolMapReader(16);
                                 Assert.assertNotNull(smr);
@@ -604,6 +670,7 @@ public void testAddSymbolWithoutSpecifyingCapacityOrCacheWhenDefaultSymbolCacheC
                     engine.clear();
 
                     execute("alter table x add column meh symbol");
+                    drainWalQueue();
 
                     try (TableReader reader = getReader("x")) {
                         SymbolMapReader smr = reader.getSymbolMapReader(16);
@@ -628,20 +695,48 @@ public void testAddTwoColumns() throws Exception {
                 () -> {
                     createX();
 
-                    execute("alter table x add column mycol int, second symbol");
+                    try {
+                        execute("alter table x add column mycol int, second symbol");
+                        if (isWal) {
+                            Assert.fail("Exception expected");
+                        }
+                    } catch (CairoException e) {
+                        if (!isWal) {
+                            throw e;
+                        }
+                        assertContains(e.getFlyweightMessage(), "statement is either no-op," +
+                                " or contains multiple transactions, such as 'alter table add column col1, col2'," +
+                                " and currently not supported for WAL tables [table=x, oldStructureVersion=0, newStructureVersion=2]");
+                    }
+
+                    final String originalColumns =
+                            "column\ttype\tindexed\tindexBlockCapacity\tsymbolCached\tsymbolCapacity\tdesignated\tupsertKey\n" +
+                                    "i\tINT\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "sym\tSYMBOL\tfalse\t0\ttrue\t128\tfalse\tfalse\n" +
+                                    "amt\tDOUBLE\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "timestamp\tTIMESTAMP\tfalse\t0\tfalse\t0\ttrue\tfalse\n" +
+                                    "b\tBOOLEAN\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "c\tSTRING\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "d\tDOUBLE\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "e\tFLOAT\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "f\tSHORT\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "g\tDATE\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "ik\tSYMBOL\tfalse\t0\ttrue\t128\tfalse\tfalse\n" +
+                                    "j\tLONG\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "k\tTIMESTAMP\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "l\tBYTE\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "m\tBINARY\tfalse\t0\tfalse\t0\tfalse\tfalse\n" +
+                                    "n\tSTRING\tfalse\t0\tfalse\t0\tfalse\tfalse\n";
+
                     assertQueryNoLeakCheck(
-                            "c\tmycol\tsecond\n" +
-                                    "XYZ\tnull\t\n" +
-                                    "ABC\tnull\t\n" +
-                                    "ABC\tnull\t\n" +
-                                    "XYZ\tnull\t\n" +
-                                    "\tnull\t\n" +
-                                    "CDE\tnull\t\n" +
-                                    "CDE\tnull\t\n" +
-                                    "ABC\tnull\t\n" +
-                                    "\tnull\t\n" +
-                                    "XYZ\tnull\t\n",
-                            "select c, mycol, second from x"
+                            isWal
+                                    ? originalColumns
+                                    : originalColumns +
+                                    "mycol\tINT\tfalse\t256\tfalse\t0\tfalse\tfalse\n" +
+                                    "second\tSYMBOL\tfalse\t256\ttrue\t128\tfalse\tfalse\n",
+                            "show columns from x",
+                            null,
+                            false
                     );
                 }
         );
@@ -660,6 +755,8 @@ public void testAddTwoColumnsWithSemicolonAtTheEnd() throws Exception {
 
                     execute("alter table x add column mycol int; \n");
                     execute("alter table x add column second symbol;");
+                    drainWalQueue();
+
                     assertQueryNoLeakCheck(
                             "c\tmycol\tsecond\n" +
                                     "XYZ\tnull\t\n" +
@@ -743,7 +840,10 @@ private void createX() throws SqlException {
                         " rnd_bin(10, 20, 2) m," +
                         " rnd_str(5,16,2) n" +
                         " from long_sequence(10)" +
-                        ") timestamp (timestamp);"
+                        ") timestamp (timestamp)" +
+                        " partition by day" +
+                        (isWal ? " wal" : "") +
+                        ";"
         );
     }
 }


Reviews:
user: login: bluestreak01
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T01:35:11Z
user: login: glasstiger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T09:35:55Z
user: login: bluestreak01
body: I get bizarre error on this branch:

![image](https://github.com/user-attachments/assets/cff66eac-1318-44ad-b519-fd0030dbe94b)

state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-16T15:38:31Z
user: login: bluestreak01
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T18:24:39Z

Review comments:
diff_hunk: @@ -94,14 +141,15 @@ public void testAddBadSyntax() throws Exception {
 
     @Test
     public void testAddBusyTable() throws Exception {
+        Assume.assumeFalse(isWal);
path: core/src/test/java/io/questdb/test/griffin/AlterTableAddColumnTest.java
user: login: bluestreak01
body: that looks like a lot of effort draining queues just to disable WAL runs like that
created_at: 2025-05-15T01:35:11Z
author_association: MEMBER
diff_hunk: @@ -94,14 +141,15 @@ public void testAddBadSyntax() throws Exception {
 
     @Test
     public void testAddBusyTable() throws Exception {
+        Assume.assumeFalse(isWal);
path: core/src/test/java/io/questdb/test/griffin/AlterTableAddColumnTest.java
user: login: glasstiger
body: This line disables only a single test for WAL: `testAddBusyTable()`
The test asserts that `EntryUnavailableException` is thrown when trying to add a column to the table while the writer is locked.
I guess the point of the test is to make sure that `EntryUnavailableException` is thrown, and not something else, because that will put the command on the async queue, instead of rejecting it outright.
This test does not make sense for WAL table.
created_at: 2025-05-15T09:35:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: bluestreak01
created_at: 2025-05-16T15:40:00Z
body: what should happen is:

- if table column exists and it is the same type, SQL must return successfully
- it table column exists and NOT the desired type - SQL must error out
author_association: MEMBER
user: login: glasstiger
created_at: 2025-05-16T18:03:44Z
body: > what should happen is:
> 
> * if table column exists and it is the same type, SQL must return successfully
> * it table column exists and NOT the desired type - SQL must error out

first one is fixed in https://github.com/questdb/questdb/pull/5675/commits/4f402b587686111587989cfc018c490b1a6f3452,
second is fixed in https://github.com/questdb/questdb/pull/5675/commits/45eb980e28717585aee2eb0664687995e368cac7
author_association: CONTRIBUTOR
user: login: glasstiger
created_at: 2025-05-16T18:36:14Z
body: #### [PR Coverage check]

:heart_eyes: **pass** : 24 / 24 (100.00%)




#### file detail

|   |path|covered line|new line|coverage|
|----|----|----|----|----|
|:large_blue_circle:|io/questdb/cairo/wal/WalWriter.java|3|3|100.00%|
|:large_blue_circle:|io/questdb/griffin/CompiledQueryImpl.java|5|5|100.00%|
|:large_blue_circle:|io/questdb/griffin/SqlCompilerImpl.java|16|16|100.00%|



author_association: CONTRIBUTOR