Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/grammar/keywords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const keywords: string[] = [
"cast",
"checkpoint",
"cleanup",
"cold",
"column",
"columns",
"compile",
Expand Down Expand Up @@ -75,6 +76,7 @@ export const keywords: string[] = [
"flush",
"following",
"for",
"force",
"foreign",
"format",
"from",
Expand Down Expand Up @@ -112,6 +114,7 @@ export const keywords: string[] = [
"live",
"lock",
"lt",
"manager",
"maps",
"materialized",
"maxUncommittedRows",
Expand Down Expand Up @@ -155,6 +158,7 @@ export const keywords: string[] = [
"rebase",
"references",
"refresh",
"refresher",
"release",
"reindex",
"remove",
Expand Down
7 changes: 4 additions & 3 deletions src/parser/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,11 +958,12 @@ export interface BackupStatement extends AstNode {
table?: QualifiedName
}

// Enterprise: SWITCH ROLE TO {PRIMARY|REPLICA} [TIMEOUT <ms>] | SWITCH STATUS
// Enterprise role and cold-storage role switching.
export interface SwitchStatement extends AstNode {
type: "switch"
action: "role" | "status"
role?: "PRIMARY" | "REPLICA"
action: "role" | "status" | "coldStorageRole" | "coldStorageStatus"
role?: "PRIMARY" | "REPLICA" | "MANAGER" | "REFRESHER"
force?: boolean
timeout?: number
}

Expand Down
15 changes: 10 additions & 5 deletions src/parser/cst-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1997,13 +1997,18 @@ export interface SwitchStatementCstNode extends CstNode {

export type SwitchStatementCstChildren = {
Switch: IToken[];
Role?: IToken[];
To?: IToken[];
Role?: (IToken)[];
To?: (IToken)[];
Primary?: IToken[];
Replica?: IToken[];
Timeout?: IToken[];
NumberLiteral?: IToken[];
Status?: IToken[];
Timeout?: (IToken)[];
NumberLiteral?: (IToken)[];
Status?: (IToken)[];
Cold?: IToken[];
Storage?: IToken[];
Manager?: IToken[];
Force?: IToken[];
Refresher?: IToken[];
};

export interface CompileViewStatementCstNode extends CstNode {
Expand Down
8 changes: 8 additions & 0 deletions src/parser/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ import {
Timeout,
Expire,
Cleanup,
Cold,
Force,
Manager,
Refresher,
Highest,
Lowest,
Live,
Expand Down Expand Up @@ -638,6 +642,10 @@ export {
Timeout,
Expire,
Cleanup,
Cold,
Force,
Manager,
Refresher,
Highest,
Lowest,
Live,
Expand Down
45 changes: 44 additions & 1 deletion src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ import {
Timeout,
Expire,
Cleanup,
Cold,
Force,
Manager,
Refresher,
Highest,
Lowest,
Live,
Expand Down Expand Up @@ -3754,7 +3758,10 @@ class QuestDBParser extends CstParser {
])
})

// SWITCH ROLE TO { PRIMARY | REPLICA } [ TIMEOUT <ms> ] | SWITCH STATUS
// SWITCH ROLE TO { PRIMARY | REPLICA } [ TIMEOUT <ms> ]
// SWITCH STATUS
// SWITCH COLD STORAGE ROLE TO { MANAGER [ FORCE ] | REFRESHER } [ TIMEOUT <ms> ]
// SWITCH COLD STORAGE STATUS
private switchStatement = this.RULE("switchStatement", () => {
this.CONSUME(Switch)
this.OR([
Expand All @@ -3773,6 +3780,42 @@ class QuestDBParser extends CstParser {
},
},
{ ALT: () => this.CONSUME(Status) },
{
ALT: () => {
this.CONSUME(Cold)
this.CONSUME(Storage)
this.OR2([
{ ALT: () => this.CONSUME1(Status) },
{
ALT: () => {
this.CONSUME1(Role)
this.CONSUME1(To)
this.OR3([
{
ALT: () => {
this.CONSUME(Manager)
this.OPTION1(() => this.CONSUME(Force))
this.OPTION2(() => {
this.CONSUME1(Timeout)
this.CONSUME1(NumberLiteral)
})
},
},
{
ALT: () => {
this.CONSUME(Refresher)
this.OPTION3(() => {
this.CONSUME2(Timeout)
this.CONSUME2(NumberLiteral)
})
},
},
])
},
},
])
},
},
])
})

Expand Down
7 changes: 6 additions & 1 deletion src/parser/toSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,7 +1439,12 @@ function backupToSql(stmt: AST.BackupStatement): string {

function switchToSql(stmt: AST.SwitchStatement): string {
if (stmt.action === "status") return "SWITCH STATUS"
let s = `SWITCH ROLE TO ${stmt.role}`
if (stmt.action === "coldStorageStatus") return "SWITCH COLD STORAGE STATUS"
const coldStorage = stmt.action === "coldStorageRole"
let s = coldStorage
? `SWITCH COLD STORAGE ROLE TO ${stmt.role}`
: `SWITCH ROLE TO ${stmt.role}`
if (coldStorage && stmt.force) s += " FORCE"
if (stmt.timeout !== undefined) s += ` TIMEOUT ${stmt.timeout}`
return s
}
Expand Down
8 changes: 8 additions & 0 deletions src/parser/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ export const IDENTIFIER_KEYWORD_NAMES = new globalThis.Set([
"Only",
"Align",
"Latest",
"Cold",
"Force",
"Manager",
"Refresher",
// New constants that can be used as identifiers
"Ilp",
"Native",
Expand Down Expand Up @@ -765,6 +769,10 @@ export const Replica = getToken("Replica")
export const Timeout = getToken("Timeout")
export const Expire = getToken("Expire")
export const Cleanup = getToken("Cleanup")
export const Cold = getToken("Cold")
export const Force = getToken("Force")
export const Manager = getToken("Manager")
export const Refresher = getToken("Refresher")
export const Highest = getToken("Highest")
export const Lowest = getToken("Lowest")
export const Live = getToken("Live")
Expand Down
17 changes: 17 additions & 0 deletions src/parser/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3187,6 +3187,23 @@ class QuestDBVisitor extends BaseVisitor {
}

switchStatement(ctx: SwitchStatementCstChildren): AST.SwitchStatement {
if (ctx.Cold) {
if (ctx.Status) {
return { type: "switch", action: "coldStorageStatus" }
}
const result: AST.SwitchStatement = {
type: "switch",
action: "coldStorageRole",
role: ctx.Refresher ? "REFRESHER" : "MANAGER",
}
if (ctx.Force) {
result.force = true
}
if (ctx.Timeout && ctx.NumberLiteral) {
result.timeout = tokenInt(ctx.NumberLiteral[0].image)
}
return result
}
if (ctx.Status) {
return { type: "switch", action: "status" }
}
Expand Down
29 changes: 29 additions & 0 deletions tests/autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,35 @@ describe("createAutocompleteProvider", () => {
const labels = getLabelsAt(provider, "SELECT * FROM trades ORDER ")
expect(labels).toContain("BY")
})

it("walks through SWITCH COLD STORAGE role and status commands", () => {
assertSuggestionsWalkthrough(provider, [
{ typed: "SWITCH ", expects: ["COLD", "ROLE", "STATUS"] },
{ typed: "SWITCH COLD ", expects: ["STORAGE"] },
{
typed: "SWITCH COLD STORAGE ",
expects: ["ROLE", "STATUS"],
},
{ typed: "SWITCH COLD STORAGE ROLE ", expects: ["TO"] },
{
typed: "SWITCH COLD STORAGE ROLE TO ",
expects: ["MANAGER", "REFRESHER"],
},
{
typed: "SWITCH COLD STORAGE ROLE TO MANAGER ",
expects: ["FORCE", "TIMEOUT"],
},
{
typed: "SWITCH COLD STORAGE ROLE TO MANAGER FORCE ",
expects: ["TIMEOUT"],
},
{
typed: "SWITCH COLD STORAGE ROLE TO REFRESHER ",
expects: ["TIMEOUT"],
rejects: ["FORCE"],
},
])
})
})

describe("column suggestions", () => {
Expand Down
19 changes: 19 additions & 0 deletions tests/lexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ describe("QuestDB Lexer", () => {
expect(result3.tokens[0].tokenType.name).toBe("Select")
})

it("should tokenize cold storage switch keywords", () => {
const result = tokenize(
"SWITCH COLD STORAGE ROLE TO MANAGER FORCE TIMEOUT 10000",
)

expect(result.errors).toHaveLength(0)
expect(result.tokens.map((token) => token.tokenType.name)).toEqual([
"Switch",
"Cold",
"Storage",
"Role",
"To",
"Manager",
"Force",
"Timeout",
"NumberLiteral",
])
})

it("should skip whitespace and comments", () => {
const result = tokenize(`
-- This is a comment
Expand Down
74 changes: 74 additions & 0 deletions tests/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8064,6 +8064,57 @@ orders PIVOT (sum(amount) FOR status IN ('open'))`
})
})

describe("SWITCH COLD STORAGE — parse & AST", () => {
it("parses a forced manager switch with a timeout", () => {
const result = parseToAst(
"SWITCH COLD STORAGE ROLE TO MANAGER FORCE TIMEOUT 10000",
)
expect(result.errors).toHaveLength(0)
expect(result.ast).toHaveLength(1)

const statement = result.ast[0] as AST.SwitchStatement
expect(statement.type).toBe("switch")
expect(statement.action).toBe("coldStorageRole")
expect(statement.role).toBe("MANAGER")
expect(statement.force).toBe(true)
expect(statement.timeout).toBe(10000)
})

it("parses a refresher switch without FORCE", () => {
const statement = parseToAst(
"SWITCH COLD STORAGE ROLE TO REFRESHER TIMEOUT 30000",
).ast[0] as AST.SwitchStatement

expect(statement.action).toBe("coldStorageRole")
expect(statement.role).toBe("REFRESHER")
expect(statement.force).toBeUndefined()
expect(statement.timeout).toBe(30000)
})

it("parses cold storage status", () => {
const statement = parseToAst("SWITCH COLD STORAGE STATUS")
.ast[0] as AST.SwitchStatement

expect(statement.action).toBe("coldStorageStatus")
expect(statement.role).toBeUndefined()
})

it("is case-insensitive", () => {
expect(
parseToAst("sWiTcH cOlD sToRaGe rOlE tO mAnAgEr fOrCe tImEoUt 10000")
.errors,
).toHaveLength(0)
})

it("keeps the new non-reserved keywords usable as identifiers", () => {
expect(
parseToAst(
"SELECT cold, force, manager, refresher FROM cold AS manager",
).errors,
).toHaveLength(0)
})
})

describe("GRANT/REVOKE column wildcard + EXCLUDE — parse & AST", () => {
it("wildcard", () => {
const r = parseToAst("GRANT SELECT ON tab(*) TO alice")
Expand Down Expand Up @@ -8334,6 +8385,29 @@ orders PIVOT (sum(amount) FOR status IN ('open'))`
}
})

describe("SWITCH COLD STORAGE (Enterprise #989) — round-trip", () => {
const queries = [
"SWITCH COLD STORAGE STATUS",
"SWITCH COLD STORAGE ROLE TO MANAGER",
"SWITCH COLD STORAGE ROLE TO MANAGER FORCE",
"SWITCH COLD STORAGE ROLE TO MANAGER TIMEOUT 10000",
"SWITCH COLD STORAGE ROLE TO MANAGER FORCE TIMEOUT 10000",
"SWITCH COLD STORAGE ROLE TO REFRESHER",
"SWITCH COLD STORAGE ROLE TO REFRESHER TIMEOUT 30000",
]

for (const query of queries) {
it(`round-trips: ${query}`, () => {
const parsed = parseToAst(query)
expect(parsed.errors, JSON.stringify(parsed.errors)).toHaveLength(0)

const reparsed = parseToAst(toSql(parsed.ast[0]))
expect(reparsed.errors).toHaveLength(0)
expect(reparsed.ast[0]).toEqual(parsed.ast[0])
})
}
})

describe("GRANT/REVOKE column wildcard + EXCLUDE (Enterprise #1033) — round-trip", () => {
const queries = [
"grant select on t1(*) to ddd",
Expand Down
Loading