drizzle-cube
Version:
Drizzle ORM-first semantic layer with Cube.js compatibility. Type-safe analytics and dashboards with SQL injection protection.
1,143 lines (1,074 loc) • 116 kB
TypeScript
import { AnyColumn } from 'drizzle-orm';
import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
import { MySql2Database } from 'drizzle-orm/mysql2';
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import { SQL } from 'drizzle-orm';
import { Subquery } from 'drizzle-orm';
import { Table } from 'drizzle-orm';
import { View } from 'drizzle-orm';
/**
* AI-generated analysis of an execution plan
*/
export declare interface AIExplainAnalysis {
/** One-sentence description of what the query does */
summary: string;
/** Overall performance assessment */
assessment: 'good' | 'warning' | 'critical';
/** Reason for the assessment */
assessmentReason: string;
/** Detailed explanation of the query's purpose and structure */
queryUnderstanding: string;
/** Issues identified in the execution plan */
issues: ExplainIssue[];
/** Actionable recommendations for improvement */
recommendations: ExplainRecommendation[];
}
/**
* Abstract base class for database executors
*/
export declare abstract class BaseDatabaseExecutor implements DatabaseExecutor {
db: DrizzleDatabase;
schema?: any | undefined;
databaseAdapter: DatabaseAdapter;
constructor(db: DrizzleDatabase, schema?: any | undefined, engineType?: 'postgres' | 'mysql' | 'sqlite' | 'singlestore');
abstract execute<T = any[]>(query: SQL | any, numericFields?: string[]): Promise<T>;
abstract getEngineType(): 'postgres' | 'mysql' | 'sqlite' | 'singlestore';
abstract explainQuery(sqlString: string, params: unknown[], options?: ExplainOptions): Promise<ExplainResult>;
abstract getTableIndexes(tableNames: string[]): Promise<IndexInfo[]>;
}
/**
* Base query definition that can be extended dynamically
* Returns just the FROM/JOIN/WHERE setup, not a complete SELECT
*/
export declare interface BaseQueryDefinition {
/** Main table to query from */
from: QueryableRelation;
/** Optional joins to other tables */
joins?: Array<{
table: QueryableRelation;
on: SQL;
type?: 'left' | 'right' | 'inner' | 'full';
}>;
/** Base WHERE conditions (typically security context filtering) */
where?: SQL;
}
/**
* Build the complete EXPLAIN analysis prompt with all context
*
* @param databaseType - The database engine type
* @param cubeSchema - JSON-formatted cube schema
* @param semanticQuery - JSON of the original semantic query
* @param sqlQuery - The generated SQL query
* @param normalizedPlan - JSON of the normalized ExplainOperation[]
* @param rawExplain - Raw EXPLAIN output from the database
* @param existingIndexes - Optional: JSON of existing indexes on relevant tables
* @returns Complete prompt ready to send to AI
*/
export declare function buildExplainAnalysisPrompt(databaseType: 'postgres' | 'mysql' | 'sqlite', cubeSchema: string, semanticQuery: string, sqlQuery: string, normalizedPlan: string, rawExplain: string, existingIndexes?: string): string;
/**
* Build the Step 0 validation prompt
*
* @param userPrompt - User's raw input to validate
* @returns Complete prompt ready to send to AI
*/
export declare function buildStep0Prompt(userPrompt: string): string;
/**
* Build the Step 1 prompt for query shape analysis
*
* @param cubeSchema - JSON-formatted cube schema
* @param userPrompt - User's natural language query
* @returns Complete prompt ready to send to AI
*/
export declare function buildStep1Prompt(cubeSchema: string, userPrompt: string): string;
/**
* Build the Step 2 prompt with actual dimension values from the database
*
* @param cubeSchema - JSON-formatted cube schema
* @param userPrompt - User's natural language query
* @param dimensionValues - Actual dimension values fetched from database
* @returns Complete prompt ready to send to AI
*/
export declare function buildStep2Prompt(cubeSchema: string, userPrompt: string, dimensionValues: DimensionValues): string;
/**
* Build the single-step system prompt with schema and user prompt
*
* @param cubeSchema - JSON-formatted cube schema
* @param userPrompt - User's natural language query
* @returns Complete prompt ready to send to AI
*/
export declare function buildSystemPrompt(cubeSchema: string, userPrompt: string): string;
/**
* Cache configuration options
*/
export declare interface CacheConfig {
/**
* Cache provider implementation
* Required if caching is enabled
*/
provider: CacheProvider;
/**
* Default TTL in milliseconds
* @default 300000 (5 minutes)
*/
defaultTtlMs?: number;
/**
* Prefix for all cache keys
* Useful for multi-environment setups (e.g., 'prod:', 'dev:')
* @default 'drizzle-cube:'
*/
keyPrefix?: string;
/**
* Enable/disable caching globally
* Allows disabling without removing configuration
* @default true
*/
enabled?: boolean;
/**
* Whether to include security context in cache key
* CRITICAL for multi-tenant applications - should almost always be true
* @default true
*/
includeSecurityContext?: boolean;
/**
* Custom function to extract cacheable parts of security context
* Use when security context contains non-serializable values
* @default Uses JSON.stringify on entire security context
*/
securityContextSerializer?: (ctx: SecurityContext) => string;
/**
* Callback for cache errors (get/set failures)
* Cache errors are non-fatal by default - queries still execute
*/
onError?: (error: Error, operation: 'get' | 'set' | 'delete') => void;
/**
* Callback for cache events (hits, misses, sets)
* Useful for monitoring and debugging
*/
onCacheEvent?: (event: CacheEvent) => void;
}
/**
* Metadata about a cached entry
* Used to provide cache information in query responses
*/
export declare interface CacheEntryMetadata {
/** Unix timestamp (ms) when the entry was cached */
cachedAt: number;
/** Original TTL in milliseconds */
ttlMs: number;
/** Remaining TTL in milliseconds */
ttlRemainingMs: number;
}
/**
* Cache event for monitoring and debugging
*/
export declare interface CacheEvent {
/** Type of cache event */
type: 'hit' | 'miss' | 'set' | 'error';
/** Cache key involved */
key: string;
/** Duration of the cache operation in milliseconds */
durationMs: number;
}
/**
* Return type for cache get operations
* Includes value and optional metadata for TTL tracking
*/
export declare interface CacheGetResult<T> {
value: T;
metadata?: CacheEntryMetadata;
}
/**
* Configuration for cache key generation
*/
declare interface CacheKeyConfig {
/** Prefix for all cache keys */
keyPrefix?: string;
/** Whether to include security context in cache key */
includeSecurityContext?: boolean;
/** Custom serializer for security context */
securityContextSerializer?: (ctx: SecurityContext) => string;
}
/**
* Cache provider interface that users implement for their preferred backend
* All methods use async/Promise to support network-based caches (Redis, etc.)
*/
export declare interface CacheProvider {
/**
* Get a cached value by key
* @returns The cached value with metadata, or null/undefined if not found or expired
*/
get<T = unknown>(key: string): Promise<CacheGetResult<T> | null | undefined>;
/**
* Set a value in the cache
* @param key - Cache key
* @param value - Value to cache (must be JSON-serializable)
* @param ttlMs - Time-to-live in milliseconds (optional, uses default if not provided)
*/
set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void>;
/**
* Delete a specific key from the cache
* @returns true if key existed and was deleted, false otherwise
*/
delete(key: string): Promise<boolean>;
/**
* Delete all keys matching a pattern (for cache invalidation)
* Pattern uses glob-style matching: 'prefix:*' matches all keys starting with 'prefix:'
* @returns Number of keys deleted
*/
deletePattern(pattern: string): Promise<number>;
/**
* Check if a key exists in the cache
*/
has(key: string): Promise<boolean>;
/**
* Optional: Called when the cache provider is no longer needed
* Use for cleanup (e.g., closing Redis connections)
*/
close?(): Promise<void>;
}
/**
* Compiled cube with execution function
*/
export declare interface CompiledCube extends Cube {
/** Execute a query against this cube */
queryFn: (query: SemanticQuery, securityContext: SecurityContext) => Promise<QueryResult>;
}
/**
* Auto-detect database type and create appropriate executor
* @param db - Drizzle database instance
* @param schema - Optional schema for type inference
* @param engineType - Optional explicit engine type override
* @returns Appropriate database executor
*/
export declare function createDatabaseExecutor(db: DrizzleDatabase, schema?: any, engineType?: 'postgres' | 'mysql' | 'sqlite' | 'singlestore'): DatabaseExecutor;
/**
* Create a new semantic layer instance with Drizzle integration
* Use this when you need multiple isolated instances
*/
export declare function createDrizzleSemanticLayer(options: {
drizzle: DrizzleDatabase;
schema?: any;
}): SemanticLayerCompiler;
/**
* Helper to create multi-cube query context
*/
export declare function createMultiCubeContext(baseContext: QueryContext, cubes: Map<string, Cube>, currentCube: Cube): MultiCubeQueryContext;
/**
* Factory function for creating MySQL executors
*/
export declare function createMySQLExecutor(db: DrizzleDatabase, schema?: any): MySQLExecutor;
/**
* Factory function for creating PostgreSQL executors
*/
export declare function createPostgresExecutor(db: DrizzleDatabase, schema?: any): PostgresExecutor;
/**
* Factory function for creating SQLite executors
*/
export declare function createSQLiteExecutor(db: DrizzleDatabase, schema?: any): SQLiteExecutor;
/**
* Cube definition focused on Drizzle query building
*/
declare interface Cube {
name: string;
title?: string;
description?: string;
/**
* Base query setup - returns the foundation that can be extended
* Should return FROM/JOIN/WHERE setup, NOT a complete SELECT
*/
sql: (ctx: QueryContext) => BaseQueryDefinition;
/** Cube dimensions using direct column references */
dimensions: Record<string, Dimension>;
/** Cube measures using direct column references */
measures: Record<string, Measure>;
/** Optional joins to other cubes for multi-cube queries */
joins?: Record<string, CubeJoin>;
/** Whether cube is publicly accessible */
public?: boolean;
/** SQL alias for the cube */
sqlAlias?: string;
/** Data source identifier */
dataSource?: string;
/** Additional metadata */
meta?: Record<string, any>;
}
export { Cube }
export { Cube as SemanticCube }
/**
* Helper type for creating type-safe cubes
*/
export declare interface CubeDefiner {
<TName extends string>(name: TName, definition: CubeDefinition): Cube & {
name: TName;
};
}
/**
* Utility type for cube definition with schema inference
*/
export declare type CubeDefinition = Omit<Cube, 'name'> & {
name?: string;
};
/**
* Type-safe cube join definition with lazy loading support
*/
declare interface CubeJoin {
/** Target cube reference - lazy loaded to avoid circular dependencies */
targetCube: Cube | (() => Cube);
/** Semantic relationship - determines join behavior */
relationship: CubeRelationship;
/** Array of join conditions - supports multi-column joins */
on: Array<{
/** Column from source cube */
source: AnyColumn;
/** Column from target cube */
target: AnyColumn;
/** Comparison operator - defaults to eq */
as?: (source: AnyColumn, target: AnyColumn) => SQL;
}>;
/** Override default SQL join type (derived from relationship) */
sqlJoinType?: 'inner' | 'left' | 'right' | 'full';
/**
* Many-to-many relationship configuration through a junction table
* Only used when relationship is 'belongsToMany'
*/
through?: {
/** Junction/join table (Drizzle table reference) */
table: Table;
/** Join conditions from source cube to junction table */
sourceKey: Array<{
source: AnyColumn;
target: AnyColumn;
as?: (source: AnyColumn, target: AnyColumn) => SQL;
}>;
/** Join conditions from junction table to target cube */
targetKey: Array<{
source: AnyColumn;
target: AnyColumn;
as?: (source: AnyColumn, target: AnyColumn) => SQL;
}>;
/** Optional security context SQL for junction table */
securitySql?: (securityContext: SecurityContext) => SQL | SQL[];
};
}
export { CubeJoin }
export { CubeJoin as SemanticJoin }
/**
* Cube metadata for API responses
*/
export declare interface CubeMetadata {
name: string;
title: string;
description?: string;
measures: MeasureMetadata[];
dimensions: DimensionMetadata[];
segments: any[];
relationships?: CubeRelationshipMetadata[];
/** Additional cube metadata (e.g., eventStream configuration for funnel queries) */
meta?: Record<string, any>;
}
/**
* Relationship types supported by cube joins
*/
export declare type CubeRelationship = 'belongsTo' | 'hasOne' | 'hasMany' | 'belongsToMany';
/**
* Cube relationship metadata for ERD visualization
*/
export declare interface CubeRelationshipMetadata {
targetCube: string;
relationship: 'belongsTo' | 'hasOne' | 'hasMany' | 'belongsToMany';
joinFields: Array<{
sourceField: string;
targetField: string;
}>;
}
declare interface DatabaseAdapter {
/**
* Get the database engine type this adapter supports
*/
getEngineType(): 'postgres' | 'mysql' | 'sqlite' | 'singlestore';
/**
* Check if the database supports LATERAL joins
* Required for optimized flow queries with index-backed seeks
* @returns true for PostgreSQL 9.3+, MySQL 8.0.14+, SingleStore; false for SQLite
*/
supportsLateralJoins(): boolean;
/**
* Build SQL INTERVAL from ISO 8601 duration string
* Used for time window constraints in funnel analysis
* @param duration - ISO 8601 duration (e.g., "P7D" for 7 days, "PT1H" for 1 hour)
* @returns SQL expression representing the interval
*/
buildIntervalFromISO(duration: string): SQL;
/**
* Build time difference expression in seconds between two timestamps
* Used for calculating time-to-convert metrics in funnel analysis
* @param end - End timestamp expression
* @param start - Start timestamp expression
* @returns SQL expression for (end - start) in seconds
*/
buildTimeDifferenceSeconds(end: SQL, start: SQL): SQL;
/**
* Build expression to add an ISO 8601 duration to a timestamp
* Used for time window constraint checks in funnel analysis
* @param timestamp - Base timestamp expression
* @param duration - ISO 8601 duration to add
* @returns SQL expression for timestamp + interval
*/
buildDateAddInterval(timestamp: SQL, duration: string): SQL;
/**
* Build conditional aggregation with database-specific syntax
* Used for single-pass funnel metrics aggregation
* PostgreSQL uses FILTER clause, MySQL/SQLite use CASE WHEN
* @param aggFn - Aggregation function: 'count' | 'avg' | 'min' | 'max' | 'sum'
* @param expr - Expression to aggregate (null for COUNT(*))
* @param condition - Condition for filtering
* @returns SQL for conditional aggregation
*/
buildConditionalAggregation(aggFn: 'count' | 'avg' | 'min' | 'max' | 'sum', expr: SQL | null, condition: SQL): SQL;
/**
* Build time dimension expression with granularity truncation
* @param granularity - Time granularity (day, month, year, etc.)
* @param fieldExpr - The date/timestamp field expression
* @returns SQL expression for truncated time dimension
*/
buildTimeDimension(granularity: TimeGranularity, fieldExpr: AnyColumn | SQL): SQL;
/**
* Build string matching condition
* @param fieldExpr - The field to search in
* @param operator - The string matching operator
* @param value - The value to match
* @returns SQL expression for string matching
*/
buildStringCondition(fieldExpr: AnyColumn | SQL, operator: 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'like' | 'notLike' | 'ilike' | 'regex' | 'notRegex', value: string): SQL;
/**
* Cast expression to specific database type
* @param fieldExpr - The field expression to cast
* @param targetType - Target database type
* @returns SQL expression with type casting
*/
castToType(fieldExpr: AnyColumn | SQL, targetType: 'timestamp' | 'decimal' | 'integer'): SQL;
/**
* Build AVG aggregation expression with database-specific null handling
* @param fieldExpr - The field expression to average
* @returns SQL expression for AVG aggregation (COALESCE vs IFNULL for null handling)
*/
buildAvg(fieldExpr: AnyColumn | SQL): SQL;
/**
* Build CASE WHEN conditional expression
* @param conditions - Array of condition/result pairs
* @param elseValue - Optional ELSE clause value
* @returns SQL expression for CASE WHEN statement
*/
buildCaseWhen(conditions: Array<{
when: SQL;
then: any;
}>, elseValue?: any): SQL;
/**
* Build boolean literal expression
* @param value - Boolean value to represent
* @returns SQL expression for boolean literal (TRUE/FALSE/1/0 depending on database)
*/
buildBooleanLiteral(value: boolean): SQL;
/**
* Convert filter values to database-compatible types
* @param value - The filter value to convert
* @returns Converted value for database queries
*/
convertFilterValue(value: any): any;
/**
* Prepare date value for database-specific storage format
* @param date - Date value to prepare
* @returns Database-compatible date representation
*/
prepareDateValue(date: Date): any;
/**
* Check if this database stores timestamps as integers
* @returns True if timestamps are stored as integers (milliseconds), false for native timestamps
*/
isTimestampInteger(): boolean;
/**
* Convert time dimension result values back to Date objects for consistency
* @param value - The time dimension value from query results
* @returns Date object or original value if not a time dimension
*/
convertTimeDimensionResult(value: any): any;
/**
* Preprocess calculated measure template for database-specific transformations
* This allows each adapter to modify the template before substitution occurs
* @param calculatedSql - The template string with {member} references
* @returns Preprocessed template string
*/
preprocessCalculatedTemplate(calculatedSql: string): string;
/**
* Get database capabilities for feature detection
* Used for graceful degradation when functions aren't supported
*/
getCapabilities(): DatabaseCapabilities;
/**
* Build STDDEV aggregation expression
* @param fieldExpr - The field expression to calculate stddev for
* @param useSample - Use sample stddev (STDDEV_SAMP) vs population (STDDEV_POP). Default: false
* @returns SQL expression or null if unsupported
*/
buildStddev(fieldExpr: AnyColumn | SQL, useSample?: boolean): SQL | null;
/**
* Build VARIANCE aggregation expression
* @param fieldExpr - The field expression to calculate variance for
* @param useSample - Use sample variance (VAR_SAMP) vs population (VAR_POP). Default: false
* @returns SQL expression or null if unsupported
*/
buildVariance(fieldExpr: AnyColumn | SQL, useSample?: boolean): SQL | null;
/**
* Build PERCENTILE aggregation expression
* @param fieldExpr - The field expression to calculate percentile for
* @param percentile - Percentile value (0-100)
* @returns SQL expression or null if unsupported
*/
buildPercentile(fieldExpr: AnyColumn | SQL, percentile: number): SQL | null;
/**
* Build a window function expression
* @param type - Window function type (lag, lead, rank, etc.)
* @param fieldExpr - The field expression (null for rank functions that don't need a field)
* @param partitionBy - PARTITION BY columns
* @param orderBy - ORDER BY columns with direction
* @param config - Additional configuration (offset, default, frame, etc.)
* @returns SQL expression or null if unsupported
*/
buildWindowFunction(type: WindowFunctionType, fieldExpr: AnyColumn | SQL | null, partitionBy?: (AnyColumn | SQL)[], orderBy?: Array<{
field: AnyColumn | SQL;
direction: 'asc' | 'desc';
}>, config?: WindowFunctionConfig): SQL | null;
}
/**
* Database capabilities for feature detection
* Used for graceful degradation when functions aren't supported
*/
declare interface DatabaseCapabilities {
/** Whether the database supports STDDEV_POP/STDDEV_SAMP */
supportsStddev: boolean;
/** Whether the database supports VAR_POP/VAR_SAMP */
supportsVariance: boolean;
/** Whether the database supports PERCENTILE_CONT or similar */
supportsPercentile: boolean;
/** Whether the database supports window functions (LAG, LEAD, RANK, etc.) */
supportsWindowFunctions: boolean;
/** Whether the database supports frame clauses (ROWS BETWEEN, RANGE BETWEEN) */
supportsFrameClause: boolean;
/** Whether the database supports LATERAL joins (PostgreSQL 9.3+, MySQL 8.0.14+) */
supportsLateralJoins: boolean;
}
/**
* Database executor interface that wraps Drizzle ORM
* Provides type-safe SQL execution with engine-specific implementations
*/
export declare interface DatabaseExecutor {
/** The Drizzle database instance */
db: DrizzleDatabase;
/** Optional schema for type inference */
schema?: any;
/** Database adapter for SQL dialect-specific operations */
databaseAdapter: DatabaseAdapter;
/** Execute a Drizzle SQL query or query object */
execute<T = any[]>(query: SQL | any, numericFields?: string[]): Promise<T>;
/** Get the database engine type */
getEngineType(): 'postgres' | 'mysql' | 'sqlite' | 'singlestore';
/** Execute EXPLAIN on a SQL query to get the execution plan */
explainQuery(sqlString: string, params: unknown[], options?: ExplainOptions): Promise<ExplainResult>;
/** Get existing indexes for the specified tables */
getTableIndexes(tableNames: string[]): Promise<IndexInfo[]>;
}
/**
* Helper function to create cubes
*/
export declare function defineCube(name: string, definition: Omit<Cube, 'name'>): Cube;
/**
* Dimension definition
*/
declare interface Dimension {
name: string;
title?: string;
description?: string;
type: DimensionType;
/** Direct column reference or SQL expression */
sql: AnyColumn | SQL | ((ctx: QueryContext) => AnyColumn | SQL);
/** Whether this is a primary key */
primaryKey?: boolean;
/** Whether to show in UI */
shown?: boolean;
/** Display format */
format?: string;
/** Additional metadata */
meta?: Record<string, any>;
}
export { Dimension }
export { Dimension as SemanticDimension }
export declare interface DimensionAnnotation {
title: string;
shortTitle: string;
type: string;
format?: DimensionFormat;
}
export declare type DimensionFormat = 'currency' | 'percent' | 'number' | 'date' | 'datetime' | 'id' | 'link';
/**
* Dimension metadata for API responses
*/
export declare interface DimensionMetadata {
name: string;
title: string;
shortTitle: string;
type: string;
format?: DimensionFormat;
description?: string;
}
export declare type DimensionType = 'string' | 'number' | 'time' | 'boolean';
/**
* Actual dimension values fetched from the database
* Used in Step 2 to provide real values for filter generation
*/
export declare interface DimensionValues {
/** Map of dimension name to array of distinct values */
[dimensionName: string]: string[];
}
/**
* Drizzle column reference type
* Use native Drizzle AnyColumn type for better type safety
*/
export declare type DrizzleColumn = AnyColumn;
/**
* Core database interface that supports all Drizzle instances
* Uses flexible typing for maximum compatibility across different database drivers
*/
export declare interface DrizzleDatabase {
select: (fields?: any) => any;
insert: (table: any) => any;
update: (table: any) => any;
delete: (table: any) => any;
execute?: (query: SQL) => Promise<unknown[]>;
run?: (query: SQL) => unknown;
all?: (query: SQL) => unknown[];
get?: (query: SQL) => unknown;
$with: (alias: string) => {
as: (query: any) => any;
};
with: (...args: any[]) => any;
schema?: unknown;
}
/**
* System prompt template for EXPLAIN plan analysis.
*
* Placeholders:
* - {DATABASE_TYPE} - postgres, mysql, or sqlite
* - {CUBE_SCHEMA} - JSON-formatted cube schema with relationships
* - {SEMANTIC_QUERY} - The original semantic query object
* - {SQL_QUERY} - The generated SQL query
* - {NORMALIZED_PLAN} - JSON of normalized ExplainOperation[]
* - {RAW_EXPLAIN} - Raw EXPLAIN output from database
*/
export declare const EXPLAIN_ANALYSIS_PROMPT = "You are a database performance expert analyzing query execution plans for a semantic layer (Cube.js/drizzle-cube).\n\nCRITICAL CONTEXT - READ CAREFULLY:\nThe user is working with a semantic layer that auto-generates SQL queries. They do NOT write or modify SQL directly.\n\nTherefore, your recommendations MUST focus ONLY on:\n1. INDEX CREATION - Specific CREATE INDEX statements they can run\n2. TABLE STRUCTURE - Schema changes (column types, constraints)\n3. CUBE CONFIGURATION - How cube definitions (joins, filters) might be improved\n4. GENERAL INSIGHTS - Understanding what makes the query slow\n\nDO NOT recommend:\n- Rewriting the SQL query (users can't do this)\n- Changing JOIN order (the semantic layer handles this)\n- Using different query patterns (CTEs, subqueries, etc.)\n- Any SQL modification beyond index/schema changes\n\nDATABASE TYPE: {DATABASE_TYPE}\n\nCUBE DEFINITION SYNTAX (drizzle-cube):\nUsers define cubes in TypeScript like this. There are TWO valid syntax patterns for security context:\n\nPATTERN 1 - Simple WHERE filter (older syntax):\n```typescript\nconst employeesCube = defineCube({\n name: 'Employees',\n // Security filter - returns just the WHERE condition\n sql: (securityContext) => eq(employees.organisationId, securityContext.organisationId),\n // ...\n})\n```\n\nPATTERN 2 - Full QueryContext with BaseQueryDefinition (recommended):\n```typescript\nconst employeesCube = defineCube({\n name: 'Employees',\n // Security filter - returns object with 'from' and 'where'\n sql: (ctx: QueryContext<Schema>): BaseQueryDefinition => ({\n from: employees,\n where: eq(employees.organisationId, ctx.securityContext.organisationId)\n }),\n // ...\n})\n```\n\nBOTH patterns correctly implement security context filtering. The key is:\n- Pattern 1: The function receives securityContext directly and returns a WHERE condition\n- Pattern 2: The function receives ctx (QueryContext) and accesses ctx.securityContext\n\nFULL CUBE EXAMPLE:\n```typescript\nconst employeesCube = defineCube({\n name: 'Employees',\n // Security filter using Pattern 2 (recommended)\n sql: (ctx: QueryContext<Schema>): BaseQueryDefinition => ({\n from: employees,\n where: eq(employees.organisationId, ctx.securityContext.organisationId)\n }),\n\n // Joins to other cubes\n joins: {\n Departments: {\n targetCube: () => departmentsCube,\n relationship: 'belongsTo', // or 'hasOne', 'hasMany', 'belongsToMany'\n on: [{ source: employees.departmentId, target: departments.id }]\n }\n },\n\n measures: {\n count: { type: 'count', sql: () => employees.id },\n avgSalary: { type: 'avg', sql: () => employees.salary }\n },\n\n dimensions: {\n name: { type: 'string', sql: () => employees.name },\n createdAt: { type: 'time', sql: () => employees.createdAt }\n }\n})\n```\n\nSECURITY CONTEXT VALIDATION:\nWhen checking if a cube has proper security context, look for EITHER:\n- `sql: (securityContext) => eq(table.organisationId, securityContext.organisationId)`\n- `sql: (ctx) => ({ from: table, where: eq(table.organisationId, ctx.securityContext.organisationId) })`\n- Any variation that filters by organisationId using the security context parameter\n\nA cube is MISSING security context ONLY if:\n- The sql function doesn't use the securityContext/ctx parameter at all\n- There's no filter on organisationId (or equivalent tenant identifier)\n- The sql property is missing entirely\n\nCUBE RECOMMENDATION TYPES:\nWhen suggesting cube changes, ONLY recommend features that drizzle-cube supports:\n\nSUPPORTED FEATURES:\n- dimensions (with sql expressions)\n- measures (count, sum, avg, min, max, countDistinct, countDistinctApprox)\n- joins (belongsTo, hasOne, hasMany, belongsToMany)\n- security context filtering via sql function\n\nNOT SUPPORTED (do NOT recommend these):\n- preAggregations (not implemented)\n- segments (not implemented)\n- refreshKey (not implemented)\n- scheduledRefresh (not implemented)\n\n1. ADDING JOINS - If queries frequently combine cubes without explicit joins:\n ```typescript\n joins: {\n TargetCube: {\n targetCube: () => targetCube,\n relationship: 'belongsTo', // or 'hasOne', 'hasMany', 'belongsToMany'\n on: [{ source: table.foreignKey, target: targetTable.id }]\n }\n }\n ```\n\n2. OPTIMIZING BASE QUERY FILTERS (ONLY if SQL lacks tenant filtering):\n NOTE: If the SQL already filters by organisation_id, tenant_id, or similar, the cube is correctly configured.\n Only suggest this if security/tenant filtering is genuinely missing from the generated SQL.\n ```typescript\n sql: (ctx: QueryContext<Schema>): BaseQueryDefinition => ({\n from: table,\n where: and(\n eq(table.organisationId, ctx.securityContext.organisationId),\n eq(table.isActive, true) // Add commonly-used filters to base query\n )\n })\n ```\n\n3. ADDING CALCULATED MEASURES - For commonly-needed aggregations:\n ```typescript\n measures: {\n averageOrderValue: {\n type: 'avg',\n sql: () => orders.total\n },\n activeUserCount: {\n type: 'count',\n sql: () => users.id,\n filters: [{ sql: () => eq(users.isActive, true) }]\n }\n }\n ```\n\nCUBE SCHEMA (the semantic layer structure):\n{CUBE_SCHEMA}\n\nSEMANTIC QUERY (what the user requested):\n{SEMANTIC_QUERY}\n\nGENERATED SQL:\n{SQL_QUERY}\n\nEXECUTION PLAN (normalized format):\n{NORMALIZED_PLAN}\n\nRAW EXPLAIN OUTPUT:\n{RAW_EXPLAIN}\n\nEXISTING INDEXES ON RELEVANT TABLES:\n{EXISTING_INDEXES}\n\nIMPORTANT: Before recommending an index, check if it already exists above. If an index already exists:\n- Do NOT recommend creating it again\n- Instead, note that the index exists and analyze whether it's being used effectively\n- If the index exists but isn't being used, recommend investigating why (wrong column order, statistics outdated, etc.)\n\nIMPORTANT: Before recommending security context optimizations, CHECK THE SQL QUERY above for existing filters:\n- Look for tenant/security filters like: organisation_id, organizationId, tenant_id, tenantId, org_id, orgId, company_id, companyId, or similar\n- If the SQL already contains parameterized filters on any of these columns (e.g., \"organisation_id = $1\", \"tenant_id = ?\"), security context IS ALREADY IMPLEMENTED\n- Do NOT suggest \"add security context\" or \"optimize base query filters\" if the SQL already filters by a tenant identifier\n- drizzle-cube AUTOMATICALLY applies security context to all queries - if you see tenant filters in the SQL, the cube is correctly configured\n- Only suggest security filter optimizations if the SQL genuinely lacks tenant filtering (which would be a serious bug)\n\nANALYSIS TASKS:\n\n1. UNDERSTAND THE QUERY\n - What business question is this answering?\n - What cubes and relationships are involved?\n - What aggregations and filters are applied?\n\n2. IDENTIFY PERFORMANCE ISSUES\n - Sequential scans on large tables (look for \"Seq Scan\" / \"ALL\" access)\n - Missing indexes (filters/joins on unindexed columns)\n - High row estimates with filters that could benefit from indexes\n - Sort operations that could use indexes\n\n3. GENERATE ACTIONABLE RECOMMENDATIONS\n For each issue, provide:\n - Specific CREATE INDEX statement (if applicable)\n - Exact table and column names\n - Expected impact estimate\n - {DATABASE_TYPE}-specific syntax\n\nINDEX SYNTAX BY DATABASE:\n- PostgreSQL: CREATE INDEX idx_name ON table_name (column1, column2);\n- MySQL: CREATE INDEX idx_name ON table_name (column1, column2);\n- SQLite: CREATE INDEX idx_name ON table_name (column1, column2);\n\nCOMPOSITE INDEX GUIDANCE:\n- For filters: Index columns used in WHERE clauses\n- For joins: Index foreign key columns (e.g., department_id, organisation_id)\n- For sorting: Include ORDER BY columns in index\n- Multi-tenant: Always consider including organisation_id in composite indexes\n\nRESPONSE FORMAT (JSON):\n{\n \"summary\": \"Brief description of what this query does\",\n \"assessment\": \"good|warning|critical\",\n \"assessmentReason\": \"Why this assessment\",\n \"queryUnderstanding\": \"Detailed explanation of the query's purpose and structure\",\n \"issues\": [\n {\n \"type\": \"sequential_scan|missing_index|high_cost|sort_operation\",\n \"description\": \"What the issue is\",\n \"severity\": \"high|medium|low\"\n }\n ],\n \"recommendations\": [\n {\n \"type\": \"index\",\n \"severity\": \"critical|warning|suggestion\",\n \"title\": \"Short actionable title\",\n \"description\": \"Detailed explanation of why this helps\",\n \"sql\": \"CREATE INDEX idx_name ON table (columns);\",\n \"table\": \"table_name\",\n \"columns\": [\"col1\", \"col2\"],\n \"estimatedImpact\": \"Expected improvement\"\n },\n {\n \"type\": \"cube\",\n \"severity\": \"critical|warning|suggestion\",\n \"title\": \"Short actionable title\",\n \"description\": \"Why this cube change helps\",\n \"cubeCode\": \"TypeScript snippet to add to the cube definition\",\n \"cubeName\": \"CubeName\",\n \"estimatedImpact\": \"Expected improvement\"\n }\n ]\n}\n\nCRITICAL: Return ONLY valid JSON. No markdown, no explanations outside JSON.";
/**
* Issue identified in the execution plan
*/
export declare interface ExplainIssue {
/** Type of issue */
type: 'sequential_scan' | 'missing_index' | 'high_cost' | 'sort_operation' | string;
/** Description of the issue */
description: string;
/** Severity level */
severity: 'high' | 'medium' | 'low';
}
/**
* A single operation/node in the query execution plan
* Normalized structure across all databases
*/
export declare interface ExplainOperation {
/** Operation type (e.g., 'Seq Scan', 'Index Scan', 'Hash Join', 'Sort') */
type: string;
/** Table name if applicable */
table?: string;
/** Index name if used */
index?: string;
/** Estimated row count from planner */
estimatedRows?: number;
/** Actual row count (if ANALYZE was used) */
actualRows?: number;
/** Estimated cost (database-specific units) */
estimatedCost?: number;
/** Filter condition if any */
filter?: string;
/** Additional details specific to this operation */
details?: string;
/** Nested/child operations */
children?: ExplainOperation[];
}
/**
* Options for EXPLAIN query execution
*/
export declare interface ExplainOptions {
/** Use EXPLAIN ANALYZE to actually execute the query and get real timing (PostgreSQL, MySQL 8.0.18+) */
analyze?: boolean;
}
/**
* A recommendation from AI analysis of an execution plan
*/
export declare interface ExplainRecommendation {
/** Type of recommendation */
type: 'index' | 'table' | 'cube' | 'general';
/** Severity/priority of the recommendation */
severity: 'critical' | 'warning' | 'suggestion';
/** Short actionable title */
title: string;
/** Detailed explanation of why this helps */
description: string;
/** Actionable SQL statement (e.g., CREATE INDEX) - for index/table recommendations */
sql?: string;
/** TypeScript code snippet to add to cube definition - for cube recommendations */
cubeCode?: string;
/** Which cube to modify - for cube recommendations */
cubeName?: string;
/** Affected database table */
table?: string;
/** Affected columns */
columns?: string[];
/** Expected performance improvement */
estimatedImpact?: string;
}
/**
* Result of an EXPLAIN query
* Provides both normalized structure and raw output
*/
export declare interface ExplainResult {
/** Normalized hierarchical plan as operations */
operations: ExplainOperation[];
/** Summary statistics */
summary: ExplainSummary;
/** Raw EXPLAIN output as text (for display) */
raw: string;
/** Original SQL query */
sql: {
sql: string;
params?: unknown[];
};
}
/**
* Summary statistics from the execution plan
*/
export declare interface ExplainSummary {
/** Database engine type */
database: 'postgres' | 'mysql' | 'sqlite';
/** Planning time in milliseconds (if available) */
planningTime?: number;
/** Execution time in milliseconds (if ANALYZE was used) */
executionTime?: number;
/** Total estimated cost */
totalCost?: number;
/** Quick flag: true if any sequential scans detected */
hasSequentialScans: boolean;
/** List of indexes used in the plan */
usedIndexes: string[];
}
/**
* Type-level utility to extract the schema type from a cube reference
* Since we removed generics, this now returns 'any'
*/
export declare type ExtractSchemaFromCubeRef = any;
/**
* Filter definitions with logical operators
*/
export declare type Filter = FilterCondition | LogicalFilter;
/**
* Manages filter SQL caching for a single query execution
*
* Design principles:
* - Immutable SQL: Once cached, SQL objects are never modified
* - Cache lifetime: Created at query start, discarded after query execution
* - Key by content: Filters with same member/operator/values share SQL
* - Thread-safe: No shared mutable state between queries
*/
declare class FilterCacheManager {
private cache;
private stats;
/**
* Get cached SQL or build and cache it
*
* @param key - The cache key (use getFilterKey or getTimeDimensionFilterKey)
* @param builder - Function to build the SQL if not cached
* @returns The cached or freshly built SQL, or null if builder returns null
*/
getOrBuild(key: string, builder: () => SQL | null): SQL | null;
/**
* Check if a key exists in the cache without affecting stats
*/
has(key: string): boolean;
/**
* Get cached SQL without building (returns undefined if not cached)
*/
get(key: string): SQL | undefined;
/**
* Pre-populate cache with multiple filters
* Useful for batch initialization at query start
*/
preload(entries: Array<{
key: string;
sql: SQL;
}>): void;
/**
* Store a single entry in the cache
*/
set(key: string, sql: SQL): void;
/**
* Get cache statistics for debugging
*/
getStats(): FilterCacheStats;
/**
* Clear the cache (normally not needed as cache is per-query)
*/
clear(): void;
}
/**
* Cache statistics for debugging and monitoring
*/
declare interface FilterCacheStats {
hits: number;
misses: number;
cacheSize: number;
}
export declare interface FilterCondition {
member: string;
operator: FilterOperator;
values: any[];
dateRange?: string | string[];
}
/**
* Supported filter operators
*/
export declare type FilterOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'startsWith' | 'notStartsWith' | 'endsWith' | 'notEndsWith' | 'gt' | 'gte' | 'lt' | 'lte' | 'set' | 'notSet' | 'inDateRange' | 'beforeDate' | 'afterDate' | 'between' | 'notBetween' | 'in' | 'notIn' | 'like' | 'notLike' | 'ilike' | 'regex' | 'notRegex' | 'isEmpty' | 'isNotEmpty' | 'arrayContains' | 'arrayOverlaps' | 'arrayContained';
/**
* Flow query configuration for server-side execution
* This is the configuration extracted from SemanticQuery.flow
*/
declare interface FlowQueryConfig {
/**
* Binding key that identifies individual entities (e.g., userId)
* Can be a single string like 'Events.userId' or array for multi-cube
*/
bindingKey: string | {
cube: string;
dimension: string;
}[];
/**
* Time dimension used for ordering events
* Can be a single string like 'Events.timestamp' or array for multi-cube
*/
timeDimension: string | {
cube: string;
dimension: string;
}[];
/**
* The starting step from which we explore paths
* Defines the anchor point for bidirectional flow analysis
*/
startingStep: {
/** Display name for the starting step */
name: string;
/** Filter(s) that identify events for this starting step */
filter?: Filter | Filter[];
};
/** Number of steps to explore BEFORE the starting step (0-5) */
stepsBefore: number;
/** Number of steps to explore AFTER the starting step (0-5) */
stepsAfter: number;
/**
* Event dimension that categorizes events (e.g., 'Events.eventType')
* This dimension's values become the node labels in the Sankey diagram
*/
eventDimension: string;
/**
* Optional limit on the number of entities to process
* Useful for performance on large datasets
*/
entityLimit?: number;
/**
* Output mode for flow data aggregation
* - 'sankey': Aggregate by (layer, event_type) - standard flow visualization where paths can converge
* - 'sunburst': Path-qualified nodes for hierarchical tree visualization where each path is unique
* @default 'sankey'
*/
outputMode?: 'sankey' | 'sunburst';
/**
* Join strategy for fetching before/after steps
* - 'auto' (default): Use lateral when supported, otherwise window
* - 'lateral': Force lateral joins (error if not supported)
* - 'window': Force window-function strategy
*/
joinStrategy?: 'auto' | 'lateral' | 'window';
}
/**
* FNV-1a hash - fast, non-cryptographic hash function
* Returns hex string for cache key readability
*
* Properties:
* - O(n) time complexity
* - Low collision rate for similar strings
* - Deterministic across runs
*
* @param str - String to hash
* @returns 8-character hex string
*/
export declare function fnv1aHash(str: string): string;
/**
* Format cube metadata for AI consumption
* Includes relationships and joins for full context
*
* @param metadata - Array of CubeMetadata from the semantic layer
* @returns JSON string formatted for the AI prompt
*/
export declare function formatCubeSchemaForExplain(metadata: CubeMetadata[]): string;
/**
* Format existing indexes for AI consumption
*
* @param indexes - Array of index information from database query
* @returns Formatted string for the AI prompt
*/
export declare function formatExistingIndexes(indexes: Array<{
table_name: string;
index_name: string;
columns: string[];
is_unique?: boolean;
is_primary?: boolean;
}>): string;
/**
* Binding key mapping for multi-cube funnels
* Maps the user/entity identifier across different cubes
*/
export declare interface FunnelBindingKeyMapping {
cube: string;
dimension: string;
}
/**
* Funnel capabilities per database engine
*/
export declare interface FunnelCapabilities {
/** Whether database supports PERCENTILE_CONT */
supportsPercentile: boolean;
/** Whether database supports INTERVAL arithmetic directly */
supportsIntervalArithmetic: boolean;
}
/**
* Funnel query configuration
*/
export declare interface FunnelQueryConfig {
/**
* Binding key - dimension that links entities across steps.
* This is typically a user ID, session ID, or other entity identifier.
* String for single-cube (e.g., 'Events.userId'),
* Array for multi-cube with different column names per cube.
*/
bindingKey: string | FunnelBindingKeyMapping[];
/**
* Time dimension for temporal ordering.
* Used to determine event order and calculate time-to-convert.
* String for single-cube (e.g., 'Events.timestamp'),
* Array for multi-cube with different timestamp columns per cube.
*/
timeDimension: string | FunnelTimeDimensionMapping[];
/** Ordered array of funnel steps (minimum 2) */
steps: FunnelStep[];
/** Include time-to-convert metrics in results (default: false) */
includeTimeMetrics?: boolean;
/**
* Global time window - all steps must complete within this duration
* from the first step. Format: ISO 8601 duration.
*/
globalTimeWindow?: string;
}
/**
* Result row for funnel query
*/
export declare interface FunnelResultRow {
/** Step name */
step: string;
/** Step position (0-indexed) */
stepIndex: number;
/** Count of entities that reached this step */
count: number;
/**
* Step-to-step conversion rate (count / previous step count).
* null for first step.
*/
conversionRate: number | null;
/**
* Cumulative conversion rate from first step (count / first step count).
*/
cumulativeConversionRate: number;
/** Average time to reach this step from previous step (seconds) */
avgSecondsToConvert?: number | null;
/** Median time to reach this step from previous step (seconds) */
medianSecondsToConvert?: number | null;
/** 90th percentile time to reach this step (seconds) */
p90SecondsToConvert?: number | null;
/** Minimum time to reach this step (seconds) */
minSecondsToConvert?: number | null;
/** Maximum time to reach this step (seconds) */
maxSecondsToConvert?: number | null;
}
export declare type FunnelStep = FunnelStepSingleCube | FunnelStepMultiCube;
/**
* Multi-cube step definition - steps can use different cubes
*/
export declare interface FunnelStepMultiCube extends FunnelStepSingleCube {
/** Which cube this step uses */
cube: string;
}
/**
* Single-cube step definition - all steps use the same cube
*/
export declare interface FunnelStepSingleCube {
/** Step display name */
name: string;
/** Filter conditions for this step */
filter?: Filter | Filter[];
/**
* Time window constraint - max duration from previous step.
* Format: ISO 8601 duration (e.g., "P7D" for 7 days, "PT1H" for 1 hour)
*/
timeToConvert?: string;
}
/**
* Time dimension mapping for multi-cube funnels
* Maps the timestamp field across different cubes
*/
export declare interface FunnelTimeDimensionMapping {
cube: string;
dimension: string;
}
/**
* Generate a deterministic cache key from query and security context
*
* Key structure: {prefix}query:{queryHash}:ctx:{securityHash}
*
* Uses FNV-1a hash for:
* - Speed: ~3x faster than SHA-256
* - Simplicity: No dependencies required
* - Sufficient collision resistance for cache keys
*
* @param query - The semantic query to cache
* @param securityContext - Security context for tenant isolation
* @param config - Cache key configuration
* @returns Deterministic cache key string
*/
export declare function generateCacheKey(query: SemanticQuery, securityContext: SecurityContext, config?: CacheKeyConfig): string;
/**
* Generate invalidation pattern for a cube
* Used when cube data changes and all related cache entries need clearing
*
* @param cubeName - Name of the cube to invalidate
* @param keyPrefix - Cache key prefix
* @returns Glob pattern for cache invalidation
*/
export declare function getCubeInvalidationPattern(cubeName: string, keyPrefix?: string): string;
/**
* Derive SQL join type from semantic relationship
*/
export declare function getJoinType(relationship: string, override?: string): string;
/**
*