drizzle-cube
Version:
Drizzle ORM-first semantic layer with Cube.js compatibility. Type-safe analytics and dashboards with SQL injection protection.
1,483 lines • 500 kB
JavaScript
import { sql as n, eq as K, and as F, SQL as Te, gte as Q, lte as Z, arrayContained as ee, arrayOverlaps as Re, arrayContains as Ae, isNotNull as ZE, ne as jE, or as z, isNull as kE, notInArray as qE, inArray as nE, lt as rE, gt as CE, StringChunk as Se, sum as W, max as EE, min as TE, count as WE, countDistinct as te, desc as se, asc as zE } from "drizzle-orm";
class KE {
/**
* 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 Ne extends KE {
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, R) {
switch (T) {
case "contains":
return n`${E} ILIKE ${`%${R}%`}`;
case "notContains":
return n`${E} NOT ILIKE ${`%${R}%`}`;
case "startsWith":
return n`${E} ILIKE ${`${R}%`}`;
case "endsWith":
return n`${E} ILIKE ${`%${R}`}`;
case "like":
return n`${E} LIKE ${R}`;
case "notLike":
return n`${E} NOT LIKE ${R}`;
case "ilike":
return n`${E} ILIKE ${R}`;
case "regex":
return n`${E} ~* ${R}`;
case "notRegex":
return n`${E} !~* ${R}`;
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 R = E.map((A) => n`WHEN ${A.when} THEN ${A.then}`).reduce((A, S) => n`${A} ${S}`);
return T !== void 0 ? n`CASE ${R} ELSE ${T} END` : n`CASE ${R} 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;
}
// ============================================
// Statistical & Window Function Methods
// ============================================
/**
* PostgreSQL has full support for statistical and window functions
*/
getCapabilities() {
return {
supportsStddev: !0,
supportsVariance: !0,
supportsPercentile: !0,
supportsWindowFunctions: !0,
supportsFrameClause: !0
};
}
/**
* Build PostgreSQL STDDEV aggregation
* Uses STDDEV_POP for population, STDDEV_SAMP for sample
*/
buildStddev(E, T = !1) {
const R = T ? "STDDEV_SAMP" : "STDDEV_POP";
return n`COALESCE(${n.raw(R)}(${E}), 0)`;
}
/**
* Build PostgreSQL VARIANCE aggregation
* Uses VAR_POP for population, VAR_SAMP for sample
*/
buildVariance(E, T = !1) {
const R = T ? "VAR_SAMP" : "VAR_POP";
return n`COALESCE(${n.raw(R)}(${E}), 0)`;
}
/**
* Build PostgreSQL PERCENTILE_CONT aggregation
* Uses ordered-set aggregate function
*/
buildPercentile(E, T) {
const R = T / 100;
return n`PERCENTILE_CONT(${R}) WITHIN GROUP (ORDER BY ${E})`;
}
/**
* Build PostgreSQL window function expression
* PostgreSQL has full window function support
*/
buildWindowFunction(E, T, R, A, S) {
const t = R && R.length > 0 ? n`PARTITION BY ${n.join(R, n`, `)}` : n``, O = A && A.length > 0 ? n`ORDER BY ${n.join(A.map(
(C) => C.direction === "desc" ? n`${C.field} DESC` : n`${C.field} ASC`
), n`, `)}` : n``;
let r = n``;
if (S?.frame) {
const { type: C, start: i, end: L } = S.frame, M = C.toUpperCase(), a = i === "unbounded" ? "UNBOUNDED PRECEDING" : typeof i == "number" ? `${i} PRECEDING` : "CURRENT ROW", U = L === "unbounded" ? "UNBOUNDED FOLLOWING" : L === "current" ? "CURRENT ROW" : typeof L == "number" ? `${L} FOLLOWING` : "CURRENT ROW";
r = n`${n.raw(M)} BETWEEN ${n.raw(a)} AND ${n.raw(U)}`;
}
const s = [];
R && R.length > 0 && s.push(t), A && A.length > 0 && s.push(O), S?.frame && s.push(r);
const I = s.length > 0 ? n.join(s, n` `) : n``, N = n`OVER (${I})`;
switch (E) {
case "lag":
return n`LAG(${T}, ${S?.offset ?? 1}${S?.defaultValue !== void 0 ? n`, ${S.defaultValue}` : n``}) ${N}`;
case "lead":
return n`LEAD(${T}, ${S?.offset ?? 1}${S?.defaultValue !== void 0 ? n`, ${S.defaultValue}` : n``}) ${N}`;
case "rank":
return n`RANK() ${N}`;
case "denseRank":
return n`DENSE_RANK() ${N}`;
case "rowNumber":
return n`ROW_NUMBER() ${N}`;
case "ntile":
return n`NTILE(${S?.nTile ?? 4}) ${N}`;
case "firstValue":
return n`FIRST_VALUE(${T}) ${N}`;
case "lastValue":
return n`LAST_VALUE(${T}) ${N}`;
case "movingAvg":
return n`AVG(${T}) ${N}`;
case "movingSum":
return n`SUM(${T}) ${N}`;
default:
throw new Error(`Unsupported window function: ${E}`);
}
}
}
class VT extends KE {
getEngineType() {
return "mysql";
}
/**
* Build MySQL time dimension using DATE_FORMAT function
* MySQL equivalent to PostgreSQL's DATE_TRUNC
*/
buildTimeDimension(E, T) {
const R = {
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 A = R[E];
return A ? n`STR_TO_DATE(DATE_FORMAT(${T}, ${A}), '%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, R) {
switch (T) {
case "contains":
return n`LOWER(${E}) LIKE ${`%${R.toLowerCase()}%`}`;
case "notContains":
return n`LOWER(${E}) NOT LIKE ${`%${R.toLowerCase()}%`}`;
case "startsWith":
return n`LOWER(${E}) LIKE ${`${R.toLowerCase()}%`}`;
case "endsWith":
return n`LOWER(${E}) LIKE ${`%${R.toLowerCase()}`}`;
case "like":
return n`${E} LIKE ${R}`;
case "notLike":
return n`${E} NOT LIKE ${R}`;
case "ilike":
return n`LOWER(${E}) LIKE ${R.toLowerCase()}`;
case "regex":
return n`${E} REGEXP ${R}`;
case "notRegex":
return n`${E} NOT REGEXP ${R}`;
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 R = E.map((A) => n`WHEN ${A.when} THEN ${A.then}`).reduce((A, S) => n`${A} ${S}`);
return T !== void 0 ? n`CASE ${R} ELSE ${T} END` : n`CASE ${R} 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;
}
// ============================================
// Statistical & Window Function Methods
// ============================================
/**
* MySQL 8.0+ has support for statistical and window functions
* but not PERCENTILE_CONT
*/
getCapabilities() {
return {
supportsStddev: !0,
supportsVariance: !0,
supportsPercentile: !1,
// MySQL doesn't have PERCENTILE_CONT
supportsWindowFunctions: !0,
supportsFrameClause: !0
};
}
/**
* Build MySQL STDDEV aggregation
* Uses STDDEV_POP for population, STDDEV_SAMP for sample
*/
buildStddev(E, T = !1) {
const R = T ? "STDDEV_SAMP" : "STDDEV_POP";
return n`IFNULL(${n.raw(R)}(${E}), 0)`;
}
/**
* Build MySQL VARIANCE aggregation
* Uses VAR_POP for population, VAR_SAMP for sample
*/
buildVariance(E, T = !1) {
const R = T ? "VAR_SAMP" : "VAR_POP";
return n`IFNULL(${n.raw(R)}(${E}), 0)`;
}
/**
* MySQL does not support PERCENTILE_CONT
* Returns null for graceful degradation
*/
buildPercentile(E, T) {
return null;
}
/**
* Build MySQL window function expression
* MySQL 8.0+ has full window function support
*/
buildWindowFunction(E, T, R, A, S) {
const t = R && R.length > 0 ? n`PARTITION BY ${n.join(R, n`, `)}` : n``, O = A && A.length > 0 ? n`ORDER BY ${n.join(A.map(
(C) => C.direction === "desc" ? n`${C.field} DESC` : n`${C.field} ASC`
), n`, `)}` : n``;
let r = n``;
if (S?.frame) {
const { type: C, start: i, end: L } = S.frame, M = C.toUpperCase(), a = i === "unbounded" ? "UNBOUNDED PRECEDING" : typeof i == "number" ? `${i} PRECEDING` : "CURRENT ROW", U = L === "unbounded" ? "UNBOUNDED FOLLOWING" : L === "current" ? "CURRENT ROW" : typeof L == "number" ? `${L} FOLLOWING` : "CURRENT ROW";
r = n`${n.raw(M)} BETWEEN ${n.raw(a)} AND ${n.raw(U)}`;
}
const s = [];
R && R.length > 0 && s.push(t), A && A.length > 0 && s.push(O), S?.frame && s.push(r);
const I = s.length > 0 ? n.join(s, n` `) : n``, N = n`OVER (${I})`;
switch (E) {
case "lag":
return n`LAG(${T}, ${S?.offset ?? 1}${S?.defaultValue !== void 0 ? n`, ${S.defaultValue}` : n``}) ${N}`;
case "lead":
return n`LEAD(${T}, ${S?.offset ?? 1}${S?.defaultValue !== void 0 ? n`, ${S.defaultValue}` : n``}) ${N}`;
case "rank":
return n`RANK() ${N}`;
case "denseRank":
return n`DENSE_RANK() ${N}`;
case "rowNumber":
return n`ROW_NUMBER() ${N}`;
case "ntile":
return n`NTILE(${S?.nTile ?? 4}) ${N}`;
case "firstValue":
return n`FIRST_VALUE(${T}) ${N}`;
case "lastValue":
return n`LAST_VALUE(${T}) ${N}`;
case "movingAvg":
return n`AVG(${T}) ${N}`;
case "movingSum":
return n`SUM(${T}) ${N}`;
default:
throw new Error(`Unsupported window function: ${E}`);
}
}
}
class Oe extends KE {
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 R = n`datetime(${T}, 'unixepoch')`;
return n`datetime(${R}, 'start of year',
'+' || (((CAST(strftime('%m', ${R}) 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 R = n`datetime(${T}, 'unixepoch')`;
return n`datetime(strftime('%Y-%m-%d %H:00:00', ${R}))`;
}
case "minute": {
const R = n`datetime(${T}, 'unixepoch')`;
return n`datetime(strftime('%Y-%m-%d %H:%M:00', ${R}))`;
}
case "second": {
const R = n`datetime(${T}, 'unixepoch')`;
return n`datetime(strftime('%Y-%m-%d %H:%M:%S', ${R}))`;
}
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, R) {
switch (T) {
case "contains":
return n`LOWER(${E}) LIKE ${`%${R.toLowerCase()}%`}`;
case "notContains":
return n`LOWER(${E}) NOT LIKE ${`%${R.toLowerCase()}%`}`;
case "startsWith":
return n`LOWER(${E}) LIKE ${`${R.toLowerCase()}%`}`;
case "endsWith":
return n`LOWER(${E}) LIKE ${`%${R.toLowerCase()}`}`;
case "like":
return n`${E} LIKE ${R}`;
case "notLike":
return n`${E} NOT LIKE ${R}`;
case "ilike":
return n`LOWER(${E}) LIKE ${R.toLowerCase()}`;
case "regex":
return n`${E} GLOB ${R}`;
case "notRegex":
return n`${E} NOT GLOB ${R}`;
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 R = E.map((A) => A.then && typeof A.then == "object" && (A.then.queryChunks || A.then._ || A.then.sql) ? n`WHEN ${A.when} THEN ${n.raw("(")}${A.then}${n.raw(")")}` : n`WHEN ${A.when} THEN ${A.then}`).reduce((A, S) => n`${A} ${S}`);
return T !== void 0 ? T && typeof T == "object" && (T.queryChunks || T._ || T.sql) ? n`CASE ${R} ELSE ${n.raw("(")}${T}${n.raw(")")} END` : n`CASE ${R} ELSE ${T} END` : n`CASE ${R} 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, (R, A) => `${A.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;
}
// ============================================
// Statistical & Window Function Methods
// ============================================
/**
* SQLite has limited statistical support (no native STDDEV/VARIANCE/PERCENTILE)
* but supports window functions since SQLite 3.25
*/
getCapabilities() {
return {
supportsStddev: !1,
// Requires extension
supportsVariance: !1,
// Requires extension
supportsPercentile: !1,
// Requires extension
supportsWindowFunctions: !0,
// SQLite 3.25+
supportsFrameClause: !0
};
}
/**
* SQLite does not have native STDDEV
* Returns null for graceful degradation
*/
buildStddev(E, T = !1) {
return null;
}
/**
* SQLite does not have native VARIANCE
* Returns null for graceful degradation
*/
buildVariance(E, T = !1) {
return null;
}
/**
* SQLite does not have native PERCENTILE
* Returns null for graceful degradation
*/
buildPercentile(E, T) {
return null;
}
/**
* Build SQLite window function expression
* SQLite 3.25+ supports window functions
*/
buildWindowFunction(E, T, R, A, S) {
const t = R && R.length > 0 ? n`PARTITION BY ${n.join(R, n`, `)}` : n``, O = A && A.length > 0 ? n`ORDER BY ${n.join(A.map(
(C) => C.direction === "desc" ? n`${C.field} DESC` : n`${C.field} ASC`
), n`, `)}` : n``;
let r = n``;
if (S?.frame) {
const { type: C, start: i, end: L } = S.frame, M = C.toUpperCase(), a = i === "unbounded" ? "UNBOUNDED PRECEDING" : typeof i == "number" ? `${i} PRECEDING` : "CURRENT ROW", U = L === "unbounded" ? "UNBOUNDED FOLLOWING" : L === "current" ? "CURRENT ROW" : typeof L == "number" ? `${L} FOLLOWING` : "CURRENT ROW";
r = n`${n.raw(M)} BETWEEN ${n.raw(a)} AND ${n.raw(U)}`;
}
const s = [];
R && R.length > 0 && s.push(t), A && A.length > 0 && s.push(O), S?.frame && s.push(r);
const I = s.length > 0 ? n.join(s, n` `) : n``, N = n`OVER (${I})`;
switch (E) {
case "lag":
return n`LAG(${T}, ${S?.offset ?? 1}${S?.defaultValue !== void 0 ? n`, ${S.defaultValue}` : n``}) ${N}`;
case "lead":
return n`LEAD(${T}, ${S?.offset ?? 1}${S?.defaultValue !== void 0 ? n`, ${S.defaultValue}` : n``}) ${N}`;
case "rank":
return n`RANK() ${N}`;
case "denseRank":
return n`DENSE_RANK() ${N}`;
case "rowNumber":
return n`ROW_NUMBER() ${N}`;
case "ntile":
return n`NTILE(${S?.nTile ?? 4}) ${N}`;
case "firstValue":
return n`FIRST_VALUE(${T}) ${N}`;
case "lastValue":
return n`LAST_VALUE(${T}) ${N}`;
case "movingAvg":
return n`AVG(${T}) ${N}`;
case "movingSum":
return n`SUM(${T}) ${N}`;
default:
throw new Error(`Unsupported window function: ${E}`);
}
}
}
class Ie extends VT {
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 ne(e) {
switch (e) {
case "postgres":
return new Ne();
case "mysql":
return new VT();
case "sqlite":
return new Oe();
case "singlestore":
return new Ie();
default:
throw new Error(`Unsupported database engine: ${e}`);
}
}
class wE {
constructor(E, T, R) {
this.db = E, this.schema = T;
const A = R || this.getEngineType();
this.databaseAdapter = ne(A);
}
databaseAdapter;
}
class re extends wE {
async execute(E, T) {
if (E && typeof E == "object" && typeof E.execute == "function") {
const A = await E.execute();
return Array.isArray(A) ? A.map((S) => this.convertNumericFields(S, T)) : A;
}
if (!this.db.execute)
throw new Error("PostgreSQL database instance must have an execute method");
const R = await this.db.execute(E);
return Array.isArray(R) ? R.map((A) => this.convertNumericFields(A, T)) : R;
}
/**
* Convert numeric string fields to numbers (only for measure fields)
*/
convertNumericFields(E, T) {
if (!E || typeof E != "object") return E;
const R = {};
for (const [A, S] of Object.entries(E))
T && T.includes(A) ? R[A] = this.coerceToNumber(S) : R[A] = S;
return R;
}
/**
* 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 ET(e, E) {
return new re(e, E, "postgres");
}
class bT extends wE {
async execute(E, T) {
if (E && typeof E == "object" && typeof E.execute == "function") {
const A = await E.execute();
return Array.isArray(A) ? A.map((S) => this.convertNumericFields(S, T)) : A;
}
if (!this.db.execute)
throw new Error("MySQL database instance must have an execute method");
const R = await this.db.execute(E);
return Array.isArray(R) ? R.map((A) => this.convertNumericFields(A, T)) : R;
}
/**
* Convert numeric string fields to numbers (measure fields + numeric dimensions)
*/
convertNumericFields(E, T) {
if (!E || typeof E != "object") return E;
const R = {};
for (const [A, S] of Object.entries(E))
T && T.includes(A) ? R[A] = this.coerceToNumber(S) : R[A] = S;
return R;
}
/**
* 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 Ce(e, E) {
return new bT(e, E, "mysql");
}
class ie extends wE {
async execute(E, T) {
if (E && typeof E == "object" && typeof E.execute == "function") {
const R = await E.execute();
return Array.isArray(R) ? R.map((A) => this.convertNumericFields(A, T)) : R;
}
try {
if (this.db.all) {
const R = this.db.all(E);
return Array.isArray(R) ? R.map((A) => this.convertNumericFields(A, T)) : R;
} else {
if (this.db.run)
return this.db.run(E);
throw new Error("SQLite database instance must have an all() or run() method");
}
} catch (R) {
throw new Error(`SQLite execution failed: ${R instanceof Error ? R.message : "Unknown error"}`);
}
}
/**
* Convert numeric string fields to numbers (only for measure fields)
*/
convertNumericFields(E, T) {
if (!E || typeof E != "object") return E;
const R = {};
for (const [A, S] of Object.entries(E))
T && T.includes(A) ? R[A] = this.coerceToNumber(S) : R[A] = S;
return R;
}
/**
* 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 TT(e, E) {
return new ie(e, E, "sqlite");
}
class Le 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 _e(e, E) {
return new Le(e, E);
}
function eT(e, E, T) {
if (T)
switch (T) {
case "postgres":
return ET(e, E);
case "mysql":
return Ce(e, E);
case "sqlite":
return TT(e, E);
case "singlestore":
return _e(e, E);
}
if (e.all && e.run)
return TT(e, E);
if (e.execute)
return ET(e, E);
throw new Error("Unable to determine database engine type. Please specify engineType parameter.");
}
function tE(e) {
return typeof e == "function" ? e() : e;
}
function gE(e, E) {
if (E) return E;
switch (e) {
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 yE(e) {
return e && typeof e == "object" ? n`${n`${e}`}` : e;
}
function Y(e, E) {
const T = typeof e == "function" ? e(E) : e;
return yE(T);
}
function Us(e, E, T) {
return {
...e,
cubes: E,
currentCube: T
};
}
function cs(e, E) {
return {
name: e,
...E
};
}
function ae(e, E) {
if (e.relationship !== "belongsToMany" || !e.through)
throw new Error("expandBelongsToManyJoin can only be called on belongsToMany relationships with through configuration");
const { table: T, sourceKey: R, targetKey: A, securitySql: S } = e.through, t = [];
for (const I of R) {
const N = I.as || K;
t.push(N(I.source, I.target));
}
const O = [];
for (const I of A) {
const N = I.as || K;
O.push(N(I.source, I.target));
}
let r;
if (S) {
const I = S(E);
r = Array.isArray(I) ? I : [I];
}
const s = gE("belongsToMany", e.sqlJoinType);
return {
junctionJoins: [
{
joinType: s,
table: T,
condition: F(...t)
},
{
joinType: s,
table: T,
// This will be replaced with target cube table in query planner
condition: F(...O)
}
],
junctionSecurityConditions: r
};
}
function sE(e) {
if ("and" in e)
return `and:[${e.and.map(sE).sort().join(",")}]`;
if ("or" in e)
return `or:[${e.or.map(sE).sort().join(",")}]`;
const E = e, T = JSON.stringify(
Array.isArray(E.values) ? [...E.values].sort() : E.values
), R = E.dateRange ? `:dr:${JSON.stringify(E.dateRange)}` : "";
return `${E.member}:${E.operator}:${T}${R}`;
}
function WT(e, E) {
return `timeDim:${e}:${JSON.stringify(E)}`;
}
class oe {
cache = /* @__PURE__ */ new Map();
stats = { hits: 0, misses: 0 };
/**
* 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(E, T) {
const R = this.cache.get(E);
if (R !== void 0)
return this.stats.hits++, R;
const A = T();
return A && this.cache.set(E, A), this.stats.misses++, A;
}
/**
* Check if a key exists in the cache without affecting stats
*/
has(E) {
return this.cache.has(E);
}
/**
* Get cached SQL without building (returns undefined if not cached)
*/
get(E) {
const T = this.cache.get(E);
return T !== void 0 && this.stats.hits++, T;
}
/**
* Pre-populate cache with multiple filters
* Useful for batch initialization at query start
*/
preload(E) {
for (const { key: T, sql: R } of E)
this.cache.has(T) || this.cache.set(T, R);
}
/**
* Store a single entry in the cache
*/
set(E, T) {
this.cache.set(E, T);
}
/**
* Get cache statistics for debugging
*/
getStats() {
return { ...this.stats, cacheSize: this.cache.size };
}
/**
* Clear the cache (normally not needed as cache is per-query)
*/
clear() {
this.cache.clear(), this.stats = { hits: 0, misses: 0 };
}
}
function XE(e) {
const E = [];
for (const T of e)
"and" in T && T.and ? E.push(...XE(T.and)) : "or" in T && T.or ? E.push(...XE(T.or)) : "member" in T && E.push(T);
return E;
}
class De {
constructor(E) {
this.databaseAdapter = E;
}
/**
* Build time dimension expression with granularity using database adapter
*/
buildTimeDimensionExpression(E, T, R) {
const A = Y(E, R);
return T ? this.databaseAdapter.buildTimeDimension(T, A) : A instanceof Te ? A : n`${A}`;
}
/**
* Build date range condition for time dimensions
*/
buildDateRangeCondition(E, T) {
if (!T) return null;
if (Array.isArray(T) && T.length >= 2) {
const R = this.normalizeDate(T[0]);
let A = this.normalizeDate(T[1]);
if (!R || !A) return null;
if (typeof T[1] == "string" && /^\d{4}-\d{2}-\d{2}$/.test(T[1].trim())) {
const S = typeof A == "number" ? new Date(A * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(A), t = new Date(S);
t.setUTCHours(23, 59, 59, 999), this.databaseAdapter.isTimestampInteger() ? A = this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(t.getTime() / 1e3) : t.getTime() : A = t.toISOString();
}
return F(
Q(E, R),
Z(E, A)
);
}
if (typeof T == "string") {
const R = this.parseRelativeDateRange(T);
if (R) {
let I, N;
return this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? (I = Math.floor(R.start.getTime() / 1e3), N = Math.floor(R.end.getTime() / 1e3)) : (I = R.start.getTime(), N = R.end.getTime()) : (I = R.start.toISOString(), N = R.end.toISOString()), F(
Q(E, I),
Z(E, N)
);
}
const A = this.normalizeDate(T);
if (!A) return null;
const S = typeof A == "number" ? new Date(A * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(A), t = new Date(S);
t.setUTCHours(0, 0, 0, 0);
const O = new Date(S);
O.setUTCHours(23, 59, 59, 999);
let r, s;
return this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? (r = Math.floor(t.getTime() / 1e3), s = Math.floor(O.getTime() / 1e3)) : (r = t.getTime(), s = O.getTime()) : (r = t.toISOString(), s = O.toISOString()), F(
Q(E, r),
Z(E, s)
);
}
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(), R = E.toLowerCase().trim(), A = T.getUTCFullYear(), S = T.getUTCMonth(), t = T.getUTCDate(), O = T.getUTCDay();
if (R === "today") {
const C = new Date(T);
C.setUTCHours(0, 0, 0, 0);
const i = new Date(T);
return i.setUTCHours(23, 59, 59, 999), { start: C, end: i };
}
if (R === "yesterday") {
const C = new Date(T);
C.setUTCDate(t - 1), C.setUTCHours(0, 0, 0, 0);
const i = new Date(T);
return i.setUTCDate(t - 1), i.setUTCHours(23, 59, 59, 999), { start: C, end: i };
}
if (R === "this week") {
const C = O === 0 ? -6 : 1 - O, i = new Date(T);
i.setUTCDate(t + C), i.setUTCHours(0, 0, 0, 0);
const L = new Date(i);
return L.setUTCDate(i.getUTCDate() + 6), L.setUTCHours(23, 59, 59, 999), { start: i, end: L };
}
if (R === "this month") {
const C = new Date(Date.UTC(A, S, 1, 0, 0, 0, 0)), i = new Date(Date.UTC(A, S + 1, 0, 23, 59, 59, 999));
return { start: C, end: i };
}
if (R === "this quarter") {
const C = Math.floor(S / 3), i = new Date(Date.UTC(A, C * 3, 1, 0, 0, 0, 0)), L = new Date(Date.UTC(A, C * 3 + 3, 0, 23, 59, 59, 999));
return { start: i, end: L };
}
if (R === "this year") {
const C = new Date(Date.UTC(A, 0, 1, 0, 0, 0, 0)), i = new Date(Date.UTC(A, 11, 31, 23, 59, 59, 999));
return { start: C, end: i };
}
const r = R.match(/^last\s+(\d+)\s+days?$/);
if (r) {
const C = parseInt(r[1], 10), i = new Date(T);
i.setUTCDate(t - C + 1), i.setUTCHours(0, 0, 0, 0);
const L = new Date(T);
return L.setUTCHours(23, 59, 59, 999), { start: i, end: L };
}
const s = R.match(/^last\s+(\d+)\s+weeks?$/);
if (s) {
const i = parseInt(s[1], 10) * 7, L = new Date(T);
L.setUTCDate(t - i + 1), L.setUTCHours(0, 0, 0, 0);
const M = new Date(T);
return M.setUTCHours(23, 59, 59, 999), { start: L, end: M };
}
if (R === "last week") {
const C = O === 0 ? -13 : -6 - O, i = new Date(T);
i.setUTCDate(t + C), i.setUTCHours(0, 0, 0, 0);
const L = new Date(i);
return L.setUTCDate(i.getUTCDate() + 6), L.setUTCHours(23, 59, 59, 999), { start: i, end: L };
}
if (R === "last month") {
const C = new Date(Date.UTC(A, S - 1, 1, 0, 0, 0, 0)), i = new Date(Date.UTC(A, S, 0, 23, 59, 59, 999));
return { start: C, end: i };
}
if (R === "last quarter") {
const C = Math.floor(S / 3), i = C === 0 ? 3 : C - 1, L = C === 0 ? A - 1 : A, M = new Date(Date.UTC(L, i * 3, 1, 0, 0, 0, 0)), a = new Date(Date.UTC(L, i * 3 + 3, 0, 23, 59, 59, 999));
return { start: M, end: a };
}
if (R === "last year") {
const C = new Date(Date.UTC(A - 1, 0, 1, 0, 0, 0, 0)), i = new Date(Date.UTC(A - 1, 11, 31, 23, 59, 59, 999));
return { start: C, end: i };
}
if (R === "last 12 months") {
const C = new Date(Date.UTC(A, S - 11, 1, 0, 0, 0, 0)), i = new Date(T);
return i.setUTCHours(23, 59, 59, 999), { start: C, end: i };
}
const I = R.match(/^last\s+(\d+)\s+months?$/);
if (I) {
const C = parseInt(I[1], 10), i = new Date(Date.UTC(A, S - C + 1, 1, 0, 0, 0, 0)), L = new Date(T);
return L.setUTCHours(23, 59, 59, 999), { start: i, end: L };
}
const N = R.match(/^last\s+(\d+)\s+years?$/);
if (N) {
const C = parseInt(N[1], 10), i = new Date(Date.UTC(A - C, 0, 1, 0, 0, 0, 0)), L = new Date(T);
return L.setUTCHours(23, 59, 59, 999), { start: i, end: L };
}
return null;
}
/**
* Normalize date values to handle strings, numbers, and Date objects
* Returns ISO string for PostgreSQL/MySQL, Unix timestamp for SQLite, or null
* Ensures dates are in the correct format for each database engine
*/
normalizeDate(E) {
if (!E) return null;
if (E instanceof Date)
return isNaN(E.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(E.getTime() / 1e3) : E.getTime() : E.toISOString();
if (typeof E == "number") {
const R = E < 1e10 ? E * 1e3 : E, A = new Date(R);
return isNaN(A.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(R / 1e3) : R : A.toISOString();
}
if (typeof E == "string") {
if (/^\d{4}-\d{2}-\d{2}$/.test(E.trim())) {
const A = /* @__PURE__ */ new Date(E + "T00:00:00Z");
return isNaN(A.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(A.getTime() / 1e3) : A.getTime() : A.toISOString();
}
const R = new Date(E);
return isNaN(R.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(R.getTime() / 1e3) : R.getTime() : R.toISOString();
}
const T = new Date(E);
return isNaN(T.getTime()) ? null : this.databaseAdapter.isTimestampInteger() ? this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(T.getTime() / 1e3) : T.getTime() : T.toISOString();
}
}
class Pe {
constructor(E, T) {
this.databaseAdapter = E, this.dateTimeBuilder = T;
}
/**
* Build filter condition using Drizzle operators
*/
buildFilterCondition(E, T, R, A, 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 (A && A.type !== "time")
throw new Error(
`dateRange can only be used on time dimensions, but field '${A.name || "unknown"}' has type '${A.type}'`
);
return this.dateTimeBuilder.buildDateRangeCondition(E, S);
}
if (!R || R.length === 0)
return T === "equals" ? this.databaseAdapter.buildBooleanLiteral(!1) : null;
const t = R.filter((r) => !(r == null || r === "" || typeof r == "string" && r.includes("\0"))).map(this.databaseAdapter.convertFilterValue);
if (t.length === 0 && !["set", "notSet"].includes(T))
return T === "equals" ? this.databaseAdapter.buildBooleanLiteral(!1) : null;
const O = t[0];
switch (T) {
case "equals":
if (t.length > 1) {
if (A?.type === "time") {
const r = t.map((s) => this.dateTimeBuilder.normalizeDate(s) || s);
return nE(E, r);
}
return nE(E, t);
} else if (t.length === 1) {
const r = A?.type === "time" && this.dateTimeBuilder.normalizeDate(O) || O;
return K(E, r);
}
return this.databaseAdapter.buildBooleanLiteral(!1);
case "notEquals":
return t.length > 1 ? qE(E, t) : t.length === 1 ? jE(E, O) : null;
case "contains":
return this.databaseAdapter.buildStringCondition(E, "contains", O);
case "notContains":
return this.databaseAdapter.buildStringCondition(E, "notContains", O);
case "startsWith":
return this.databaseAdapter.buildStringCondition(E, "startsWith", O);
case "endsWith":
return this.databaseAdapter.buildStringCondition(E, "endsWith", O);
case "gt":
return CE(E, O);
case "gte":
return Q(E, O);
case "lt":
return rE(E, O);
case "lte":
return Z(E, O);
case "set":
return ZE(E);
case "notSet":
return kE(E);
case "inDateRange":
if (t.length >= 2) {
const r = this.dateTimeBuilder.normalizeDate(t[0]);
let s = this.dateTimeBuilder.normalizeDate(t[1]);
if (r && s) {
const I = R[1];
if (typeof I == "string" && /^\d{4}-\d{2}-\d{2}$/.test(I.trim())) {
const N = typeof s == "number" ? new Date(s * (this.databaseAdapter.getEngineType() === "sqlite" ? 1e3 : 1)) : new Date(s), C = new Date(N);
C.setUTCHours(23, 59, 59, 999), this.databaseAdapter.isTimestampInteger() ? s = this.databaseAdapter.getEngineType() === "sqlite" ? Math.floor(C.getTime() / 1e3) : C.getTime() : s = C.toISOString();
}
return F(
Q(E, r),
Z(E, s)
);
}
}
return null;
case "beforeDate": {
const r = this.dateTimeBuilder.normalizeDate(O);
return r ? rE(E, r) : null;
}
case "afterDate": {
const r = this.dateTimeBuilder.normalizeDate(O);
return r ? CE(E, r) : null;
}
case "between":
return t.length >= 2 ? F(
Q(E, t[0]),
Z(E, t[1])
) : null;
case "notBetween":
return t.length >= 2 ? z(
rE(E, t[0]),
CE(E, t[1])
) : null;
case "in":
return t.length > 0 ? nE(E, t) : null;
case "notIn":
return t.length > 0 ? qE(E, t) : null;
case "like":
return this.databaseAdapter.buildStringCondition(E, "like", O);
case "notLike":
return this.databaseAdapter.buildStringCondition(E, "notLike", O);
case "ilike":
return this.databaseAdapter.buildStringCondition(E, "ilike", O);
case "regex":
return this.databaseAdapter.buildStringCondition(E, "regex", O);
case "notRegex":
return this.databaseAdapter.buildStringCondition(E, "notRegex", O);
case "isEmpty":
return z(
kE(E),
K(E, "")
);
case "isNotEmpty":
return F(
ZE(E),
jE(E, "")
);
// PostgreSQL array operators - silent no-op for other databases
// These use Drizzle's built-in array operator functions
case "arrayContains":
return this.databaseAdapter.getEngineType() === "postgres" ? Ae(E, t) : null;
case "arrayOverlaps":
return this.databaseAdapter.getEngineType() === "postgres" ? Re(E, t) : null;
case "arrayContained":
return this.databaseAdapter.getEngineType() === "postgres" ? ee(E, t) : null;
default:
return null;
}
}
/**
* Build a logical filter (AND/OR) - used by executor for cache preloading
* This handles nested filter structures and builds combined SQL
*/
buildLogicalFilter(E, T, R) {
if ("and" in E && E.and) {
const A = E.and.map((S) => this.buildSingleFilter(S, T, R)).filter((S) => S !== null);
return A.length > 0 ? A.length === 1 ? A[0] : F(...A) : null;
}
if ("or" in E && E.or) {
const A = E.or.map((S) => this.buildSingleFilter(S, T, R)).filter((S) => S !== null);
return A.length > 0 ? A.length === 1 ? A[0] : z(...A) : null;
}
return null;
}
/**
* Build SQL for a single filter condition (simple or logical)
* Used for cache preloading to build filters independently of query context
*/
buildSingleFilter(E, T, R) {
if ("and" in E || "or" in E)
return this.buildLogicalFilter(E, T, R);
const A = E, [S, t] = A.member.split("."), O = T.get(S);
if (!O) return null;
const r = O.dimensions?.[t];
if (!r) return null;
const s = Y(r.sql, R);
return this.buildFilterCondition(
s,
A.operator,
A.values,
r,
A.dateRange
);
}
}
class Me {
constructor(E) {
this.dateTimeBuilder = E;
}
/**
* Check if a measure type is a window function
*/
isWindowFunctionType(E) {
return [
"lag",
"lead",
"rank",
"denseRank",
"rowNumber",
"ntile",
"firstValue",
"lastValue",
"movingAvg",
"movingSum"
].includes(E);
}
/**
* Check if a measure type is an aggregate function (requires GROUP BY)
*/
isAggregateFunctionType(E) {
return [
"count",
"countDistinct",
"sum",
"avg",
"min",
"max",
"stddev",
"stddevSamp",
"variance",
"varianceSamp",
"median",
"p95",
"p99",
"percentile"
].includes(E);
}
/**
* Build GROUP BY fields from dimensions and time dimensions
* Works for both single and multi-cube queries
*
* NOTE: GROUP BY is only added when there are AGGREGATE measures.
* Window functions do not require GROUP BY and operate on individual rows.
*/
buildGroupByFields(E, T, R, A) {
const S = [], t = E instanceof Map ? E : /* @__PURE__ */ new Map([[E.name, E]]);
let O = !1, r = !1;
for (const s of T.measures || []) {
const [I, N] = s.split("."), C = t.get(I);
if (C && C.measures && C.measures[N]) {
const i = C.measures[N];
(this.isAggregateFunctionType(i.type) || i.type === "calculated") && (O = !0), this.isWindowFunctionType(i.type) && (r = !0);
}
}
if (O && r && console.warn(
"[drizzle-cube] Warning: Mixing aggregate measures (count, sum, avg, etc.) with window functions (lag, lead, rank, etc.) in the same query may produce incorrect results. Window functions will operate on raw rows before aggregation. Consider separating these into different queries or using CTE pattern."
), !O)
return [];
if (T.dimensions)
for (const s of T.dimensions) {
const [I, N] = s.split("."), C = t.get(I);
if (C && C.dimensions && C.dimensions[N])
if (A?.preAggregationCTEs?.some((L) => L.cube.name === I)) {
const L = A.preAggregationCTEs.find((a) => a.cube.name === I), M = L.joinKeys.find((a) => a.targetColumn === N);
if (M && M.sourceColumnObj)
S.push(M.sourceColumnObj);
else {
const a = n`${n.identifier(L.cteAlias)}.${n.identifier(N)}`;
S.push(a);
}
} else {
const L = C.dimensions[N], M = Y(L.sql, R);
S.push(M);
}
}
if (T.timeDimensions)
for (const s of T.timeDimensions) {
const [I, N] = s.dimension.split("."), C = t.get(I);
if (C && C.dimensions && C.dimensions[N])
if (A?.preAggregationCTEs?.some((L) => L.cube.name === I)) {
const L = A.preAggregationCTEs.find((a) => a.cube.name === I), M = L.joinKeys.find((a) => a.targetColumn === N);
if (M && M.sourceColumnObj) {
const a = this.dateTimeBuilder.buildTimeDimensionExpression(
M.sourceColumnObj,
s.granularity,
R
);
S.push(a);
} else {
const a = n`${n.identifier(L.cteAlias)}.${n.identifier(N)}`;
S.push(a);
}
} else {
const L = C.dimensions[N], M = this.dateTimeBuilder.buildTimeDimensionExpression(
L.sql,
s.granularity,
R
);
S.push(M);
}
}
return S;
}
}
class $ {
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 calculated