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
35 changes: 35 additions & 0 deletions src/autocomplete/content-assist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export interface ContentAssistResult {
* clauses can be autocompleted. See `contextKeywordSuggestions`.
*/
contextKeywords: string[]
/**
* Function names the grammar accepts only through `identifier` at the cursor,
* such as the SUBSAMPLE methods. Rendered as functions, not keywords.
* See `contextFunctionSuggestions`.
*/
contextFunctions: string[]
}

// =============================================================================
Expand Down Expand Up @@ -841,6 +847,29 @@ interface ComputeResult extends CategoryFlags {
nextTokenTypes: TokenType[]
isConditionContext: boolean
contextKeywords: string[]
contextFunctions: string[]
}

// SUBSAMPLE methods (questdb/questdb#7013). The grammar reads the method
// through `identifier` so the names stay non-reserved; suggest them here.
const SUBSAMPLE_METHODS = ["uniform", "cadence", "m4", "minmax", "lttb", "sdt"]

/**
* Function names to suggest where the grammar only accepts them through the
* generic `identifier` sub-rule. Today that is the method after SUBSAMPLE.
*/
function contextFunctionSuggestions(
tokens: IToken[],
suggestions: ContentAssistSuggestion[],
): string[] {
const last = tokens[tokens.length - 1]?.tokenType.name
if (last !== "Subsample") return []
const inSubsample = suggestions.some(
(s) =>
s.nextTokenType.name === "IdentifierKeyword" &&
s.ruleStack.includes("subsampleClause"),
)
return inSubsample ? [...SUBSAMPLE_METHODS] : []
}

// Category names valid inside SHOW CREATE DATABASE (INCLUDE|EXCLUDE) ( ... ).
Expand Down Expand Up @@ -999,12 +1028,14 @@ function computeSuggestions(tokens: IToken[]): ComputeResult {
)

const contextKeywords = contextKeywordSuggestions(tokens, suggestions)
const contextFunctions = contextFunctionSuggestions(tokens, suggestions)

return {
nextTokenTypes: result,
...flags,
isConditionContext,
contextKeywords,
contextFunctions,
}
}

Expand Down Expand Up @@ -1146,6 +1177,7 @@ export function getContentAssist(
referencedColumns: new Set(),
isConditionContext: false,
contextKeywords: [],
contextFunctions: [],
}
}
}
Expand Down Expand Up @@ -1180,6 +1212,7 @@ export function getContentAssist(
let suggestTableValuedFunctions = false
let isConditionContext = false
let contextKeywords: string[] = []
let contextFunctions: string[] = []
try {
const computed = computeSuggestions(tokensForAssist)
nextTokenTypes = computed.nextTokenTypes
Expand All @@ -1191,6 +1224,7 @@ export function getContentAssist(
suggestTableValuedFunctions = computed.suggestTableValuedFunctions
isConditionContext = computed.isConditionContext
contextKeywords = computed.contextKeywords
contextFunctions = computed.contextFunctions
} catch (e) {
// If content assist fails, return empty suggestions
// This can happen with malformed input
Expand Down Expand Up @@ -1283,6 +1317,7 @@ export function getContentAssist(
referencedColumns,
isConditionContext,
contextKeywords,
contextFunctions,
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/autocomplete/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export function createAutocompleteProvider(
referencedColumns,
isConditionContext,
contextKeywords,
contextFunctions,
} = getContentAssist(query, cursorOffset)

// Merge CTE columns into the schema so getColumnsInScope() can find them
Expand Down Expand Up @@ -286,6 +287,23 @@ export function createAutocompleteProvider(
})
}
}
if (contextFunctions.length > 0) {
const seen = new Set(suggestions.map((s) => s.label.toLowerCase()))
for (const fn of contextFunctions) {
if (seen.has(fn)) continue
if (isMidWord && partialPrefix && !fn.startsWith(partialPrefix)) {
continue
}
seen.add(fn)
suggestions.push({
label: fn,
kind: SuggestionKind.Function,
insertText: fn,
filterText: fn,
priority: SuggestionPriority.Medium,
})
}
}

if (suggestTables) {
rankTableSuggestions(suggestions, referencedColumns, columnIndex)
Expand Down
1 change: 1 addition & 0 deletions src/formatter/phrases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const selectPhrases: Phrase[] = [
["Latest", "By"],
["Sample", "By"],
["Group", "By"],
["Subsample"],
["Order", "By"],
["Limit"],
["Window"],
Expand Down
6 changes: 6 additions & 0 deletions src/grammar/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const constants: string[] = [
"beginning",
"bitmap",
"brotli",
"cadence",
"century",
"complete",
"cpu_weight",
Expand Down Expand Up @@ -37,9 +38,11 @@ export const constants: string[] = [
"linear",
"local",
"lowest",
"lttb",
"lz4",
"lz4_raw",
"lzo",
"m4",
"manual",
"materialized_views",
"max_active_queries",
Expand All @@ -51,6 +54,7 @@ export const constants: string[] = [
"millennium",
"millisecond",
"milliseconds",
"minmax",
"minute",
"minutes",
"month",
Expand All @@ -74,6 +78,7 @@ export const constants: string[] = [
"rest",
"rle_dictionary",
"schema",
"sdt",
"search_path",
"second",
"seconds",
Expand All @@ -88,6 +93,7 @@ export const constants: string[] = [
"transaction_isolation",
"true",
"uncompressed",
"uniform",
"unlimited",
"views",
"week",
Expand Down
6 changes: 6 additions & 0 deletions src/grammar/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,17 +286,23 @@ export const aggregateFunctions: string[] = [
]

export const windowFunctions: string[] = [
"cadence",
"cume_dist",
"dense_rank",
"first_value",
"lag",
"last_value",
"lead",
"lttb",
"m4",
"minmax",
"nth_value",
"ntile",
"percent_rank",
"rank",
"row_number",
"sdt",
"uniform",
]

export const tableValuedFunctions: string[] = [
Expand Down
1 change: 1 addition & 0 deletions src/grammar/keywords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export const keywords: string[] = [
"status",
"step",
"storage",
"subsample",
"suspend",
"switch",
"system",
Expand Down
16 changes: 16 additions & 0 deletions src/parser/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export interface SelectStatement extends AstNode {
pivot?: PivotClause
/** Named window definitions: SELECT ... WINDOW w AS (...) [, w2 AS (...)] */
namedWindows?: NamedWindow[]
/** SUBSAMPLE method(args): server-side downsampling, before ORDER BY */
subsample?: SubsampleClause
orderBy?: OrderByItem[]
limit?: LimitClause
setOperations?: SetOperation[]
Expand Down Expand Up @@ -1172,6 +1174,20 @@ export interface LatestOnClause extends AstNode {
partitionBy: QualifiedName[]
}

/**
* SUBSAMPLE clause: `SELECT ... SUBSAMPLE lttb(price, 2000)`.
* Reduces the result to a representative subset of its original rows.
* Methods as of QuestDB Sep 2026: uniform(points), cadence(stride[, seed]),
* m4(column, points), minmax(column, points), lttb(column, points[, gap]),
* sdt(column, compdev). The parser accepts any method name; the server
* validates it.
*/
export interface SubsampleClause extends AstNode {
type: "subsample"
method: string
args: Expression[]
}

export interface OrderByItem extends AstNode {
type: "orderByItem"
expression: Expression
Expand Down
17 changes: 17 additions & 0 deletions src/parser/cst-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export type SimpleSelectCstChildren = {
pivotBody?: PivotBodyCstNode[];
RParen?: IToken[];
windowClause?: WindowClauseCstNode[];
subsampleClause?: SubsampleClauseCstNode[];
orderByClause?: OrderByClauseCstNode[];
limitClause?: LimitClauseCstNode[];
};
Expand Down Expand Up @@ -256,6 +257,7 @@ export type ImplicitSelectBodyCstChildren = {
sampleByClause?: SampleByClauseCstNode[];
latestOnClause?: LatestOnClauseCstNode[];
groupByClause?: GroupByClauseCstNode[];
subsampleClause?: SubsampleClauseCstNode[];
orderByClause?: OrderByClauseCstNode[];
limitClause?: LimitClauseCstNode[];
};
Expand Down Expand Up @@ -510,6 +512,20 @@ export type LatestOnClauseCstChildren = {
Comma?: (IToken)[];
};

export interface SubsampleClauseCstNode extends CstNode {
name: "subsampleClause";
children: SubsampleClauseCstChildren;
}

export type SubsampleClauseCstChildren = {
Subsample: IToken[];
identifier: IdentifierCstNode[];
LParen: IToken[];
expression: (ExpressionCstNode)[];
Comma?: IToken[];
RParen: IToken[];
};

export interface FillClauseCstNode extends CstNode {
name: "fillClause";
children: FillClauseCstChildren;
Expand Down Expand Up @@ -2974,6 +2990,7 @@ export interface ICstNodeVisitor<IN, OUT> extends ICstVisitor<IN, OUT> {
whereClause(children: WhereClauseCstChildren, param?: IN): OUT;
sampleByClause(children: SampleByClauseCstChildren, param?: IN): OUT;
latestOnClause(children: LatestOnClauseCstChildren, param?: IN): OUT;
subsampleClause(children: SubsampleClauseCstChildren, param?: IN): OUT;
fillClause(children: FillClauseCstChildren, param?: IN): OUT;
fillValue(children: FillValueCstChildren, param?: IN): OUT;
alignToClause(children: AlignToClauseCstChildren, param?: IN): OUT;
Expand Down
2 changes: 2 additions & 0 deletions src/parser/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ import {
Brotli,
Lzo,
Storage,
Subsample,
Policy,
Local,
Remote,
Expand Down Expand Up @@ -636,6 +637,7 @@ export {
Brotli,
Lzo,
Storage,
Subsample,
Policy,
Local,
Remote,
Expand Down
22 changes: 22 additions & 0 deletions src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ import {
Default,
// Storage policy tokens
Storage,
Subsample,
Policy,
Local,
Remote,
Expand Down Expand Up @@ -661,6 +662,9 @@ class QuestDBParser extends CstParser {
this.LA(1).tokenType === Window && this.LA(2).tokenType !== Join,
DEF: () => this.SUBRULE(this.windowClause),
})
// SUBSAMPLE method(args): after WHERE / LATEST ON / SAMPLE BY / GROUP BY /
// WINDOW and before ORDER BY / LIMIT (questdb/questdb#7013).
this.OPTION8(() => this.SUBRULE(this.subsampleClause))
this.OPTION6(() => this.SUBRULE(this.orderByClause))
this.OPTION7(() => this.SUBRULE(this.limitClause))
})
Expand Down Expand Up @@ -859,6 +863,7 @@ class QuestDBParser extends CstParser {
this.OPTION1(() => this.SUBRULE(this.sampleByClause))
this.OPTION2(() => this.SUBRULE(this.latestOnClause))
this.OPTION3(() => this.SUBRULE(this.groupByClause))
this.OPTION6(() => this.SUBRULE(this.subsampleClause))
this.OPTION4(() => this.SUBRULE(this.orderByClause))
this.OPTION5(() => this.SUBRULE(this.limitClause))
})
Expand Down Expand Up @@ -1292,6 +1297,23 @@ class QuestDBParser extends CstParser {
])
})

// SUBSAMPLE method(arg, ...): server-side downsampling that returns original
// rows (uniform, cadence, m4, minmax, lttb, sdt). The method name goes
// through `identifier`, as QuestDB's parser reads any word here and the
// optimiser rejects unknown methods, so the six names stay usable as plain
// identifiers and window functions elsewhere.
private subsampleClause = this.RULE("subsampleClause", () => {
this.CONSUME(Subsample)
this.SUBRULE(this.identifier)
this.CONSUME(LParen)
this.SUBRULE(this.expression)
this.MANY(() => {
this.CONSUME(Comma)
this.SUBRULE1(this.expression)
})
this.CONSUME(RParen)
})

private fillClause = this.RULE("fillClause", () => {
this.CONSUME(Fill)
this.CONSUME(LParen)
Expand Down
6 changes: 6 additions & 0 deletions src/parser/toSql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ function selectToSql(stmt: AST.SelectStatement): string {
parts.push(stmt.namedWindows.map(namedWindowToSql).join(", "))
}

// SUBSAMPLE method(args)
if (stmt.subsample) {
const args = stmt.subsample.args.map(expressionToSql).join(", ")
parts.push(`SUBSAMPLE ${escapeIdentifier(stmt.subsample.method)}(${args})`)
}

// ORDER BY
if (stmt.orderBy && stmt.orderBy.length > 0) {
parts.push("ORDER BY")
Expand Down
7 changes: 7 additions & 0 deletions src/parser/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,12 @@ export const IDENTIFIER_KEYWORD_NAMES = new globalThis.Set([
"Pgwire",
"Storage",
"Policy",
"Cadence",
"Lttb",
"M4",
"Minmax",
"Sdt",
"Uniform",
// Window frame keywords
"Row",
"Rows",
Expand Down Expand Up @@ -704,6 +710,7 @@ export const Lzo = getToken("Lzo")

// Storage policy keywords / constants
export const Storage = getToken("Storage")
export const Subsample = getToken("Subsample")
export const Policy = getToken("Policy")
export const Local = getToken("Local")
export const Remote = getToken("Remote")
Expand Down
Loading
Loading