Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | 14x 14x 14x 14x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 14x 14x 14x 14x 26x 26x 26x 26x 26x 26x 26x 26x 14x 14x 14x 14x 40x 30x 40x 26x 14x 42x 42x 42x 42x 73x 73x 73x 42x 15x 15x 15x 325x 23x 302x 22x 22x 15x 1x 15x 15x 15x 15x 15x 15x 22x 22x 22x 15x 13x 13x 13x 25x 25x 25x 13x | /**
* Template Substitution Engine
*
* Handles substitution of {member} references in calculatedSql templates
* with actual SQL expressions while maintaining Drizzle type safety.
*/
import { sql, SQL, StringChunk } from 'drizzle-orm'
import type { Cube, QueryContext } from './types/cube'
/**
* Resolved measure SQL builders
* Maps full measure names (e.g., "Cube.measure") to functions that build their SQL
* Using functions instead of SQL objects avoids mutation and shared reference issues
*/
export type ResolvedMeasures = Map<string, () => SQL>
/**
* Substitution context
*/
export interface SubstitutionContext {
/** The cube being processed */
cube: Cube
/** All available cubes for cross-cube references */
allCubes: Map<string, Cube>
/** Already resolved measure SQL expressions */
resolvedMeasures: ResolvedMeasures
/** Query context for SQL generation */
queryContext: QueryContext
}
/**
* Substitute {member} references in calculatedSql template
*
* Replaces {member} with the corresponding SQL expression from resolvedMeasures.
* Supports both same-cube ({measure}) and cross-cube ({Cube.measure}) references.
*
* @param calculatedSql - Template string with {member} references
* @param context - Substitution context
* @returns SQL expression with substituted values
* @throws Error if referenced measure is not resolved
*/
export function substituteTemplate(
calculatedSql: string,
context: SubstitutionContext
): SQL {
const { cube, allCubes, resolvedMeasures } = context
// Extract all {member} references
const memberRefs = extractMemberReferences(calculatedSql)
// Build substitution map (maps member names to their SQL expressions)
const substitutions = new Map<string, SQL>()
for (const ref of memberRefs) {
const { originalRef, cubeName, fieldName } = ref
// Resolve cube and measure name
const targetCubeName = cubeName || cube.name
const targetCube = allCubes.get(targetCubeName)
Iif (!targetCube) {
throw new Error(
`Cannot substitute {${originalRef}}: cube '${targetCubeName}' not found`
)
}
// Get resolved SQL builder for the measure
const fullMeasureName = `${targetCubeName}.${fieldName}`
const resolvedBuilder = resolvedMeasures.get(fullMeasureName)
Iif (!resolvedBuilder) {
throw new Error(
`Cannot substitute {${originalRef}}: measure '${fullMeasureName}' not resolved yet. ` +
`Ensure measures are resolved in dependency order.`
)
}
// Call the builder function to get a fresh SQL object
// Single wrap is OK here - each builder() call creates fresh SQL that won't be reused
// The builder functions themselves handle isolation via resolveSqlExpression()
const resolvedSql = resolvedBuilder()
const wrappedSql = sql`${resolvedSql}`
substitutions.set(originalRef, wrappedSql)
}
// Build SQL expression by parsing the template and substituting SQL expressions
// We need to build a single sql`` template literal with the substituted expressions
// Build arrays for template parts and values
const sqlParts: string[] = []
const sqlValues: SQL[] = []
let lastIndex = 0
// Find all {member} references and build template parts
for (const ref of memberRefs) {
const pattern = `{${ref.originalRef}}`
const index = calculatedSql.indexOf(pattern, lastIndex)
Eif (index >= 0) {
// Add the string part before this reference
sqlParts.push(calculatedSql.substring(lastIndex, index))
// Add the SQL expression as a value
const resolvedSql = substitutions.get(ref.originalRef)
Eif (resolvedSql) {
sqlValues.push(resolvedSql)
}
lastIndex = index + pattern.length
}
}
// Add any remaining string part
sqlParts.push(calculatedSql.substring(lastIndex))
// Build the final SQL expression
// If no member references, return raw SQL
Iif (sqlValues.length === 0) {
return sql.raw(calculatedSql)
}
// Build SQL using Drizzle's sql.join to avoid mutation
// Keep SQL objects intact instead of spreading their chunks
const parts: (StringChunk | SQL)[] = []
for (let i = 0; i < sqlParts.length; i++) {
// Add string part if non-empty
if (sqlParts[i]) {
parts.push(new StringChunk(sqlParts[i]))
}
// Add SQL value (if any)
if (i < sqlValues.length) {
parts.push(sqlValues[i])
}
}
// Use sql.join with empty separator to concatenate
return sql.join(parts, sql.raw(''))
}
/**
* Member reference extracted from template
*/
interface MemberReference {
/** Original reference as it appears in template (e.g., "measure" or "Cube.measure") */
originalRef: string
/** Cube name if cross-cube reference, null otherwise */
cubeName: string | null
/** Field/measure name */
fieldName: string
}
/**
* Extract all {member} references from calculatedSql template
*
* @param calculatedSql - Template string
* @returns Array of member references
*/
function extractMemberReferences(calculatedSql: string): MemberReference[] {
const regex = /\{([^}]+)\}/g
const matches = calculatedSql.matchAll(regex)
const references: MemberReference[] = []
for (const match of matches) {
const memberRef = match[1].trim()
Iif (memberRef.includes('.')) {
// Cross-cube reference: {Cube.measure}
const [cubeName, fieldName] = memberRef.split('.').map(s => s.trim())
references.push({
originalRef: memberRef,
cubeName,
fieldName
})
} else {
// Same-cube reference: {measure}
references.push({
originalRef: memberRef,
cubeName: null,
fieldName: memberRef
})
}
}
return references
}
/**
* Validate calculatedSql template syntax
*
* @param calculatedSql - Template string to validate
* @returns Validation result
*/
export function validateTemplateSyntax(calculatedSql: string): {
isValid: boolean
errors: string[]
} {
const errors: string[] = []
// Check for unmatched braces
let braceDepth = 0
for (let i = 0; i < calculatedSql.length; i++) {
if (calculatedSql[i] === '{') {
braceDepth++
} else if (calculatedSql[i] === '}') {
braceDepth--
Iif (braceDepth < 0) {
errors.push(`Unmatched closing brace at position ${i}`)
break
}
}
}
if (braceDepth > 0) {
errors.push('Unmatched opening brace in template')
}
// Check for empty references
const emptyRefRegex = /\{\s*\}/
Iif (emptyRefRegex.test(calculatedSql)) {
errors.push('Empty member reference {} found in template')
}
// Check for nested braces
const nestedBraceRegex = /\{[^}]*\{/
Iif (nestedBraceRegex.test(calculatedSql)) {
errors.push('Nested braces are not allowed in member references')
}
// Check for invalid characters in member names
const memberRefs = extractMemberReferences(calculatedSql)
for (const ref of memberRefs) {
const fullRef = ref.cubeName ? `${ref.cubeName}.${ref.fieldName}` : ref.fieldName
// Allow alphanumeric, underscore, and dot
Iif (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(fullRef)) {
errors.push(
`Invalid member reference {${ref.originalRef}}: must start with letter or underscore, ` +
`and contain only letters, numbers, underscores, and dots`
)
}
// Check for multiple dots (only Cube.measure allowed)
Iif (fullRef.split('.').length > 2) {
errors.push(
`Invalid member reference {${ref.originalRef}}: only one dot allowed (Cube.measure format)`
)
}
}
return {
isValid: errors.length === 0,
errors
}
}
/**
* Check if a template contains any member references
*
* @param calculatedSql - Template string to check
* @returns True if template contains {member} references
*/
export function hasMemberReferences(calculatedSql: string): boolean {
return /\{[^}]+\}/.test(calculatedSql)
}
/**
* Get list of all unique member references in a template
*
* @param calculatedSql - Template string
* @returns Array of unique full member names (e.g., ["Cube.measure", "otherMeasure"])
*/
export function getMemberReferences(calculatedSql: string, currentCube: string): string[] {
const refs = extractMemberReferences(calculatedSql)
const uniqueRefs = new Set<string>()
for (const ref of refs) {
const cubeName = ref.cubeName || currentCube
const fullName = `${cubeName}.${ref.fieldName}`
uniqueRefs.add(fullName)
}
return Array.from(uniqueRefs)
}
|