fix: realtime improvements

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-03-20 09:52:29 +01:00
parent d1b39c4c93
commit 88a2d876ce
20 changed files with 2060 additions and 536 deletions

View File

@@ -73,18 +73,20 @@ export class Query<T = any> {
};
private _transform?: Record<string, (item: T) => any>;
private _union?: Query;
private _dateRegex = /\d{4}-\d{2}-\d{2}([\s\:\d\.]+)?/g;
private _dateRegex = /\d{4}-\d{2}-\d{2}([\s:\d.]+)?/g;
constructor(
private client: ClickHouseClient,
private timezone: string,
private timezone: string
) {}
// Select methods
select<U>(
columns: (string | Expression | null | undefined | false)[],
type: 'merge' | 'replace' = 'replace',
type: 'merge' | 'replace' = 'replace'
): Query<U> {
if (this._skipNext) return this as unknown as Query<U>;
if (this._skipNext) {
return this as unknown as Query<U>;
}
if (type === 'merge') {
this._select = [
...this._select,
@@ -92,7 +94,7 @@ export class Query<T = any> {
];
} else {
this._select = columns.filter((col): col is string | Expression =>
Boolean(col),
Boolean(col)
);
}
return this as unknown as Query<U>;
@@ -122,8 +124,12 @@ export class Query<T = any> {
// Where methods
private escapeValue(value: SqlParam): string {
if (value === null) return 'NULL';
if (value instanceof Expression) return `(${value.toString()})`;
if (value === null) {
return 'NULL';
}
if (value instanceof Expression) {
return `(${value.toString()})`;
}
if (Array.isArray(value)) {
return `(${value.map((v) => this.escapeValue(v)).join(', ')})`;
}
@@ -139,7 +145,9 @@ export class Query<T = any> {
}
where(column: string, operator: Operator, value?: SqlParam): this {
if (this._skipNext) return this;
if (this._skipNext) {
return this;
}
const condition = this.buildCondition(column, operator, value);
this._where.push({ condition, operator: 'AND' });
return this;
@@ -148,7 +156,7 @@ export class Query<T = any> {
public buildCondition(
column: string,
operator: Operator,
value?: SqlParam,
value?: SqlParam
): string {
switch (operator) {
case 'IS NULL':
@@ -162,7 +170,7 @@ export class Query<T = any> {
throw new Error('BETWEEN operator requires an array of two values');
case 'IN':
case 'NOT IN':
if (!Array.isArray(value) && !(value instanceof Expression)) {
if (!(Array.isArray(value) || value instanceof Expression)) {
throw new Error(`${operator} operator requires an array value`);
}
return `${column} ${operator} ${this.escapeValue(value)}`;
@@ -224,7 +232,9 @@ export class Query<T = any> {
// Order by methods
orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): this {
if (this._skipNext) return this;
if (this._skipNext) {
return this;
}
this._orderBy.push({ column, direction });
return this;
}
@@ -259,7 +269,7 @@ export class Query<T = any> {
fill(
from: string | Date | Expression,
to: string | Date | Expression,
step: string | Expression,
step: string | Expression
): this {
this._fill = {
from:
@@ -288,7 +298,7 @@ export class Query<T = any> {
innerJoin(
table: string | Expression,
condition: string,
alias?: string,
alias?: string
): this {
return this.joinWithType('INNER', table, condition, alias);
}
@@ -296,7 +306,7 @@ export class Query<T = any> {
leftJoin(
table: string | Expression | Query,
condition: string,
alias?: string,
alias?: string
): this {
return this.joinWithType('LEFT', table, condition, alias);
}
@@ -304,7 +314,7 @@ export class Query<T = any> {
leftAnyJoin(
table: string | Expression | Query,
condition: string,
alias?: string,
alias?: string
): this {
return this.joinWithType('LEFT ANY', table, condition, alias);
}
@@ -312,7 +322,7 @@ export class Query<T = any> {
rightJoin(
table: string | Expression,
condition: string,
alias?: string,
alias?: string
): this {
return this.joinWithType('RIGHT', table, condition, alias);
}
@@ -320,7 +330,7 @@ export class Query<T = any> {
fullJoin(
table: string | Expression,
condition: string,
alias?: string,
alias?: string
): this {
return this.joinWithType('FULL', table, condition, alias);
}
@@ -333,9 +343,11 @@ export class Query<T = any> {
type: JoinType,
table: string | Expression | Query,
condition: string,
alias?: string,
alias?: string
): this {
if (this._skipNext) return this;
if (this._skipNext) {
return this;
}
this._joins.push({
type,
table,
@@ -386,9 +398,9 @@ export class Query<T = any> {
// on them, otherwise any embedded date strings get double-escaped
// (e.g. ''2025-12-16 23:59:59'') which ClickHouse rejects.
.map((col) =>
col instanceof Expression ? col.toString() : this.escapeDate(col),
col instanceof Expression ? col.toString() : this.escapeDate(col)
)
.join(', '),
.join(', ')
);
} else {
parts.push('SELECT *');
@@ -411,7 +423,7 @@ export class Query<T = any> {
const aliasClause = join.alias ? ` ${join.alias} ` : ' ';
const conditionStr = join.condition ? `ON ${join.condition}` : '';
parts.push(
`${join.type} JOIN ${join.table instanceof Query ? `(${join.table.toSQL()})` : join.table instanceof Expression ? `(${join.table.toString()})` : join.table}${aliasClause}${conditionStr}`,
`${join.type} JOIN ${join.table instanceof Query ? `(${join.table.toSQL()})` : join.table instanceof Expression ? `(${join.table.toString()})` : join.table}${aliasClause}${conditionStr}`
);
});
}
@@ -524,10 +536,10 @@ export class Query<T = any> {
// Execution methods
async execute(): Promise<T[]> {
const query = this.buildQuery();
console.log(
'query',
`${query.replaceAll('\n', ' ').replaceAll('\t', ' ').replaceAll('\r', ' ')} SETTINGS session_timezone = '${this.timezone}'`,
);
// console.log(
// 'query',
// `${query.replaceAll('\n', ' ').replaceAll('\t', ' ').replaceAll('\r', ' ')} SETTINGS session_timezone = '${this.timezone}'`,
// );
const result = await this.client.query({
query,
@@ -574,7 +586,9 @@ export class Query<T = any> {
// Add merge method
merge(query: Query): this {
if (this._skipNext) return this;
if (this._skipNext) {
return this;
}
this._from = query._from;
@@ -621,7 +635,7 @@ export class WhereGroupBuilder {
constructor(
private query: Query,
private groupOperator: 'AND' | 'OR',
private groupOperator: 'AND' | 'OR'
) {}
where(column: string, operator: Operator, value?: SqlParam): this {
@@ -706,7 +720,7 @@ clix.toStartOf = (node: string, interval: IInterval, timezone?: string) => {
clix.toStartOfInterval = (
node: string,
interval: IInterval,
origin: string | Date,
origin: string | Date
) => {
switch (interval) {
case 'minute': {