Changed some HogQL reference to TSQL

This commit is contained in:
Matt Aitken
2025-12-17 09:19:59 +00:00
parent a680ca7d13
commit 66992f33f8
11 changed files with 147 additions and 208 deletions
+6 -2
View File
@@ -2,11 +2,15 @@
TriggerSQL is a DSL that is safely converted into ClickHouse SQL queries with protection against SQL injection and it's tenant-safe (users can only query their own data).
## Attribution
This package is derived from [PostHog's HogQL](https://github.com/PostHog/posthog/tree/master/posthog/hogql) (MIT License). See [NOTICE.md](./NOTICE.md) for the full copyright notice.
## ANTLR Grammar
The ANTLR grammer is heavily inspired by [PostHog's HogQL](https://github.com/PostHog/posthog/tree/master/posthog/hogql).
The ANTLR grammar is heavily inspired by PostHog's HogQL.
These are found in [./grammar] and are the `.g4` files.
These are found in [./src/grammar](./src/grammar) and are the `.g4` files.
## Generating the source code
+2 -2
View File
@@ -1,5 +1,5 @@
// TSQL - Type-Safe SQL Query Language
// A TypeScript port of PostHog's HogQL for ClickHouse queries
// TSQL - Type-Safe SQL Query Language for ClickHouse
// Originally derived from PostHog's HogQL (see NOTICE.md for attribution)
import { CharStreams, CommonTokenStream } from "antlr4ts";
import type { ANTLRErrorListener, RecognitionException, Recognizer } from "antlr4ts";
+14 -15
View File
@@ -1,7 +1,6 @@
// TypeScript translation of posthog/hogql/ast.py
// Keep this file in sync with the Python version
import type { HogQLContext } from "./context";
import type { TSQLContext } from "./context";
import type {
DatabaseField,
ExpressionField,
@@ -15,7 +14,7 @@ import type {
UnknownDatabaseField,
VirtualTable,
} from "./models";
import type { ConstantDataType, HogQLQuerySettings } from "./constants";
import type { ConstantDataType, TSQLQuerySettings } from "./constants";
// Base types
export interface AST {
@@ -25,10 +24,10 @@ export interface AST {
}
export interface Type extends AST {
get_child?(name: string, context: HogQLContext): Type;
has_child?(name: string, context: HogQLContext): boolean;
resolve_constant_type?(context: HogQLContext): ConstantType;
resolve_column_constant_type?(name: string, context: HogQLContext): ConstantType;
get_child?(name: string, context: TSQLContext): Type;
has_child?(name: string, context: TSQLContext): boolean;
resolve_constant_type?(context: TSQLContext): ConstantType;
resolve_column_constant_type?(name: string, context: TSQLContext): ConstantType;
}
export interface Expr extends AST {
@@ -76,7 +75,7 @@ export type Expression =
| SelectSetQuery
| RatioExpr
| SampleExpr
| HogQLXTag;
| TSQLXTag;
export interface CTE extends Expr {
expression_type: "cte";
@@ -98,7 +97,7 @@ export interface FieldAliasType extends Type {
}
export interface BaseTableType extends Type {
resolve_database_table?(context: HogQLContext): Table;
resolve_database_table?(context: TSQLContext): Table;
}
export interface TableType extends BaseTableType {
@@ -504,7 +503,7 @@ export interface JoinExpr extends Expr {
expression_type: "join_expr";
type?: TableOrSelectType;
join_type?: string;
table?: SelectQuery | SelectSetQuery | Placeholder | HogQLXTag | Field;
table?: SelectQuery | SelectSetQuery | Placeholder | TSQLXTag | Field;
table_args?: Expression[];
alias?: string;
table_final?: boolean;
@@ -563,7 +562,7 @@ export interface SelectQuery extends Expr {
limit_by?: LimitByExpr;
limit_with_ties?: boolean;
offset?: Expression;
settings?: HogQLQuerySettings;
settings?: TSQLQuerySettings;
view_name?: string;
}
@@ -603,15 +602,15 @@ export interface SampleExpr extends Expr {
offset_value?: RatioExpr;
}
export interface HogQLXAttribute extends AST {
export interface TSQLXAttribute extends AST {
name: string;
value: any;
}
export interface HogQLXTag extends Expr {
expression_type: "hogqlx_tag";
export interface TSQLXTag extends Expr {
expression_type: "tsqlx_tag";
kind: string;
attributes: HogQLXAttribute[];
attributes: TSQLXAttribute[];
// Equivalent to to_dict() method
to_dict?(): Record<string, any>;
}
@@ -1,5 +1,4 @@
// TypeScript translation of posthog/hogql/constants.py
// Keep this file in sync with the Python version
export type ConstantDataType =
| "int"
@@ -45,15 +44,15 @@ export enum LimitContext {
}
// Settings applied at the SELECT level
export interface HogQLQuerySettings {
export interface TSQLQuerySettings {
optimize_aggregation_in_order?: boolean;
date_time_output_format?: string;
date_time_input_format?: string;
join_algorithm?: string;
}
// Settings applied on top of all HogQL queries
export interface HogQLGlobalSettings extends HogQLQuerySettings {
// Settings applied on top of all TSQL queries
export interface TSQLGlobalSettings extends TSQLQuerySettings {
readonly?: number;
max_execution_time?: number;
max_memory_usage?: number;
+11 -12
View File
@@ -1,19 +1,18 @@
// TypeScript translation of posthog/hogql/context.py
// Keep this file in sync with the Python version
import type { LimitContext } from "./constants";
import type { Database } from "./database";
import type { PropertySwapper } from "./property_types";
import type { HogQLTimings } from "./timings";
import type { TSQLTimings } from "./timings";
export interface HogQLNotice {
export interface TSQLNotice {
start?: number;
end?: number;
message: string;
fix?: string;
}
export interface HogQLQueryModifiers {
export interface TSQLQueryModifiers {
optimizeJoinedFilters?: boolean;
debug?: boolean;
timings?: boolean;
@@ -24,7 +23,7 @@ export interface HogQLQueryModifiers {
optimizeProjections?: boolean;
}
export interface HogQLFieldAccess {
export interface TSQLFieldAccess {
input: string[];
type?: "run";
field?: string;
@@ -36,22 +35,22 @@ export interface Team {
project_id: number;
}
export interface HogQLContext {
export interface TSQLContext {
team_id?: number;
team?: Team;
database?: Database;
values: Record<string, any>;
within_non_hogql_query?: boolean;
within_non_tsql_query?: boolean;
enable_select_queries?: boolean;
limit_top_select?: boolean;
limit_context?: LimitContext;
output_format?: string | null;
globals?: Record<string, any>;
warnings: HogQLNotice[];
notices: HogQLNotice[];
errors: HogQLNotice[];
timings: HogQLTimings;
modifiers: HogQLQueryModifiers;
warnings: TSQLNotice[];
notices: TSQLNotice[];
errors: TSQLNotice[];
timings: TSQLTimings;
modifiers: TSQLQueryModifiers;
debug?: boolean;
property_swapper?: PropertySwapper;
}
+42 -100
View File
@@ -1,5 +1,4 @@
// TypeScript translation of posthog/hogql/database/database.py
// Keep this file in sync with the Python version
//
// NOTE: This implementation requires database/ORM access for:
// - serialize() method (needs DataWarehouseTable, DataWarehouseSavedQuery queries)
@@ -7,7 +6,7 @@
// Adapt these methods to your database/ORM setup
import type { ConstantType } from "./ast";
import type { HogQLContext, HogQLQueryModifiers, Team } from "./context";
import type { TSQLContext, TSQLQueryModifiers, Team } from "./context";
import type {
DatabaseField,
ExpressionField,
@@ -18,16 +17,12 @@ import type {
TableNode,
VirtualTable,
} from "./models";
import type { HogQLTimings } from "./timings";
import type { TSQLTimings } from "./timings";
import { QueryError, ResolutionError } from "./errors";
import { HogQLTimings as HogQLTimingsClass } from "./timings";
import { TSQLTimings as TSQLTimingsClass } from "./timings";
// Type definitions for schema serialization (adapt to your schema types)
export interface DatabaseSchemaTable {
// Base schema table type
}
export interface DatabaseSchemaPostHogTable extends DatabaseSchemaTable {
fields: Record<string, DatabaseSchemaField>;
id: string;
name: string;
@@ -78,7 +73,7 @@ export interface DatabaseSchemaEndpointTable extends DatabaseSchemaTable {
export interface DatabaseSchemaField {
name: string;
hogql_value: string;
tsql_value: string;
type: DatabaseSerializedFieldType;
schema_valid: boolean;
fields?: string[];
@@ -143,6 +138,7 @@ export class Database {
private _warehouseTableNames: string[] = [];
private _warehouseSelfManagedTableNames: string[] = [];
private _viewTableNames: string[] = [];
private _coreTableNames: string[] = [];
private _timezone?: string | null;
private _weekStartDay?: string | null; // WeekStartDay enum
@@ -154,10 +150,6 @@ export class Database {
this.tables = new TableNodeImpl("root");
this._timezone = timezone || null;
this._weekStartDay = weekStartDay || null;
// NOTE: In Python, tables are initialized with all PostHog tables.
// You'll need to initialize these based on your table definitions.
// For now, this is a minimal structure.
}
getTimezone(): string {
@@ -221,20 +213,19 @@ export class Database {
const warehouseTableNames = this._warehouseTableNames.filter((x) => x.includes("."));
return [
...this.getPosthogTableNames(),
...this._coreTableNames,
...warehouseTableNames,
...this._warehouseSelfManagedTableNames,
...this._viewTableNames,
];
}
// These are the tables exposed via SQL editor autocomplete and data management
getPosthogTableNames(): string[] {
return ["events", "groups", "persons", "sessions", ...this.getSystemTableNames()];
// Core tables exposed via SQL editor autocomplete and data management
getCoreTableNames(): string[] {
return [...this._coreTableNames, ...this.getSystemTableNames()];
}
getSystemTableNames(): string[] {
// NOTE: Adapt this based on your SystemTables implementation
const systemNode = this.tables.children["system"];
if (systemNode && systemNode.resolve_all_table_names) {
return ["query_log", ...systemNode.resolve_all_table_names()];
@@ -250,6 +241,13 @@ export class Database {
return this._viewTableNames;
}
addCoreTable(tableName: string, node: TableNode): void {
if (this.tables.add_child) {
this.tables.add_child(node);
}
this._coreTableNames.push(tableName);
}
private _addWarehouseTables(node: TableNode): void {
if (this.tables.merge_with) {
this.tables.merge_with(node);
@@ -280,7 +278,7 @@ export class Database {
}
}
serialize(context: HogQLContext, includeOnly?: Set<string>): Record<string, DatabaseSchemaTable> {
serialize(context: TSQLContext, includeOnly?: Set<string>): Record<string, DatabaseSchemaTable> {
// NOTE: This method requires database queries to fetch:
// - DataWarehouseTable objects
// - DataWarehouseSavedQuery objects
@@ -294,9 +292,9 @@ export class Database {
throw new ResolutionError("Must provide team_id to serialize database");
}
// PostHog tables
const posthogTableNames = this.getPosthogTableNames();
for (const tableName of posthogTableNames) {
// Core tables
const coreTableNames = this.getCoreTableNames();
for (const tableName of coreTableNames) {
if (includeOnly && !includeOnly.has(tableName)) {
continue;
}
@@ -309,13 +307,7 @@ export class Database {
fieldInput = table.fields;
}
const fields = serializeFields(
fieldInput,
context,
tableName.split("."),
undefined,
"posthog"
);
const fields = serializeFields(fieldInput, context, tableName.split("."), undefined);
const fieldsDict: Record<string, DatabaseSchemaField> = {};
for (const field of fields) {
fieldsDict[field.name] = field;
@@ -324,7 +316,7 @@ export class Database {
fields: fieldsDict,
id: tableName,
name: tableName,
} as DatabaseSchemaPostHogTable;
} as DatabaseSchemaTable;
}
// System tables
@@ -342,13 +334,7 @@ export class Database {
systemFieldInput = table.fields;
}
const fields = serializeFields(
systemFieldInput,
context,
tableKey.split("."),
undefined,
"posthog"
);
const fields = serializeFields(systemFieldInput, context, tableKey.split("."), undefined);
const fieldsDict: Record<string, DatabaseSchemaField> = {};
for (const field of fields) {
fieldsDict[field.name] = field;
@@ -373,8 +359,8 @@ export class Database {
teamId?: number,
options?: {
team?: Team;
modifiers?: HogQLQueryModifiers;
timings?: HogQLTimings;
modifiers?: TSQLQueryModifiers;
timings?: TSQLTimings;
}
): Database {
// NOTE: This method requires extensive database/ORM access:
@@ -387,7 +373,7 @@ export class Database {
//
// This is a skeleton structure - adapt to your setup
const timings = options?.timings || new HogQLTimingsClass();
const timings = options?.timings || new TSQLTimingsClass();
const { team, modifiers } = options || {};
// Validate team/teamId
@@ -412,7 +398,6 @@ export class Database {
// NOTE: Apply modifiers, setup tables, etc.
// This requires extensive database access and table setup logic
// See Python implementation for full details
return database;
}
@@ -420,7 +405,7 @@ export class Database {
// Helper functions
const HOGQL_CHARACTERS_TO_BE_WRAPPED = ["@", "-", "!", "$", "+"];
const TSQL_CHARACTERS_TO_BE_WRAPPED = ["@", "-", "!", "$", "+"];
function constantTypeToSerializedFieldType(
constantType: ConstantType
@@ -484,10 +469,9 @@ function constantTypeToSerializedFieldType(
export function serializeFields(
fieldInput: Record<string, FieldOrTable>,
context: HogQLContext,
context: TSQLContext,
tableChain: string[],
dbColumns?: Record<string, any>, // DataWarehouseTableColumns
tableType: "posthog" | "external" = "posthog"
dbColumns?: Record<string, any> // DataWarehouseTableColumns
): DatabaseSchemaField[] {
// NOTE: This requires resolve_types_from_table from resolver
// Import as needed: import { resolveTypesFromTable } from '../resolver';
@@ -506,21 +490,18 @@ export function serializeFields(
}
}
let hogqlValue: string;
if (HOGQL_CHARACTERS_TO_BE_WRAPPED.some((char) => fieldKey.includes(char))) {
hogqlValue = `\`${fieldKey}\``;
let tsqlValue: string;
if (TSQL_CHARACTERS_TO_BE_WRAPPED.some((char) => fieldKey.includes(char))) {
tsqlValue = `\`${fieldKey}\``;
} else {
hogqlValue = fieldKey;
tsqlValue = fieldKey;
}
if ("hidden" in field && field.hidden) {
continue;
}
if (fieldKey === "team_id" && tableType === "posthog") {
// Skip team_id for posthog tables
continue;
} else if ("name" in field && "get_constant_type" in field) {
if ("name" in field && "get_constant_type" in field) {
// DatabaseField
const dbField = field as DatabaseField;
let fieldType: DatabaseSerializedFieldType;
@@ -538,7 +519,7 @@ export function serializeFields(
fieldOutput.push({
name: fieldKey,
hogql_value: hogqlValue,
tsql_value: tsqlValue,
type: fieldType,
schema_valid: schemaValid,
});
@@ -546,13 +527,13 @@ export function serializeFields(
// ExpressionField
const exprField = field as ExpressionField;
// NOTE: Requires resolve_types_from_table
// const resolvedExpr = resolveTypesFromTable(exprField.expr, tableChain, context, 'hogql');
// const resolvedExpr = resolveTypesFromTable(exprField.expr, tableChain, context, 'tsql');
// const constantType = resolvedExpr.type?.resolve_constant_type(context);
// const fieldType = constantTypeToSerializedFieldType(constantType) || DatabaseSerializedFieldType.EXPRESSION;
fieldOutput.push({
name: fieldKey,
hogql_value: hogqlValue,
tsql_value: tsqlValue,
type: DatabaseSerializedFieldType.EXPRESSION,
schema_valid: schemaValid,
});
@@ -568,10 +549,10 @@ export function serializeFields(
fieldOutput.push({
name: fieldKey,
hogql_value: hogqlValue,
tsql_value: tsqlValue,
type,
schema_valid: schemaValid,
table: resolvedTable.to_printed_hogql ? resolvedTable.to_printed_hogql() : fieldKey,
table: resolvedTable.to_printed_tsql ? resolvedTable.to_printed_tsql() : fieldKey,
fields: "fields" in resolvedTable ? Object.keys(resolvedTable.fields) : [],
id: "id" in resolvedTable && resolvedTable.id ? String(resolvedTable.id) : fieldKey,
});
@@ -581,10 +562,10 @@ export function serializeFields(
const virtualTable = field as VirtualTable;
fieldOutput.push({
name: fieldKey,
hogql_value: hogqlValue,
tsql_value: tsqlValue,
type: DatabaseSerializedFieldType.VIRTUAL_TABLE,
schema_valid: schemaValid,
table: virtualTable.to_printed_hogql ? virtualTable.to_printed_hogql() : fieldKey,
table: virtualTable.to_printed_tsql ? virtualTable.to_printed_tsql() : fieldKey,
fields: Object.keys(virtualTable.fields),
});
} else if ("chain" in field) {
@@ -592,7 +573,7 @@ export function serializeFields(
const traverser = field as FieldTraverser;
fieldOutput.push({
name: fieldKey,
hogql_value: hogqlValue,
tsql_value: tsqlValue,
type: DatabaseSerializedFieldType.FIELD_TRAVERSER,
schema_valid: schemaValid,
chain: traverser.chain,
@@ -602,42 +583,3 @@ export function serializeFields(
return fieldOutput;
}
// Helper functions for database setup (simplified versions)
function usePersonPropertiesFromEvents(database: Database): void {
const table = database.getTable("events");
table.fields["person"] = { chain: ["poe"] } as FieldTraverser;
}
function usePersonIdFromPersonOverrides(database: Database): void {
const table = database.getTable("events");
table.fields["event_person_id"] = { name: "person_id" } as DatabaseField;
// NOTE: Setup LazyJoin and ExpressionField for override logic
// This requires complex setup - see Python implementation
}
function useErrorTrackingIssueIdFromErrorTrackingIssueOverrides(database: Database): void {
const table = database.getTable("events");
// NOTE: Setup ExpressionField and LazyJoin for error tracking
// See Python implementation for details
}
function setupGroupKeyFields(database: Database, team: Team): void {
// NOTE: Requires GroupTypeMapping queries from database
// See Python implementation for full logic
const table = database.getTable("events");
// Setup group key fields based on GroupTypeMapping
}
function useVirtualFields(
database: Database,
modifiers: HogQLQueryModifiers,
timings: HogQLTimings
): void {
// NOTE: Requires channel type creation functions
// See Python implementation for full virtual field setup
const eventsTable = database.getTable("events");
const personsTable = database.getTable("persons");
const groupsTable = database.getTable("groups");
// Setup virtual fields like initial_referring_domain_type, initial_channel_type, revenue fields
}
+12 -13
View File
@@ -1,9 +1,8 @@
// TypeScript translation of posthog/hogql/errors.py
// Keep this file in sync with the Python version
import type { Expr } from "./ast";
export class BaseHogQLError extends Error {
export class BaseTSQLError extends Error {
message: string;
start?: number;
end?: number;
@@ -29,34 +28,34 @@ export class BaseHogQLError extends Error {
}
}
export class ExposedHogQLError extends BaseHogQLError {
export class ExposedTSQLError extends BaseTSQLError {
/** An exception that can be exposed to the user. */
}
export class InternalHogQLError extends BaseHogQLError {
/** An internal exception in the HogQL engine. */
export class InternalTSQLError extends BaseTSQLError {
/** An internal exception in the TSQL engine. */
}
export class SyntaxError extends ExposedHogQLError {
/** The input does not conform to HogQL syntax. */
export class SyntaxError extends ExposedTSQLError {
/** The input does not conform to TSQL syntax. */
}
export class QueryError extends ExposedHogQLError {
export class QueryError extends ExposedTSQLError {
/** The query is invalid, though correct syntactically. */
}
export class NotImplementedError extends InternalHogQLError {
/** This feature isn't implemented in HogQL (yet). */
export class NotImplementedError extends InternalTSQLError {
/** This feature isn't implemented in TSQL (yet). */
}
export class ParsingError extends InternalHogQLError {
export class ParsingError extends InternalTSQLError {
/** Parsing failed. */
}
export class ImpossibleASTError extends InternalHogQLError {
export class ImpossibleASTError extends InternalTSQLError {
/** Parsing or resolution resulted in an impossible AST. */
}
export class ResolutionError extends InternalHogQLError {
export class ResolutionError extends InternalTSQLError {
/** Resolution of a table/field/expression failed. */
}
+5 -6
View File
@@ -1,8 +1,7 @@
// TypeScript translation of posthog/hogql/database/models.py
// Keep this file in sync with the Python version
import type { Expr, ConstantType } from "./ast";
import type { HogQLContext } from "./context";
import type { TSQLContext } from "./context";
export interface FieldOrTable {
hidden?: boolean;
@@ -43,15 +42,15 @@ export interface Table extends FieldOrTable {
fields: Record<string, FieldOrTable>;
has_field?(name: string | number): boolean;
get_field?(name: string | number): FieldOrTable;
to_printed_clickhouse?(context: HogQLContext): string;
to_printed_hogql?(): string;
to_printed_clickhouse?(context: TSQLContext): string;
to_printed_tsql?(): string;
avoid_asterisk_fields?(): string[];
get_asterisk?(): Record<string, FieldOrTable>;
}
export interface LazyJoin extends FieldOrTable {
join_function?(from_table: Table, to_table: Table, requesting_table: Table): Expr;
resolve_table?(context: HogQLContext): Table;
resolve_table?(context: TSQLContext): Table;
}
export interface LazyTable extends Table {}
@@ -63,7 +62,7 @@ export interface SavedQuery extends Table {
}
export interface FunctionCallTable extends Table {
call_function?(context: HogQLContext): Expr;
call_function?(context: TSQLContext): Expr;
}
export interface TableNode {
+27 -27
View File
@@ -148,8 +148,8 @@ import {
ForInStatement,
ForStatement,
Function,
HogQLXAttribute,
HogQLXTag,
TSQLXAttribute,
TSQLXTag,
IfStatement,
JoinConstraint,
JoinExpr,
@@ -181,7 +181,7 @@ import {
WindowFunction,
} from "./ast";
import { RESERVED_KEYWORDS } from "./constants";
import { BaseHogQLError, NotImplementedError, SyntaxError } from "./errors";
import { BaseTSQLError, NotImplementedError, SyntaxError } from "./errors";
import { parseStringLiteralText } from "./parse_string";
/**
@@ -339,7 +339,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
return node;
} catch (e: any) {
if (e instanceof BaseHogQLError) {
if (e instanceof BaseTSQLError) {
if (
start !== undefined &&
end !== undefined &&
@@ -570,7 +570,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
// SELECT statements
visitSelect(ctx: SelectContext): SelectQuery | SelectSetQuery | HogQLXTag {
visitSelect(ctx: SelectContext): SelectQuery | SelectSetQuery | TSQLXTag {
const selectSetStmt = ctx.selectSetStmt();
const selectStmt = ctx.selectStmt();
const tSQLxTagElement = ctx.tSQLxTagElement();
@@ -579,7 +579,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
} else if (selectStmt) {
return this.visitSelectStmt(selectStmt);
} else if (tSQLxTagElement) {
return this.visitHogqlxTagElementNested(tSQLxTagElement);
return this.visitTsqlxTagElementNested(tSQLxTagElement);
}
throw new SyntaxError(
"Select statement must be either a select set statement, a select statement, or a tSQLx tag element"
@@ -844,7 +844,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
return tableResult;
}
// Otherwise, wrap the table expression in a JoinExpr
const table = tableResult as SelectQuery | SelectSetQuery | Placeholder | HogQLXTag | Field;
const table = tableResult as SelectQuery | SelectSetQuery | Placeholder | TSQLXTag | Field;
return {
expression_type: "join_expr",
table,
@@ -856,13 +856,13 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
/** Helper for visiting table expressions that may return JoinExpr or table types */
private visitTableExprResult(
ctx: ParserRuleContext
): JoinExpr | SelectQuery | SelectSetQuery | Placeholder | HogQLXTag | Field {
): JoinExpr | SelectQuery | SelectSetQuery | Placeholder | TSQLXTag | Field {
return this.visit(ctx) as
| JoinExpr
| SelectQuery
| SelectSetQuery
| Placeholder
| HogQLXTag
| TSQLXTag
| Field;
}
@@ -1512,7 +1512,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
return tableResult;
}
// Otherwise, wrap in a JoinExpr
const table = tableResult as SelectQuery | SelectSetQuery | Placeholder | HogQLXTag | Field;
const table = tableResult as SelectQuery | SelectSetQuery | Placeholder | TSQLXTag | Field;
return { expression_type: "join_expr", table, alias };
}
@@ -1520,8 +1520,8 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
return this.visitTableFunctionExpr(ctx.tableFunctionExpr());
}
visitTableExprTag(ctx: TableExprTagContext): HogQLXTag {
return this.visitHogqlxTagElementNested(ctx.tSQLxTagElement());
visitTableExprTag(ctx: TableExprTagContext): TSQLXTag {
return this.visitTsqlxTagElementNested(ctx.tSQLxTagElement());
}
visitTableFunctionExpr(ctx: TableFunctionExprContext): JoinExpr {
@@ -1670,51 +1670,51 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
};
}
visitHogqlxChildElement(ctx: TSQLxChildElementContext): Expression {
visitTsqlxChildElement(ctx: TSQLxChildElementContext): Expression {
const tSQLxTagElement = ctx.tSQLxTagElement();
if (tSQLxTagElement) {
return this.visitHogqlxTagElementNested(tSQLxTagElement);
return this.visitTsqlxTagElementNested(tSQLxTagElement);
}
if (ctx.TSQLX_TEXT_TEXT()) {
return this.visitHogqlxText(ctx);
return this.visitTsqlxText(ctx);
}
return this.visitAsExpr(ctx.columnExpr()!);
}
visitHogqlxText(ctx: TSQLxChildElementContext): Constant {
visitTsqlxText(ctx: TSQLxChildElementContext): Constant {
const text = ctx.TSQLX_TEXT_TEXT();
return { expression_type: "constant", value: text ? text.text : "" };
}
visitHogqlxTagElementClosed(ctx: TSQLxTagElementContext): HogQLXTag {
visitTsqlxTagElementClosed(ctx: TSQLxTagElementContext): TSQLXTag {
const kind = this.visitIdentifier(ctx.identifier()[0]);
const attributes = ctx.tSQLxTagAttribute()
? ctx
.tSQLxTagAttribute()
.map((a: TSQLxTagAttributeContext) => this.visitHogqlxTagAttribute(a))
.map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a))
: [];
return { expression_type: "hogqlx_tag", kind, attributes };
return { expression_type: "tsqlx_tag", kind, attributes };
}
visitHogqlxTagElementNested(ctx: TSQLxTagElementContext): HogQLXTag {
visitTsqlxTagElementNested(ctx: TSQLxTagElementContext): TSQLXTag {
const opening = this.visitIdentifier(ctx.identifier(0));
const closing = this.visitIdentifier(ctx.identifier(1));
if (opening !== closing) {
throw new SyntaxError(
`Opening and closing HogQLX tags must match. Got ${opening} and ${closing}`
`Opening and closing TSQLX tags must match. Got ${opening} and ${closing}`
);
}
const attributes = ctx.tSQLxTagAttribute()
? ctx
.tSQLxTagAttribute()
.map((a: TSQLxTagAttributeContext) => this.visitHogqlxTagAttribute(a))
.map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a))
: [];
// ── collect child nodes, discarding pure-indentation whitespace ──
const keptChildren: Expression[] = [];
for (const element of ctx.tSQLxChildElement()) {
const child = this.visitHogqlxChildElement(element);
const child = this.visitTsqlxChildElement(element);
if ("value" in child && typeof child.value === "string") {
const v = child.value;
@@ -1729,18 +1729,18 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
if (keptChildren.length > 0) {
if (attributes.some((a: HogQLXAttribute) => a.name === "children")) {
if (attributes.some((a: TSQLXAttribute) => a.name === "children")) {
throw new SyntaxError(
"Can't have a HogQLX tag with both children and a 'children' attribute"
"Can't have a TSQLX tag with both children and a 'children' attribute"
);
}
attributes.push({ name: "children", value: keptChildren });
}
return { expression_type: "hogqlx_tag", kind: opening, attributes };
return { expression_type: "tsqlx_tag", kind: opening, attributes };
}
visitHogqlxTagAttribute(ctx: TSQLxTagAttributeContext): HogQLXAttribute {
visitTsqlxTagAttribute(ctx: TSQLxTagAttributeContext): TSQLXAttribute {
const name = this.visitIdentifier(ctx.identifier());
const columnExpr = ctx.columnExpr();
const string = ctx.string();
@@ -1,5 +1,4 @@
// TypeScript translation of posthog/hogql/transforms/property_types.py
// Keep this file in sync with the Python version
import type {
AST,
@@ -16,20 +15,20 @@ import type {
CallType,
DateTimeType,
} from "./ast";
import type { HogQLContext } from "./context";
import type { TSQLContext } from "./context";
import type { BooleanDatabaseField, DateTimeDatabaseField, Table } from "./models";
// Helper function to escape HogQL identifiers
function escapeHogQLIdentifier(identifier: string | number): string {
// Helper function to escape TSQL identifiers
function escapeTSQLIdentifier(identifier: string | number): string {
if (typeof identifier === "number") {
return String(identifier);
}
if (identifier.includes("%")) {
throw new Error(
`The HogQL identifier "${identifier}" is not permitted as it contains the "%" character`
`The TSQL identifier "${identifier}" is not permitted as it contains the "%" character`
);
}
// HogQL allows dollars in the identifier
// TSQL allows dollars in the identifier
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) {
return identifier;
}
@@ -70,8 +69,8 @@ function getVisitorMethodName(node: AST): string {
// Handle special cases (matching Python replacements)
const replacements: Record<string, string> = {
hog_qlxtag: "hogqlx_tag",
hog_qlxattribute: "hogqlx_attribute",
tsqlxtag: "tsqlx_tag",
tsqlxattribute: "tsqlx_attribute",
uuidtype: "uuid_type",
string_jsontype: "string_json_type",
};
@@ -264,13 +263,13 @@ class CloningVisitor extends Visitor<any> {
// PropertyFinder: Traverses AST to find all property references
class PropertyFinder extends TraversingVisitor {
context: HogQLContext;
context: TSQLContext;
personProperties: Set<string> = new Set();
eventProperties: Set<string> = new Set();
groupProperties: Map<number, Set<string>> = new Map();
foundTimestamps: boolean = false;
constructor(context: HogQLContext) {
constructor(context: TSQLContext) {
super();
this.context = context;
}
@@ -281,7 +280,7 @@ class PropertyFinder extends TraversingVisitor {
if (this.isBaseTableType(tableType)) {
const table = tableType.resolve_database_table?.(this.context);
if (table) {
const tableName = table.to_printed_hogql?.() || "";
const tableName = table.to_printed_tsql?.() || "";
const propertyName = String(node.chain[0]);
if (tableName === "persons" || tableName === "raw_persons") {
@@ -358,7 +357,7 @@ export class PropertySwapper extends CloningVisitor {
eventProperties: Map<string, string>;
personProperties: Map<string, string>;
groupProperties: Map<string, string>;
context: HogQLContext;
context: TSQLContext;
setTimeZones: boolean;
constructor(
@@ -366,7 +365,7 @@ export class PropertySwapper extends CloningVisitor {
eventProperties: Map<string, string> | Record<string, string>,
personProperties: Map<string, string> | Record<string, string>,
groupProperties: Map<string, string> | Record<string, string>,
context: HogQLContext,
context: TSQLContext,
setTimeZones: boolean
) {
super(false); // Don't clear types
@@ -428,7 +427,7 @@ export class PropertySwapper extends CloningVisitor {
} else if (this.isBaseTableType(tableType)) {
const table = tableType.resolve_database_table?.(this.context);
if (table) {
const tableName = table.to_printed_hogql?.() || "";
const tableName = table.to_printed_tsql?.() || "";
if (tableName === "persons" || tableName === "raw_persons") {
if (this.personProperties.has(propertyName)) {
@@ -472,7 +471,7 @@ export class PropertySwapper extends CloningVisitor {
if (this.isBaseTableType(tableType)) {
const table = tableType.resolve_database_table?.(this.context);
if (table) {
const tableName = table.to_printed_hogql?.() || "";
const tableName = table.to_printed_tsql?.() || "";
if (tableName === "events") {
if (this.personProperties.has(propertyName)) {
return this.convertStringPropertyToType(node, "person", propertyName);
@@ -490,16 +489,16 @@ export class PropertySwapper extends CloningVisitor {
propertyType: "event" | "person" | "group",
propertyName: string
): Expr {
let posthogFieldType: string | undefined;
let fieldTypeValue: string | undefined;
if (propertyType === "person") {
posthogFieldType = this.personProperties.get(propertyName);
fieldTypeValue = this.personProperties.get(propertyName);
} else if (propertyType === "group") {
posthogFieldType = this.groupProperties.get(propertyName);
fieldTypeValue = this.groupProperties.get(propertyName);
} else {
posthogFieldType = this.eventProperties.get(propertyName);
fieldTypeValue = this.eventProperties.get(propertyName);
}
const fieldType = posthogFieldType === "Numeric" ? "Float" : posthogFieldType || "String";
const fieldType = fieldTypeValue === "Numeric" ? "Float" : fieldTypeValue || "String";
this.addPropertyNotice(node, propertyType, fieldType);
return this.fieldTypeToPropertyCall(node, fieldType);
@@ -630,7 +629,7 @@ export class PropertySwapper extends CloningVisitor {
}
// Only highlight the last part of the chain
const lastPart = node.chain[node.chain.length - 1];
const identifierLength = escapeHogQLIdentifier(lastPart).length;
const identifierLength = escapeTSQLIdentifier(lastPart).length;
this.context.notices.push({
start: Math.max(node.start, node.end - identifierLength),
end: node.end,
@@ -672,7 +671,7 @@ export class PropertySwapper extends CloningVisitor {
}
// Main function to build property swapper
export function buildPropertySwapper(node: AST, context: HogQLContext): void {
export function buildPropertySwapper(node: AST, context: TSQLContext): void {
if (!context || !context.team_id) {
return;
}
+4 -5
View File
@@ -1,5 +1,4 @@
// TypeScript translation of posthog/hogql/timings.py
// Keep this file in sync with the Python version
/**
* Get performance counter in milliseconds (Node.js equivalent of perf_counter)
@@ -31,8 +30,8 @@ const TIMING_DECIMAL_PLACES = 3; // round to milliseconds
// Not thread safe.
// See trends_query_runner for an example of how to use for multithreaded queries
export class HogQLTimings {
// Completed time in seconds for different parts of the HogQL query
export class TSQLTimings {
// Completed time in seconds for different parts of the TSQL query
timings: Record<string, number> = {};
// Used for housekeeping
@@ -44,8 +43,8 @@ export class HogQLTimings {
this._timingStarts[this._timingPointer] = this.perfCounter();
}
cloneForSubquery(seriesIndex: number): HogQLTimings {
return new HogQLTimings(`${this._timingPointer}/series_${seriesIndex}`);
cloneForSubquery(seriesIndex: number): TSQLTimings {
return new TSQLTimings(`${this._timingPointer}/series_${seriesIndex}`);
}
clearTimings(): void {