diff --git a/src/formatter/phrases.ts b/src/formatter/phrases.ts index 92276e4..56db73a 100644 --- a/src/formatter/phrases.ts +++ b/src/formatter/phrases.ts @@ -103,6 +103,8 @@ const alterActionPhrases: Phrase[] = action( ["Drop", "Partition", "List"], ["Set"], ["Squash"], + ["Force", "Drop", "Partition", "List"], + ["Force", "Reindex", "Parquet"], ["Dedup"], ["Resume", "Wal"], ["Suspend", "Wal"], diff --git a/src/parser/ast.ts b/src/parser/ast.ts index 71bd539..e131baf 100644 --- a/src/parser/ast.ts +++ b/src/parser/ast.ts @@ -554,6 +554,8 @@ export type AlterTableAction = | AttachPartitionAction | DetachPartitionAction | SquashPartitionsAction + | ForceDropPartitionAction + | ForceReindexParquetAction | SetParamAction | SetTtlAction | SetTableFormatAction @@ -628,6 +630,17 @@ export interface SquashPartitionsAction { actionType: "squashPartitions" } +export interface ForceDropPartitionAction { + actionType: "forceDropPartition" + partitions: string[] +} + +export interface ForceReindexParquetAction { + actionType: "forceReindexParquet" + column?: string + partitions?: string[] +} + export interface SetParamAction { actionType: "setParam" params: TableParam[] diff --git a/src/parser/cst-types.d.ts b/src/parser/cst-types.d.ts index fdb40df..7f18956 100644 --- a/src/parser/cst-types.d.ts +++ b/src/parser/cst-types.d.ts @@ -1538,6 +1538,7 @@ export type AlterTableActionCstChildren = { Detach?: IToken[]; Squash?: IToken[]; Partitions?: IToken[]; + alterTableForceAction?: AlterTableForceActionCstNode[]; Param?: IToken[]; tableParam?: (TableParamCstNode)[]; Ttl?: IToken[]; @@ -1566,6 +1567,24 @@ export type AlterTableActionCstChildren = { convertPartitionTarget?: ConvertPartitionTargetCstNode[]; }; +export interface AlterTableForceActionCstNode extends CstNode { + name: "alterTableForceAction"; + children: AlterTableForceActionCstChildren; +} + +export type AlterTableForceActionCstChildren = { + Force: IToken[]; + Drop?: IToken[]; + Partition?: (IToken)[]; + List?: (IToken)[]; + StringLiteral?: (IToken)[]; + Comma?: (IToken)[]; + Reindex?: IToken[]; + Parquet?: IToken[]; + Column?: IToken[]; + identifier?: IdentifierCstNode[]; +}; + export interface ConvertPartitionTargetCstNode extends CstNode { name: "convertPartitionTarget"; children: ConvertPartitionTargetCstChildren; @@ -3051,6 +3070,7 @@ export interface ICstNodeVisitor extends ICstVisitor { memoryLimit(children: MemoryLimitCstChildren, param?: IN): OUT; alterTableStatement(children: AlterTableStatementCstChildren, param?: IN): OUT; alterTableAction(children: AlterTableActionCstChildren, param?: IN): OUT; + alterTableForceAction(children: AlterTableForceActionCstChildren, param?: IN): OUT; convertPartitionTarget(children: ConvertPartitionTargetCstChildren, param?: IN): OUT; alterMaterializedViewStatement(children: AlterMaterializedViewStatementCstChildren, param?: IN): OUT; alterMaterializedViewAction(children: AlterMaterializedViewActionCstChildren, param?: IN): OUT; diff --git a/src/parser/parser.ts b/src/parser/parser.ts index d6b90da..65d9557 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -2926,6 +2926,8 @@ class QuestDBParser extends CstParser { this.CONSUME(Partitions) }, }, + // FORCE (DROP PARTITION LIST ... | REINDEX PARQUET ...) + { ALT: () => this.SUBRULE(this.alterTableForceAction) }, // SET (PARAM ... | TTL ... | TYPE WAL ... | STORAGE POLICY ...) { ALT: () => { @@ -3066,6 +3068,45 @@ class QuestDBParser extends CstParser { ]) }) + // FORCE DROP PARTITION LIST '

' [, ...] + // FORCE REINDEX PARQUET [COLUMN ] [PARTITION LIST '

' [, ...]] + private alterTableForceAction = this.RULE("alterTableForceAction", () => { + this.CONSUME(Force) + this.OR([ + { + ALT: () => { + this.CONSUME(Drop) + this.CONSUME(Partition) + this.CONSUME(List) + this.CONSUME(StringLiteral) + this.MANY(() => { + this.CONSUME(Comma) + this.CONSUME1(StringLiteral) + }) + }, + }, + { + ALT: () => { + this.CONSUME(Reindex) + this.CONSUME(Parquet) + this.OPTION(() => { + this.CONSUME(Column) + this.SUBRULE(this.identifier) + }) + this.OPTION1(() => { + this.CONSUME1(Partition) + this.CONSUME1(List) + this.CONSUME2(StringLiteral) + this.MANY1(() => { + this.CONSUME1(Comma) + this.CONSUME3(StringLiteral) + }) + }) + }, + }, + ]) + }) + private convertPartitionTarget = this.RULE("convertPartitionTarget", () => { // Optional LIST before TO this.OPTION(() => { diff --git a/src/parser/toSql.ts b/src/parser/toSql.ts index 53af9c3..83cc8ed 100644 --- a/src/parser/toSql.ts +++ b/src/parser/toSql.ts @@ -964,6 +964,21 @@ function alterTableToSql(stmt: AST.AlterTableStatement): string { case "squashPartitions": parts.push("SQUASH PARTITIONS") break + case "forceDropPartition": + parts.push("FORCE DROP PARTITION LIST") + parts.push(action.partitions.map((p) => escapeString(p)).join(", ")) + break + case "forceReindexParquet": + parts.push("FORCE REINDEX PARQUET") + if (action.column) { + parts.push("COLUMN") + parts.push(escapeIdentifier(action.column)) + } + if (action.partitions && action.partitions.length > 0) { + parts.push("PARTITION LIST") + parts.push(action.partitions.map((p) => escapeString(p)).join(", ")) + } + break case "setParam": parts.push("SET PARAM") parts.push( diff --git a/src/parser/visitor.ts b/src/parser/visitor.ts index 00bba98..2fa69e4 100644 --- a/src/parser/visitor.ts +++ b/src/parser/visitor.ts @@ -19,6 +19,7 @@ import type { AlterServiceAccountStatementCstChildren, AlterStatementCstChildren, AlterTableActionCstChildren, + AlterTableForceActionCstChildren, AlterTableStatementCstChildren, AlterUserActionCstChildren, AlterUserStatementCstChildren, @@ -2374,7 +2375,32 @@ class QuestDBVisitor extends BaseVisitor { return ctx.NumberLiteral![0].image + (ctx.Identifier?.[0]?.image ?? "") } + alterTableForceAction( + ctx: AlterTableForceActionCstChildren, + ): AST.ForceDropPartitionAction | AST.ForceReindexParquetAction { + const partitions = (ctx.StringLiteral ?? []).map((s: IToken) => + s.image.slice(1, -1), + ) + if (ctx.Drop) { + return { actionType: "forceDropPartition", partitions } + } + const result: AST.ForceReindexParquetAction = { + actionType: "forceReindexParquet", + } + if (ctx.identifier) { + result.column = this.extractIdentifierName(ctx.identifier[0].children) + } + if (partitions.length > 0) { + result.partitions = partitions + } + return result + } + alterTableAction(ctx: AlterTableActionCstChildren): AST.AlterTableAction { + // FORCE DROP PARTITION / FORCE REINDEX PARQUET + if (ctx.alterTableForceAction) { + return this.visit(ctx.alterTableForceAction) as AST.AlterTableAction + } // SET STORAGE POLICY(...) — must come before generic SET branches below if (ctx.Set && ctx.storagePolicy) { return { diff --git a/tests/fixtures/docs-queries.json b/tests/fixtures/docs-queries.json index f59df5d..d44ce80 100644 --- a/tests/fixtures/docs-queries.json +++ b/tests/fixtures/docs-queries.json @@ -7978,5 +7978,14 @@ }, { "query": "SELECT avg(b.bid) AS avg_bid, avg(a.ask) AS avg_ask FROM trades AS t HORIZON JOIN bids AS b ON (t.sym = b.sym) HORIZON JOIN asks AS a ON (t.sym = a.sym) RANGE FROM -1s TO 1s STEP 1s AS h" + }, + { + "query": "ALTER TABLE trades FORCE REINDEX PARQUET" + }, + { + "query": "ALTER TABLE trades FORCE REINDEX PARQUET COLUMN symbol" + }, + { + "query": "ALTER TABLE trades FORCE REINDEX PARQUET COLUMN symbol PARTITION LIST '2026-03-01', '2026-03-02'" } ] diff --git a/tests/formatter/fixtures.ts b/tests/formatter/fixtures.ts index cc47e98..7892615 100644 --- a/tests/formatter/fixtures.ts +++ b/tests/formatter/fixtures.ts @@ -75,6 +75,23 @@ export const fixtures: Fixture[] = [ input: "ALTER TABLE t SUSPEND WAL", expected: ["ALTER TABLE t", "SUSPEND WAL"].join("\n"), }, + { + name: "ALTER TABLE FORCE REINDEX PARQUET starts a line", + input: + "ALTER TABLE trades FORCE REINDEX PARQUET COLUMN symbol PARTITION LIST '2026-03-01'", + expected: [ + "ALTER TABLE trades", + "FORCE REINDEX PARQUET COLUMN symbol PARTITION LIST '2026-03-01'", + ].join("\n"), + }, + { + name: "ALTER TABLE FORCE DROP PARTITION starts a line", + input: "ALTER TABLE trades FORCE DROP PARTITION LIST '2026-03-01'", + expected: [ + "ALTER TABLE trades", + "FORCE DROP PARTITION LIST '2026-03-01'", + ].join("\n"), + }, { name: "ALTER TABLE DROP STORAGE POLICY starts a line", input: "ALTER TABLE t DROP STORAGE POLICY", diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 1f37533..300c076 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -7973,6 +7973,60 @@ orders PIVOT (sum(amount) FOR status IN ('open'))` }) }) + describe("FORCE REINDEX PARQUET / FORCE DROP PARTITION — parse & AST", () => { + it("bare FORCE REINDEX PARQUET", () => { + const r = parseToAst("ALTER TABLE t FORCE REINDEX PARQUET") + expect(r.errors).toHaveLength(0) + const s = r.ast[0] as AST.AlterTableStatement + expect(s.action).toEqual({ actionType: "forceReindexParquet" }) + }) + it("FORCE REINDEX PARQUET with COLUMN and PARTITION LIST", () => { + const r = parseToAst( + "ALTER TABLE t FORCE REINDEX PARQUET COLUMN b PARTITION LIST '2024-01-01', '2024-01-02'", + ) + expect(r.errors).toHaveLength(0) + const s = r.ast[0] as AST.AlterTableStatement + expect(s.action).toEqual({ + actionType: "forceReindexParquet", + column: "b", + partitions: ["2024-01-01", "2024-01-02"], + }) + }) + it("FORCE REINDEX PARQUET with PARTITION LIST only", () => { + const r = parseToAst( + "ALTER TABLE t FORCE REINDEX PARQUET PARTITION LIST '2024-01-01'", + ) + expect(r.errors).toHaveLength(0) + const s = r.ast[0] as AST.AlterTableStatement + expect(s.action).toEqual({ + actionType: "forceReindexParquet", + partitions: ["2024-01-01"], + }) + }) + it("FORCE DROP PARTITION LIST", () => { + const r = parseToAst( + "ALTER TABLE t FORCE DROP PARTITION LIST '2024-01-01', '2024-01-02'", + ) + expect(r.errors).toHaveLength(0) + const s = r.ast[0] as AST.AlterTableStatement + expect(s.action).toEqual({ + actionType: "forceDropPartition", + partitions: ["2024-01-01", "2024-01-02"], + }) + }) + it("rejects FORCE REINDEX without PARQUET", () => { + expect( + parseToAst("ALTER TABLE t FORCE REINDEX COLUMN b").errors.length, + ).toBeGreaterThan(0) + }) + it("rejects FORCE REINDEX PARQUET PARTITION without LIST", () => { + expect( + parseToAst("ALTER TABLE t FORCE REINDEX PARQUET PARTITION '2024-01-01'") + .errors.length, + ).toBeGreaterThan(0) + }) + }) + describe("SHOW CREATE DATABASE — parse & AST", () => { it("bare", () => { const s = parseToAst("SHOW CREATE DATABASE").ast[0] as AST.ShowStatement @@ -8302,6 +8356,25 @@ orders PIVOT (sum(amount) FOR status IN ('open'))` } }) + describe("FORCE REINDEX PARQUET (QuestDB #8139) — round-trip", () => { + const queries = [ + "alter table t force reindex parquet", + "alter table t force reindex parquet column b", + "alter table t force reindex parquet partition list '2024-01-01', '2024-01-02'", + "alter table t force reindex parquet column b partition list '2024-01-02'", + "alter table t force drop partition list '2024-01-01', '2024-01-02'", + ] + for (const query of queries) { + it(`round-trips: ${query.slice(0, 64)}`, () => { + const r = parseToAst(query) + expect(r.errors, JSON.stringify(r.errors)).toHaveLength(0) + const r2 = parseToAst(toSql(r.ast[0])) + expect(r2.errors).toHaveLength(0) + expect(r2.ast[0]).toEqual(r.ast[0]) + }) + } + }) + describe("SHOW CREATE DATABASE (QuestDB #7232) — round-trip", () => { const queries = [ "SHOW CREATE DATABASE",