Changed some HogQL reference to TSQL
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user