drizzle-cube
Version:
Drizzle ORM-first semantic layer with Cube.js compatibility. Type-safe analytics and dashboards with SQL injection protection.
1,573 lines • 448 kB
JavaScript
import { sql as n, eq as x, and as p, StringChunk as xT, count as SE, sum as f, max as q, min as j, countDistinct as vT, SQL as QT, or as IE, isNotNull as yE, ne as KE, isNull as $E, notInArray as gE, inArray as OE, lt as NE, gt as tE, gte as $, lte as g, desc as ZT, asc as wE } from "drizzle-orm";
class fE {
/**
* Default implementation returns template unchanged
* Override in specific adapters for database-specific preprocessing
*/
preprocessCalculatedTemplate(E) {
return E;
}
/**
* Helper method to build pattern for string matching
* Can be overridden by specific adapters if needed
*/
buildPattern(E, T) {
switch (E) {
case "contains":
case "notContains":
return `%${T}%`;
case "startsWith":
return `${T}%`;
case "endsWith":
return `%${T}`;
default:
return T;
}
}
}
class qT extends fE {
getEngineType() {
return "postgres";
}
/**
* Build PostgreSQL time dimension using DATE_TRUNC function
* Extracted from executor.ts:649-670 and multi-cube-builder.ts:306-320
*/
buildTimeDimension(E, T) {
switch (E) {
case "year":
return n`DATE_TRUNC('year', ${T}::timestamp)`;
case "quarter":
return n`DATE_TRUNC('quarter', ${T}::timestamp)`;
case "month":
return n`DATE_TRUNC('month', ${T}::timestamp)`;
case "week":
return n`DATE_TRUNC('week', ${T}::timestamp)`;
case "day":
return n`DATE_TRUNC('day', ${T}::timestamp)::timestamp`;
case "hour":
return n`DATE_TRUNC('hour', ${T}::timestamp)`;
case "minute":
return n`DATE_TRUNC('minute', ${T}::timestamp)`;
case "second":
return n`DATE_TRUNC('second', ${T}::timestamp)`;
default:
return T;
}
}
/**
* Build PostgreSQL string matching conditions using ILIKE (case-insensitive)
* Extracted from executor.ts:807-813 and multi-cube-builder.ts:468-474
*/
buildStringCondition(E, T, A) {
switch (T) {
case "contains":
return n`${E} ILIKE ${`%${A}%`}`;
case "notContains":
return n`${E} NOT ILIKE ${`%${A}%`}`;
case "startsWith":
return n`${E} ILIKE ${`${A}%`}`;
case "endsWith":
return n`${E} ILIKE ${`%${A}`}`;
case "like":
return n`${E} LIKE ${A}`;
case "notLike":
return n`${E} NOT LIKE ${A}`;
case "ilike":
return n`${E} ILIKE ${A}`;
case "regex":
return n`${E} ~* ${A}`;
case "notRegex":
return n`${E} !~* ${A}`;
default:
throw new Error(`Unsupported string operator: ${T}`);
}
}
/**
* Build PostgreSQL type casting using :: syntax
* Extracted from various locations where ::timestamp was used
*/
castToType(E, T) {
switch (T) {
case "timestamp":
return n`${E}::timestamp`;
case "decimal":
return n`${E}::decimal`;
case "integer":
return n`${E}::integer`;
default:
throw new Error(`Unsupported cast type: ${T}`);
}
}
/**
* Build PostgreSQL AVG aggregation with COALESCE for NULL handling
* PostgreSQL AVG returns NULL for empty sets, so we use COALESCE for consistent behavior
* Extracted from multi-cube-builder.ts:284
*/
buildAvg(E) {
return n`COALESCE(AVG(${E}), 0)`;
}
/**
* Build PostgreSQL CASE WHEN conditional expression
*/
buildCaseWhen(E, T) {
const A = E.map((e) => n`WHEN ${e.when} THEN ${e.then}`).reduce((e, S) => n`${e} ${S}`);
return T !== void 0 ? n`CASE ${A} ELSE ${T} END` : n`CASE ${A} END`;
}
/**
* Build PostgreSQL boolean literal
* PostgreSQL uses TRUE/FALSE keywords
*/
buildBooleanLiteral(E) {
return E ? n`TRUE` : n`FALSE`;
}
/**
* Convert filter values - PostgreSQL uses native types
* No conversion needed for PostgreSQL
*/
convertFilterValue(E) {
return E;
}
/**
* Prepare date value for PostgreSQL
* PostgreSQL accepts Date objects directly
*/
prepareDateValue(E) {
return E;
}
/**
* PostgreSQL stores timestamps as native timestamp types
*/
isTimestampInteger() {
return !1;
}
/**
* PostgreSQL time dimensions already return proper values
* No conversion needed
*/
convertTimeDimensionResult(E) {
return E;
}
}
class HT extends fE {
getEngineType() {
return "mysql";
}
/**
* Build MySQL time dimension using DATE_FORMAT function
* MySQL equivalent to PostgreSQL's DATE_TRUNC
*/
buildTimeDimension(E, T) {
const A = {
year: "%Y-01-01 00:00:00",
quarter: "%Y-%q-01 00:00:00",
// %q gives quarter (1,2,3,4), but we need to map this properly
month: "%Y-%m-01 00:00:00",
week: "%Y-%u-01 00:00:00",
// %u gives week of year
day: "%Y-%m-%d 00:00:00",
hour: "%Y-%m-%d %H:00:00",
minute: "%Y-%m-%d %H:%i:00",
second: "%Y-%m-%d %H:%i:%s"
};
switch (E) {
case "quarter":
return n`DATE_ADD(MAKEDATE(YEAR(${T}), 1), INTERVAL (QUARTER(${T}) - 1) * 3 MONTH)`;
case "week":
return n`DATE_SUB(${T}, INTERVAL WEEKDAY(${T}) DAY)`;
default:
const e = A[E];
return e ? n`STR_TO_DATE(DATE_FORMAT(${T}, ${e}), '%Y-%m-%d %H:%i:%s')` : T;
}
}
/**
* Build MySQL string matching conditions using LIKE
* MySQL LIKE is case-insensitive by default (depending on collation)
* For guaranteed case-insensitive matching, we use LOWER() functions
*/
buildStringCondition(E, T, A) {
switch (T) {
case "contains":
return n`LOWER(${E}) LIKE ${`%${A.toLowerCase()}%`}`;
case "notContains":
return n`LOWER(${E}) NOT LIKE ${`%${A.toLowerCase()}%`}`;
case "startsWith":
return n`LOWER(${E}) LIKE ${`${A.toLowerCase()}%`}`;
case "endsWith":
return n`LOWER(${E}) LIKE ${`%${A.toLowerCase()}`}`;
case "like":
return n`${E} LIKE ${A}`;
case "notLike":
return n`${E} NOT LIKE ${A}`;
case "ilike":
return n`LOWER(${E}) LIKE ${A.toLowerCase()}`;
case "regex":
return n`${E} REGEXP ${A}`;
case "notRegex":
return n`${E} NOT REGEXP ${A}`;
default:
throw new Error(`Unsupported string operator: ${T}`);
}
}
/**
* Build MySQL type casting using CAST() function
* MySQL equivalent to PostgreSQL's :: casting syntax
*/
castToType(E, T) {
switch (T) {
case "timestamp":
return n`CAST(${E} AS DATETIME)`;
case "decimal":
return n`CAST(${E} AS DECIMAL(10,2))`;
case "integer":
return n`CAST(${E} AS SIGNED INTEGER)`;
default:
throw new Error(`Unsupported cast type: ${T}`);
}
}
/**
* Build MySQL AVG aggregation with IFNULL for NULL handling
* MySQL AVG returns NULL for empty sets, using IFNULL for consistency
*/
buildAvg(E) {
return n`IFNULL(AVG(${E}), 0)`;
}
/**
* Build MySQL CASE WHEN conditional expression
*/
buildCaseWhen(E, T) {
const A = E.map((e) => n`WHEN ${e.when} THEN ${e.then}`).reduce((e, S) => n`${e} ${S}`);
return T !== void 0 ? n`CASE ${A} ELSE ${T} END` : n`CASE ${A} END`;
}
/**
* Build MySQL boolean literal
* MySQL uses TRUE/FALSE keywords (equivalent to 1/0)
*/
buildBooleanLiteral(E) {
return E ? n`TRUE` : n`FALSE`;
}
/**
* Convert filter values - MySQL uses native types
* No conversion needed for MySQL
*/
convertFilterValue(E) {
return E;
}
/**
* Prepare date value for MySQL
* MySQL accepts Date objects directly
*/
prepareDateValue(E) {
return E;
}
/**
* MySQL stores timestamps as native timestamp types
*/
isTimestampInteger() {
return !1;
}
/**
* MySQL time dimensions already return proper values
* No conversion needed
*/
convertTimeDimensionResult(E) {
return E;
}
}
class jT extends fE {
getEngineType() {
return "sqlite";
}
/**
* Build SQLite time dimension using date/datetime functions with modifiers
* For integer timestamp columns (milliseconds), first convert to datetime
* SQLite doesn't have DATE_TRUNC like PostgreSQL, so we use strftime and date modifiers
* Returns datetime strings for consistency with other databases
*/
buildTimeDimension(E, T) {
switch (E) {
case "year":
return n`datetime(${T}, 'unixepoch', 'start of year')`;
case "quarter":
const A = n`datetime(${T}, 'unixepoch')`;
return n`datetime(${A}, 'start of year',
'+' || (((CAST(strftime('%m', ${A}) AS INTEGER) - 1) / 3) * 3) || ' months')`;
case "month":
return n`datetime(${T}, 'unixepoch', 'start of month')`;
case "week":
return n`date(datetime(${T}, 'unixepoch'), 'weekday 1', '-6 days')`;
case "day":
return n`datetime(${T}, 'unixepoch', 'start of day')`;
case "hour":
const e = n`datetime(${T}, 'unixepoch')`;
return n`datetime(strftime('%Y-%m-%d %H:00:00', ${e}))`;
case "minute":
const S = n`datetime(${T}, 'unixepoch')`;
return n`datetime(strftime('%Y-%m-%d %H:%M:00', ${S}))`;
case "second":
const I = n`datetime(${T}, 'unixepoch')`;
return n`datetime(strftime('%Y-%m-%d %H:%M:%S', ${I}))`;
default:
return n`datetime(${T}, 'unixepoch')`;
}
}
/**
* Build SQLite string matching conditions using LOWER() + LIKE for case-insensitive matching
* SQLite LIKE is case-insensitive by default, but LOWER() ensures consistency
*/
buildStringCondition(E, T, A) {
switch (T) {
case "contains":
return n`LOWER(${E}) LIKE ${`%${A.toLowerCase()}%`}`;
case "notContains":
return n`LOWER(${E}) NOT LIKE ${`%${A.toLowerCase()}%`}`;
case "startsWith":
return n`LOWER(${E}) LIKE ${`${A.toLowerCase()}%`}`;
case "endsWith":
return n`LOWER(${E}) LIKE ${`%${A.toLowerCase()}`}`;
case "like":
return n`${E} LIKE ${A}`;
case "notLike":
return n`${E} NOT LIKE ${A}`;
case "ilike":
return n`LOWER(${E}) LIKE ${A.toLowerCase()}`;
case "regex":
return n`${E} GLOB ${A}`;
case "notRegex":
return n`${E} NOT GLOB ${A}`;
default:
throw new Error(`Unsupported string operator: ${T}`);
}
}
/**
* Build SQLite type casting using CAST() function
* SQLite has dynamic typing but supports CAST for consistency
*/
castToType(E, T) {
switch (T) {
case "timestamp":
return n`datetime(${E} / 1000, 'unixepoch')`;
case "decimal":
return n`CAST(${E} AS REAL)`;
case "integer":
return n`CAST(${E} AS INTEGER)`;
default:
throw new Error(`Unsupported cast type: ${T}`);
}
}
/**
* Build SQLite AVG aggregation with IFNULL for NULL handling
* SQLite AVG returns NULL for empty sets, using IFNULL for consistency
*/
buildAvg(E) {
return n`IFNULL(AVG(${E}), 0)`;
}
/**
* Build SQLite CASE WHEN conditional expression
*/
buildCaseWhen(E, T) {
const A = E.map((e) => e.then && typeof e.then == "object" && (e.then.queryChunks || e.then._ || e.then.sql) ? n`WHEN ${e.when} THEN ${n.raw("(")}${e.then}${n.raw(")")}` : n`WHEN ${e.when} THEN ${e.then}`).reduce((e, S) => n`${e} ${S}`);
return T !== void 0 ? T && typeof T == "object" && (T.queryChunks || T._ || T.sql) ? n`CASE ${A} ELSE ${n.raw("(")}${T}${n.raw(")")} END` : n`CASE ${A} ELSE ${T} END` : n`CASE ${A} END`;
}
/**
* Build SQLite boolean literal
* SQLite uses 1/0 for true/false
*/
buildBooleanLiteral(E) {
return E ? n`1` : n`0`;
}
/**
* Preprocess calculated measure templates for SQLite-specific handling
*
* SQLite performs integer division by default (5/2 = 2 instead of 2.5).
* This method wraps division operands with CAST to REAL to ensure float division.
*
* Pattern matched: {measure1} / {measure2} or {measure1} / NULLIF({measure2}, 0)
* Transforms to: CAST({measure1} AS REAL) / ...
*
* @param calculatedSql - Template string with {member} references
* @returns Preprocessed template with CAST for division operations
*/
preprocessCalculatedTemplate(E) {
const T = /(\{[^}]+\})\s*\/\s*/g;
return E.replace(T, (A, e) => `${e.replace(/\{([^}]+)\}/, "CAST({$1} AS REAL)")} / `);
}
/**
* Convert filter values to SQLite-compatible types
* SQLite doesn't support boolean types - convert boolean to integer (1/0)
* Convert Date objects to milliseconds for integer timestamp columns
*/
convertFilterValue(E) {
return typeof E == "boolean" ? E ? 1 : 0 : E instanceof Date ? E.getTime() : Array.isArray(E) ? E.map((T) => this.convertFilterValue(T)) : E;
}
/**
* Prepare date value for SQLite integer timestamp storage
* Convert Date objects to milliseconds (Unix timestamp * 1000)
*/
prepareDateValue(E) {
if (!(E instanceof Date)) {
if (typeof E == "number") return E;
if (typeof E == "string") return new Date(E).getTime();
throw new Error(`prepareDateValue expects a Date object, got ${typeof E}`);
}
return E.getTime();
}
/**
* SQLite stores timestamps as integers (milliseconds)
*/
isTimestampInteger() {
return !0;
}
/**
* Convert SQLite time dimension results back to Date objects
* SQLite time dimensions return datetime strings, but clients expect Date objects
*/
convertTimeDimensionResult(E) {
return E;
}
}
class kT extends HT {
getEngineType() {
return "singlestore";
}
// SingleStore inherits most MySQL functionality
// Override methods here only if SingleStore-specific behavior is needed
// Note: SingleStore has some known limitations:
// - ORDER BY and LIMIT cannot be chained together in some contexts
// - Nested selects with aggregation functions are not supported
// - Serial column type only assures uniqueness (tests may need ORDER BY)
// These limitations are typically handled at the query building level
// rather than in the adapter, but can be addressed here if needed
}
function zT(R) {
switch (R) {
case "postgres":
return new qT();
case "mysql":
return new HT();
case "sqlite":
return new jT();
case "singlestore":
return new kT();
default:
throw new Error(`Unsupported database engine: ${R}`);
}
}
class VE {
constructor(E, T, A) {
this.db = E, this.schema = T;
const e = A || this.getEngineType();
this.databaseAdapter = zT(e);
}
databaseAdapter;
}
class ER extends VE {
async execute(E, T) {
if (E && typeof E == "object" && typeof E.execute == "function") {
const e = await E.execute();
return Array.isArray(e) ? e.map((S) => this.convertNumericFields(S, T)) : e;
}
if (!this.db.execute)
throw new Error("PostgreSQL database instance must have an execute method");
const A = await this.db.execute(E);
return Array.isArray(A) ? A.map((e) => this.convertNumericFields(e, T)) : A;
}
/**
* Convert numeric string fields to numbers (only for measure fields)
*/
convertNumericFields(E, T) {
if (!E || typeof E != "object") return E;
const A = {};
for (const [e, S] of Object.entries(E))
T && T.includes(e) ? A[e] = this.coerceToNumber(S) : A[e] = S;
return A;
}
/**
* Coerce a value to a number if it represents a numeric type
*/
coerceToNumber(E) {
if (E == null || typeof E == "number") return E;
if (typeof E == "bigint") return Number(E);
if (E && typeof E == "object") {
if (typeof E.toString == "function") {
const T = E.toString();
if (/^-?\d+(\.\d+)?$/.test(T))
return T.includes(".") ? parseFloat(T) : parseInt(T, 10);
}
if (E.constructor?.name === "Numeric" || E.constructor?.name === "Decimal" || "digits" in E || "sign" in E) {
const T = E.toString();
return parseFloat(T);
}
return E;
}
if (typeof E == "string") {
if (/^-?\d+(\.\d+)?$/.test(E))
return E.includes(".") ? parseFloat(E) : parseInt(E, 10);
if (!isNaN(parseFloat(E)) && isFinite(parseFloat(E)))
return parseFloat(E);
}
return E;
}
getEngineType() {
return "postgres";
}
}
function JE(R, E) {
return new ER(R, E, "postgres");
}
class BT extends VE {
async execute(E, T) {
if (E && typeof E == "object" && typeof E.execute == "function") {
const e = await E.execute();
return Array.isArray(e) ? e.map((S) => this.convertNumericFields(S, T)) : e;
}
if (!this.db.execute)
throw new Error("MySQL database instance must have an execute method");
const A = await this.db.execute(E);
return Array.isArray(A) ? A.map((e) => this.convertNumericFields(e, T)) : A;
}
/**
* Convert numeric string fields to numbers (measure fields + numeric dimensions)
*/
convertNumericFields(E, T) {
if (!E || typeof E != "object") return E;
const A = {};
for (const [e, S] of Object.entries(E))
T && T.includes(e) ? A[e] = this.coerceToNumber(S) : A[e] = S;
return A;
}
/**
* Coerce a value to a number if it represents a numeric type
*/
coerceToNumber(E) {
if (E == null || typeof E == "number") return E;
if (typeof E == "string") {
if (/^-?\d+(\.\d+)?$/.test(E))
return E.includes(".") ? parseFloat(E) : parseInt(E, 10);
if (!isNaN(parseFloat(E)) && isFinite(parseFloat(E)))
return parseFloat(E);
}
return E;
}
getEngineType() {
return "mysql";
}
}
function TR(R, E) {
return new BT(R, E, "mysql");
}
class RR extends VE {
async execute(E, T) {
if (E && typeof E == "object" && typeof E.execute == "function") {
const A = await E.execute();
return Array.isArray(A) ? A.map((e) => this.convertNumericFields(e, T)) : A;
}
try {
if (this.db.all) {
const A = this.db.all(E);
return Array.isArray(A) ? A.map((e) => this.convertNumericFields(e, T)) : A;
} else {
if (this.db.run)
return this.db.run(E);
throw new Error("SQLite database instance must have an all() or run() method");
}
} catch (A) {
throw new Error(`SQLite execution failed: ${A instanceof Error ? A.message : "Unknown error"}`);
}
}
/**
* Convert numeric string fields to numbers (only for measure fields)
*/
convertNumericFields(E, T) {
if (!E || typeof E != "object") return E;
const A = {};
for (const [e, S] of Object.entries(E))
T && T.includes(e) ? A[e] = this.coerceToNumber(S) : A[e] = S;
return A;
}
/**
* Coerce a value to a number if it represents a numeric type
*/
coerceToNumber(E) {
if (E == null || typeof E == "number") return E;
if (typeof E == "string") {
if (/^-?\d+(\.\d+)?$/.test(E))
return E.includes(".") ? parseFloat(E) : parseInt(E, 10);
if (!isNaN(parseFloat(E)) && isFinite(parseFloat(E)))
return parseFloat(E);
}
return E;
}
getEngineType() {
return "sqlite";
}
}
function xE(R, E) {
return new RR(R, E, "sqlite");
}
class AR extends BT {
getEngineType() {
return "singlestore";
}
// SingleStore-specific optimizations can be added here if needed
// For now, we inherit all behavior from MySQLExecutor since
// SingleStore is largely MySQL-compatible
}
function eR(R, E) {
return new AR(R, E);
}
function vE(R, E, T) {
if (T)
switch (T) {
case "postgres":
return JE(R, E);
case "mysql":
return TR(R, E);
case "sqlite":
return xE(R, E);
case "singlestore":
return eR(R, E);
}
if (R.all && R.run)
return xE(R, E);
if (R.execute)
return JE(R, E);
throw new Error("Unable to determine database engine type. Please specify engineType parameter.");
}
function QE(R) {
return typeof R == "function" ? R() : R;
}
function dT(R, E) {
if (E) return E;
switch (R) {
case "belongsTo":
return "inner";
// FK should exist
case "hasOne":
return "left";
// Parent may have no child
case "hasMany":
return "left";
// Parent may have no children
case "belongsToMany":
return "left";
// Many-to-many through junction table
default:
return "left";
}
}
function SR(R) {
return R && typeof R == "object" ? n`${n`${R}`}` : R;
}
function X(R, E) {
const T = typeof R == "function" ? R(E) : R;
return SR(T);
}
function zI(R, E, T) {
return {
...R,
cubes: E,
currentCube: T
};
}
function EO(R, E) {
return {
name: R,
...E
};
}
function IR(R, E) {
if (R.relationship !== "belongsToMany" || !R.through)
throw new Error("expandBelongsToManyJoin can only be called on belongsToMany relationships with through configuration");
const { table: T, sourceKey: A, targetKey: e, securitySql: S } = R.through, I = [];
for (const N of A) {
const s = N.as || x;
I.push(s(N.source, N.target));
}
const t = [];
for (const N of e) {
const s = N.as || x;
t.push(s(N.source, N.target));
}
let C;
if (S) {
const N = S(E);
C = Array.isArray(N) ? N : [N];
}
const O = dT("belongsToMany", R.sqlJoinType);
return {
junctionJoins: [
{
joinType: O,
table: T,
condition: p(...I)
},
{
joinType: O,
table: T,
// This will be replaced with target cube table in query planner
condition: p(...t)
}
],
junctionSecurityConditions: C
};
}
class b {
dependencyGraph;
cubes;
constructor(E) {
this.cubes = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]), this.dependencyGraph = /* @__PURE__ */ new Map();
}
/**
* Extract {member} references from calculatedSql template
* Supports both {measure} and {Cube.measure} syntax
*
* @param calculatedSql - Template string with {member} references
* @returns Array of dependency information
*/
extractDependencies(E) {
const T = /\{([^}]+)\}/g, A = E.matchAll(T), e = [];
for (const S of A) {
const I = S[1].trim();
if (I.includes(".")) {
const [t, C] = I.split(".");
e.push({
measureName: I,
cubeName: t.trim(),
fieldName: C.trim()
});
} else
e.push({
measureName: I,
cubeName: null,
fieldName: I
});
}
return e;
}
/**
* Build dependency graph for all calculated measures in a cube
*
* @param cube - The cube containing measures
*/
buildGraph(E) {
for (const [T, A] of Object.entries(E.measures))
if (A.type === "calculated" && A.calculatedSql) {
const e = `${E.name}.${T}`, S = this.extractDependencies(A.calculatedSql), I = /* @__PURE__ */ new Set();
for (const t of S) {
const O = `${t.cubeName || E.name}.${t.fieldName}`;
I.add(O);
}
this.dependencyGraph.set(e, {
id: e,
dependencies: I,
inDegree: 0
});
}
this.calculateInDegrees();
}
/**
* Build dependency graph for multiple cubes
*
* @param cubes - Map of cubes to analyze
*/
buildGraphForMultipleCubes(E) {
for (const T of E.values())
this.buildGraph(T);
}
/**
* Calculate in-degree for each node (number of measures depending on it)
*/
calculateInDegrees() {
for (const E of this.dependencyGraph.values())
E.inDegree = 0;
for (const E of this.dependencyGraph.values())
for (const T of E.dependencies) {
const A = this.dependencyGraph.get(T);
A && A.inDegree++;
}
}
/**
* Perform topological sort using Kahn's algorithm
* Returns measures in dependency order (dependencies first)
*
* @param measureNames - List of measure names to sort
* @returns Sorted array of measure names
* @throws Error if circular dependency detected
*/
topologicalSort(E) {
const T = /* @__PURE__ */ new Map(), A = [], e = [];
for (const S of E) {
const I = this.dependencyGraph.get(S);
I && T.set(S, {
id: I.id,
dependencies: new Set(I.dependencies),
inDegree: 0
// Will recalculate below
});
}
for (const S of T.values()) {
let I = 0;
for (const t of S.dependencies)
T.has(t) && I++;
S.inDegree = I;
}
for (const [S, I] of T)
I.inDegree === 0 && A.push(S);
for (; A.length > 0; ) {
const S = A.shift();
e.push(S);
for (const [I, t] of T)
t.dependencies.has(S) && (t.inDegree--, t.inDegree === 0 && A.push(I));
}
if (e.length < T.size) {
const S = this.detectCycle();
throw new Error(
`Circular dependency detected in calculated measures: ${S ? S.join(" -> ") : "unknown cycle"}`
);
}
return e;
}
/**
* Detect circular dependencies using DFS
* Returns the cycle path if found, null otherwise
*
* @returns Array representing the cycle, or null
*/
detectCycle() {
const E = /* @__PURE__ */ new Set(), T = /* @__PURE__ */ new Set(), A = [];
for (const e of this.dependencyGraph.keys())
if (!E.has(e)) {
const S = this.dfs(e, E, T, A);
if (S)
return S;
}
return null;
}
/**
* DFS helper for cycle detection
*/
dfs(E, T, A, e) {
T.add(E), A.add(E), e.push(E);
const S = this.dependencyGraph.get(E);
if (!S)
return e.pop(), A.delete(E), null;
for (const I of S.dependencies)
if (T.has(I)) {
if (A.has(I)) {
const t = e.indexOf(I);
return [...e.slice(t), I];
}
} else {
const t = this.dfs(I, T, A, e);
if (t)
return t;
}
return e.pop(), A.delete(E), null;
}
/**
* Get all dependencies for a specific measure (direct and transitive)
*
* @param measureName - Full measure name (e.g., "Cube.measure")
* @returns Set of all dependency measure names
*/
getAllDependencies(E) {
const T = /* @__PURE__ */ new Set(), A = /* @__PURE__ */ new Set(), e = (S) => {
if (A.has(S)) return;
A.add(S);
const I = this.dependencyGraph.get(S);
if (I)
for (const t of I.dependencies)
T.add(t), e(t);
};
return e(E), T;
}
/**
* Validate that all dependencies exist
*
* @param cube - The cube to validate
* @throws Error if dependencies are missing
*/
validateDependencies(E) {
for (const [T, A] of Object.entries(E.measures))
if (A.type === "calculated" && A.calculatedSql) {
const e = this.extractDependencies(A.calculatedSql);
for (const S of e) {
const I = S.cubeName || E.name, t = this.cubes.get(I);
if (!t)
throw new Error(
`Calculated measure '${E.name}.${T}' references unknown cube '${I}'`
);
if (!t.measures[S.fieldName])
throw new Error(
`Calculated measure '${E.name}.${T}' references unknown measure '${S.measureName}'`
);
if (I === E.name && S.fieldName === T)
throw new Error(
`Calculated measure '${E.name}.${T}' cannot reference itself`
);
}
}
}
/**
* Auto-populate dependencies array for calculated measures
* Updates the measure objects with detected dependencies
*
* @param cube - The cube to update
*/
populateDependencies(E) {
for (const [, T] of Object.entries(E.measures))
if (T.type === "calculated" && T.calculatedSql && !T.dependencies) {
const A = this.extractDependencies(T.calculatedSql);
T.dependencies = A.map((e) => e.measureName);
}
}
/**
* Check if a measure is a calculated measure
*
* @param measure - The measure to check
* @returns True if the measure is calculated
*/
static isCalculatedMeasure(E) {
return E.type === "calculated" && !!E.calculatedSql;
}
}
function NR(R, E) {
const { cube: T, allCubes: A, resolvedMeasures: e } = E, S = WE(R), I = /* @__PURE__ */ new Map();
for (const s of S) {
const { originalRef: r, cubeName: L, fieldName: i } = s, o = L || T.name;
if (!A.get(o))
throw new Error(
`Cannot substitute {${r}}: cube '${o}' not found`
);
const P = `${o}.${i}`, c = e.get(P);
if (!c)
throw new Error(
`Cannot substitute {${r}}: measure '${P}' not resolved yet. Ensure measures are resolved in dependency order.`
);
const M = c(), u = n`${M}`;
I.set(r, u);
}
const t = [], C = [];
let O = 0;
for (const s of S) {
const r = `{${s.originalRef}}`, L = R.indexOf(r, O);
if (L >= 0) {
t.push(R.substring(O, L));
const i = I.get(s.originalRef);
i && C.push(i), O = L + r.length;
}
}
if (t.push(R.substring(O)), C.length === 0)
return n.raw(R);
const N = [];
for (let s = 0; s < t.length; s++)
t[s] && N.push(new xT(t[s])), s < C.length && N.push(C[s]);
return n.join(N, n.raw(""));
}
function WE(R) {
const E = /\{([^}]+)\}/g, T = R.matchAll(E), A = [];
for (const e of T) {
const S = e[1].trim();
if (S.includes(".")) {
const [I, t] = S.split(".").map((C) => C.trim());
A.push({
originalRef: S,
cubeName: I,
fieldName: t
});
} else
A.push({
originalRef: S,
cubeName: null,
fieldName: S
});
}
return A;
}
function tR(R) {
const E = [];
let T = 0;
for (let I = 0; I < R.length; I++)
if (R[I] === "{")
T++;
else if (R[I] === "}" && (T--, T < 0)) {
E.push(`Unmatched closing brace at position ${I}`);
break;
}
T > 0 && E.push("Unmatched opening brace in template"), /\{\s*\}/.test(R) && E.push("Empty member reference {} found in template"), /\{[^}]*\{/.test(R) && E.push("Nested braces are not allowed in member references");
const S = WE(R);
for (const I of S) {
const t = I.cubeName ? `${I.cubeName}.${I.fieldName}` : I.fieldName;
/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(t) || E.push(
`Invalid member reference {${I.originalRef}}: must start with letter or underscore, and contain only letters, numbers, underscores, and dots`
), t.split(".").length > 2 && E.push(
`Invalid member reference {${I.originalRef}}: only one dot allowed (Cube.measure format)`
);
}
return {
isValid: E.length === 0,
errors: E
};
}
function sE(R, E) {
const T = WE(R), A = /* @__PURE__ */ new Set();
for (const e of T) {
const I = `${e.cubeName || E}.${e.fieldName}`;
A.add(I);
}
return Array.from(A);
}
class sR {
constructor(E) {
this.databaseAdapter = E;
}
/**
* Build resolvedMeasures map for a set of measures
* This centralizes the logic for building both regular and calculated measures
* in dependency order, avoiding duplication across main queries and CTEs
*
* @param measureNames - Array of measure names to resolve (e.g., ["Employees.count", "Employees.activePercentage"])
* @param cubeMap - Map of all cubes involved in the query
* @param context - Query context with database and security context
* @param customMeasureBuilder - Optional function to override how individual measures are built
* @returns Map of measure names to SQL builder functions
*/
buildResolvedMeasures(E, T, A, e) {
const S = /* @__PURE__ */ new Map(), I = [], t = [], C = new Set(E), O = new b(T);
for (const N of T.values())
O.buildGraph(N);
for (const N of E) {
const [s, r] = N.split("."), L = T.get(s);
if (L && L.measures && L.measures[r]) {
const i = L.measures[r];
b.isCalculatedMeasure(i) ? (t.push(N), sE(i.calculatedSql, s).forEach((P) => C.add(P)), O.getAllDependencies(N).forEach((P) => {
const [c, M] = P.split("."), u = T.get(c);
if (u && u.measures[M]) {
const H = u.measures[M];
b.isCalculatedMeasure(H) && sE(H.calculatedSql, c).forEach((Y) => C.add(Y));
}
})) : I.push(N);
}
}
for (const N of C) {
const [s, r] = N.split("."), L = T.get(s);
if (L && L.measures && L.measures[r]) {
const i = L.measures[r];
b.isCalculatedMeasure(i) ? t.includes(N) || t.push(N) : I.includes(N) || I.push(N);
}
}
for (const N of I) {
const [s, r] = N.split("."), L = T.get(s), i = L.measures[r];
if (e) {
const o = e(N, i, L);
S.set(N, () => o);
} else
S.set(N, () => this.buildMeasureExpression(i, A));
}
if (t.length > 0) {
const N = O.topologicalSort(t);
for (const s of N) {
const [r, L] = s.split("."), i = T.get(r), o = i.measures[L];
S.set(s, () => this.buildCalculatedMeasure(
o,
i,
T,
S,
A
));
}
}
return S;
}
/**
* Build dynamic selections for measures, dimensions, and time dimensions
* Works for both single and multi-cube queries
* Handles calculated measures with dependency resolution
*/
buildSelections(E, T, A) {
const e = {}, S = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]);
if (T.dimensions)
for (const I of T.dimensions) {
const [t, C] = I.split("."), O = S.get(t);
if (O && O.dimensions && O.dimensions[C]) {
const N = O.dimensions[C], s = X(N.sql, A);
e[I] = n`${s}`.as(I);
}
}
if (T.measures) {
const I = this.buildResolvedMeasures(
T.measures,
S,
A
);
for (const t of T.measures) {
const C = I.get(t);
if (C) {
const O = C();
e[t] = n`${O}`.as(t);
}
}
}
if (T.timeDimensions)
for (const I of T.timeDimensions) {
const [t, C] = I.dimension.split("."), O = S.get(t);
if (O && O.dimensions && O.dimensions[C]) {
const N = O.dimensions[C], s = this.buildTimeDimensionExpression(
N.sql,
I.granularity,
A
);
e[I.dimension] = n`${s}`.as(I.dimension);
}
}
return Object.keys(e).length === 0 && (e.count = SE()), e;
}
/**
* Build calculated measure expression by substituting {member} references
* with resolved SQL expressions
*/
buildCalculatedMeasure(E, T, A, e, S) {
if (!E.calculatedSql)
throw new Error(
`Calculated measure '${T.name}.${E.name}' missing calculatedSql property`
);
const I = this.databaseAdapter.preprocessCalculatedTemplate(E.calculatedSql);
return NR(I, {
cube: T,
allCubes: A,
resolvedMeasures: e
});
}
/**
* Build resolved measures map for a calculated measure from CTE columns
* This handles re-aggregating pre-aggregated CTE columns for calculated measures
*
* IMPORTANT: For calculated measures in CTEs, we cannot sum/avg pre-computed ratios.
* We must recalculate from the base measures that were pre-aggregated in the CTE.
*
* @param measure - The calculated measure to build
* @param cube - The cube containing this measure
* @param cteInfo - CTE metadata (alias, measures, cube reference)
* @param allCubes - Map of all cubes in the query
* @param context - Query context
* @returns SQL expression for the calculated measure using CTE column references
*/
buildCTECalculatedMeasure(E, T, A, e, S) {
if (!E.calculatedSql)
throw new Error(
`Calculated measure '${T.name}.${E.name || "unknown"}' missing calculatedSql property`
);
const I = /* @__PURE__ */ new Map(), t = sE(E.calculatedSql, T.name);
for (const C of t) {
const [O, N] = C.split("."), s = e.get(O);
if (s && s.measures[N]) {
const r = s.measures[N];
if (A.measures.includes(C)) {
const L = n`${n.identifier(A.cteAlias)}.${n.identifier(N)}`;
let i;
switch (r.type) {
case "count":
case "countDistinct":
case "sum":
i = f(L);
break;
case "avg":
i = this.databaseAdapter.buildAvg(L);
break;
case "min":
i = j(L);
break;
case "max":
i = q(L);
break;
case "number":
i = f(L);
break;
default:
i = f(L);
}
I.set(C, () => i);
}
}
}
return this.buildCalculatedMeasure(
E,
T,
e,
I,
S
);
}
/**
* Build measure expression for HAVING clause, handling CTE references correctly
*/
buildHavingMeasureExpression(E, T, A, e, S) {
if (S && S.preAggregationCTEs) {
const I = S.preAggregationCTEs.find((t) => t.cube.name === E);
if (I && I.measures.includes(`${E}.${T}`))
if (A.type === "calculated" && A.calculatedSql) {
const t = S.primaryCube.name === E ? S.primaryCube : S.joinCubes?.find((O) => O.cube.name === E)?.cube;
if (!t)
throw new Error(`Cube ${E} not found in query plan`);
const C = /* @__PURE__ */ new Map([[S.primaryCube.name, S.primaryCube]]);
if (S.joinCubes)
for (const O of S.joinCubes)
C.set(O.cube.name, O.cube);
return this.buildCTECalculatedMeasure(
A,
t,
I,
C,
e
);
} else {
const t = n`${n.identifier(I.cteAlias)}.${n.identifier(T)}`;
switch (A.type) {
case "count":
case "countDistinct":
case "sum":
return f(t);
case "avg":
return this.databaseAdapter.buildAvg(t);
case "min":
return j(t);
case "max":
return q(t);
case "number":
return f(t);
default:
return f(t);
}
}
}
return this.buildMeasureExpression(A, e);
}
/**
* Build measure expression with aggregation and filters
* Note: This should NOT be called for calculated measures
*/
buildMeasureExpression(E, T) {
if (E.type === "calculated")
throw new Error(
`Cannot build calculated measure '${E.name}' directly. Use buildCalculatedMeasure instead.`
);
if (!E.sql)
throw new Error(
`Measure '${E.name}' of type '${E.type}' is missing required 'sql' property. Only calculated measures can omit 'sql'.`
);
let A = X(E.sql, T);
if (E.filters && E.filters.length > 0) {
const e = E.filters.map((S) => {
const I = S(T);
return I ? n`(${I})` : void 0;
}).filter(Boolean);
if (e.length > 0) {
const S = e.length === 1 ? e[0] : p(...e);
A = this.databaseAdapter.buildCaseWhen([
{ when: S, then: A }
]);
}
}
switch (E.type) {
case "count":
return SE(A);
case "countDistinct":
return vT(A);
case "sum":
return f(A);
case "avg":
return this.databaseAdapter.buildAvg(A);
case "min":
return j(A);
case "max":
return q(A);
case "number":
return A;
default:
return SE(A);
}
}
/**
* Build time dimension expression with granularity using database adapter
*/
buildTimeDimensionExpression(E, T, A) {
const e = X(E, A);
return T ? this.databaseAdapter.buildTimeDimension(T, e) : e instanceof QT ? e : n`${e}`;
}
/**
* Build WHERE conditions from semantic query filters (dimensions only)
* Works for both single and multi-cube queries
*/
buildWhereConditions(E, T, A, e) {
const S = [], I = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]);
if (T.filters && T.filters.length > 0)
for (const t of T.filters) {
const C = this.processFilter(t, I, A, "where", e);
C && S.push(C);
}
if (T.timeDimensions)
for (const t of T.timeDimensions) {
const [C, O] = t.dimension.split("."), N = I.get(C);
if (N && N.dimensions[O] && t.dateRange) {
if (e?.preAggregationCTEs && e.preAggregationCTEs.some((o) => o.cube.name === C))
continue;
const s = N.dimensions[O], r = X(s.sql, A), L = this.buildDateRangeCondition(r, t.dateRange);
L && S.push(L);
}
}
return S;
}
/**
* Build HAVING conditions from semantic query filters (measures only)
* Works for both single and multi-cube queries
*/
buildHavingConditions(E, T, A, e) {
const S = [], I = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]);
if (T.filters && T.filters.length > 0)
for (const t of T.filters) {
const C = this.processFilter(t, I, A, "having", e);
C && S.push(C);
}
return S;
}
/**
* Process a single filter (basic or logical)
* @param filterType - 'where' for dimension filters, 'having' for measure filters
*/
processFilter(E, T, A, e, S) {
if ("and" in E || "or" in E) {
const L = E;
if (L.and) {
const i = L.and.map((o) => this.processFilter(o, T, A, e, S)).filter((o) => o !== null);
return i.length > 0 ? p(...i) : null;
}
if (L.or) {
const i = L.or.map((o) => this.processFilter(o, T, A, e, S)).filter((o) => o !== null);
return i.length > 0 ? IE(...i) : null;
}
}
const I = E, [t, C] = I.member.split("."), O = T.get(t);
if (!O) return null;
const N = O.dimensions[C], s = O.measures[C], r = N || s;
if (!r) return null;
if (e === "where" && N) {
if (S?.preAggregationCTEs && S.preAggregationCTEs.some((o) => o.cube.name === t))
return null;
const L = X(N.sql, A);
return this.buildFilterCondition(
L,
I.operator,
I.values,
r,
I.dateRange
);
} else {
if (e === "where" && s)
return null;
if (e === "having" && s) {
const L = this.buildHavingMeasureExpression(t, C, s, A, S);
return this.buildFilterCondition(
L,
I.operator,
I.values,
r,
I.dateRange
);
}
}
return null;
}
/**
* Build filter condition using Drizzle operators
*/
buildFilterCondition(E, T, A, e, S) {
if (S !== void 0) {
if (T !== "inDateRange")
throw new Error(
`dateRange can only be used with 'inDateRange' operator, but got '${T}'. Use explicit date values in the 'values' array for other date operators.`
);
if (e && e.type !== "time")
throw new Error(
`dateRange can only be used on time dimensions, but field '${e.name || "unknown"}' has type '${e.type}'`
);
return this.buildDateRangeCondition(E, S);
}
if (!A || A.length === 0)
return T === "equals" ? this.databaseAdapter.buildBooleanLiteral(!1) : null;
const I = A.filter((C) => !(C == null || C === "" || typeof C == "string" && C.includes("\0"))).map(this.databaseAdapter.convertFilterValue);
if (I.length === 0 && !["set", "notSet"].includes(T))
return T === "equals" ? this.databaseAdapter.buildBooleanLiteral(!1) : null;
const t = I[0];
switch (T) {
case "equals":
if (I.length > 1) {
if (e?.type === "time") {
const C = I.map((O) => this.normalizeDate(O) || O);
return OE(E, C);
}
return OE(E, I);
} else if (I.length === 1) {
const C = e?.type === "time" && this.normalizeDate(t) || t;
return x(E, C);
}
return this.databaseAdapter.buildBooleanLiteral(!1);
case "notEquals":
return I.length > 1 ? gE(E, I) : I.length === 1 ? KE(E, t) : null;
case "contains":
return this.databaseAdapter.buildStringCondition(E, "contains", t);
case "notContains":
return this.databaseAdapter.buildStringCondition(E, "notContains", t);
case "startsWith":
return this.databaseAdapter.buildStringCondition(E, "startsWith", t);
case "endsWith":
return this.databaseAdapter.buildStringCondition(E, "endsWith", t);
case "gt":
return tE(E, t);
case "gte":
return $(E, t);
case "lt":
return NE(E, t);
case "lte":
return g(E, t);
case "set":
return yE(E);
case "notSet":
return $E(E);
case "inDateRange":
if (I.length >= 2) {
const C = this.normalizeDate(I[0]);
let O = this.normalizeDate(I[1]);
if (C && O) {
const N = A[1];
if (typeof N == "string" && /^\d{4}-\d{2}-\d{2}$/.test(N.trim())) {
const s = typeof O == "number" ? new Date(O * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(O), r = new Date(s);
r.setUTCHours(23, 59, 59, 999), this.databaseAdapter.isTimestampInteger() ? O = this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(r.getTime() / 1e3) : r.getTime() : O = r.toISOString();
}
return p(
$(E, C),
g(E, O)
);
}
}
return null;
case "beforeDate": {
const C = this.normalizeDate(t);
return C ? NE(E, C) : null;
}
case "afterDate": {
const C = this.normalizeDate(t);
return C ? tE(E, C) : null;
}
case "between":
return I.length >= 2 ? p(
$(E, I[0]),
g(E, I[1])
) : null;
case "notBetween":
return I.length >= 2 ? IE(
NE(E, I[0]),
tE(E, I[1])
) : null;
case "in":
return I.length > 0 ? OE(E, I) : null;
case "notIn":
return I.length > 0 ? gE(E, I) : null;
case "like":
return this.databaseAdapter.buildStringCondition(E, "like", t);
case "notLike":
return this.databaseAdapter.buildStringCondition(E, "notLike", t);
case "ilike":
return this.databaseAdapter.buildStringCondition(E, "ilike", t);
case "regex":
return this.databaseAdapter.buildStringCondition(E, "regex", t);
case "notRegex":
return this.databaseAdapter.buildStringCondition(E, "notRegex", t);
case "isEmpty":
return IE(
$E(E),
x(E, "")
);
case "isNotEmpty":
return p(
yE(E),
KE(E, "")
);
default:
return null;
}
}
/**
* Build date range condition for time dimensions
*/
buildDateRangeCondition(E, T) {
if (!T) return null;
if (Array.isArray(T) && T.length >= 2) {
const A = this.normalizeDate(T[0]);
let e = this.normalizeDate(T[1]);
if (!A || !e) return null;
if (typeof T[1] == "string" && /^\d{4}-\d{2}-\d{2}$/.test(T[1].trim())) {
const S = typeof e == "number" ? new Date(e * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(e), I = new Date(S);
I.setUTCHours(23, 59, 59, 999), this.databaseAdapter.isTimestampInteger() ? e = this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(I.getTime() / 1e3) : I.getTime() : e = I.toISOString();
}
return p(
$(E, A),
g(E, e)
);
}
if (typeof T == "string") {
const A = this.parseRelativeDateRange(T);
if (A) {
let N, s;
return this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? (N = Math.floor(A.start.getTime() / 1e3), s = Math.floor(A.end.getTime() / 1e3)) : (N = A.start.getTime(), s = A.end.getTime()) : (N = A.start.toISOString(), s = A.end.toISOString()), p(
$(E, N),
g(E, s)
);
}
const e = this.normalizeDate(T);
if (!e) return null;
const S = typeof e == "number" ? new Date(e * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(e), I = new Date(S);
I.setUTCHours(0, 0, 0, 0);
const t = new Date(S);
t.setUTCHours(23, 59, 59, 999);
let C, O;
return this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? (C = Math.floor(I.getTime() / 1e3), O = Math.floor(t.getTime() / 1e3)) : (C = I.getTime(), O = t.getTime()) : (C = I.toISOString(), O = t.toISOString()), p(
$(E, C),
g(E, O)
);
}
return null;
}
/**
* Parse relative date range expressions like "today", "yesterday", "last 7 days", "this month", etc.
* Handles all 14 DATE_RANGE_OPTIONS from the client
*/
parseRelativeDateRange(E) {
const T = /* @__PURE__ */ new Date(), A = E.toLowerCase().trim(), e = T.getUTCFullYear(), S = T.getUTCMonth(), I = T.getUTCDate(), t = T.getUTCDay();
if (A === "today") {
const r = new Date(T);
r.setUTCHours(0, 0, 0, 0);
const L = new Date(T);
return L.setUTCHours(23, 59, 59, 999), { start: r, end: L };
}
if (A === "yesterday") {
const r = new Date(T);
r.setUTCDate(I - 1), r.setUTCHours(0, 0, 0, 0);
const L = new Date(T);
return L.setUTCDate(I - 1), L.setUTCHours(23, 59, 59, 999), { start: r, end: L };
}
if (A === "this week") {
const r = t === 0 ? -6 : 1 - t, L = new Date(T);
L.setUTCDate(I + r), L.setUTCHours(0, 0, 0, 0);
const i = new Date(L);
return i.setUTCDate(L.getUTCDate() + 6), i.setUTCHours(23, 59, 59, 999), { start: L, end: i };
}
if (A === "this month") {
const r = new Date(Date.UTC(e, S, 1, 0, 0, 0, 0)), L = new Date(Date.UTC(e, S + 1, 0, 23, 59, 59, 999));
return { start: r, end: L };
}
if (A === "this quarter") {
const r = Math.floor(S / 3), L = new Date(Date.UTC(e, r * 3, 1, 0, 0, 0, 0)), i = new Date(Date.UTC(e, r * 3 + 3, 0, 23, 59, 59, 999));
return { start: L, end: i };
}
if (