UNPKG

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
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; /** *