All files adapters/utils.ts

89.04% Statements 65/73
80.39% Branches 41/51
90% Functions 18/20
88.57% Lines 62/70

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 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328                                  45x 45x 45x 45x 45x   45x 6x 3x             135x 135x 135x             135x 135x 135x   135x                                                         126x 126x 126x       126x                       36x 36x 9x       27x   27x 27x 27x     36x 27x 27x       36x         36x               36x       36x         27x 27x       27x     27x 27x                     27x 27x             27x                                                             126x 126x 126x 126x   126x                                                       66x   66x             66x               3x 3x                     30x                     48x                 75x                                           33x     69x     54x         33x 69x   54x           15x                   33x  
/**
 * Shared utilities for framework adapters
 * Common functions used across Express, Fastify, Next.js, and Hono adapters
 */
 
import { format } from 'sql-formatter'
import type {
  SemanticLayerCompiler,
  SemanticQuery,
  SecurityContext,
  QueryAnalysis
} from '../server'
 
/**
 * Calculate query complexity based on query structure
 */
export function calculateQueryComplexity(query: SemanticQuery): string {
  let complexity = 0
  complexity += (query.measures?.length || 0) * 1
  complexity += (query.dimensions?.length || 0) * 1
  complexity += (query.filters?.length || 0) * 2
  complexity += (query.timeDimensions?.length || 0) * 3
  
  if (complexity <= 5) return 'low'
  if (complexity <= 15) return 'medium'
  return 'high'
}
 
/**
 * Generate a unique request ID
 */
export function generateRequestId(): string {
  const timestamp = Date.now()
  const random = Math.random().toString(36).substring(2, 9)
  return `${timestamp}-${random}`
}
 
/**
 * Build transformed query metadata for Cube.js compatibility
 */
export function buildTransformedQuery(query: SemanticQuery): any {
  const sortedDimensions = query.dimensions || []
  const sortedTimeDimensions = query.timeDimensions || []
  const measures = query.measures || []
 
  return {
    sortedDimensions,
    sortedTimeDimensions,
    timeDimensions: sortedTimeDimensions,
    measures,
    leafMeasureAdditive: true,
    leafMeasures: measures,
    measureToLeafMeasures: {},
    hasNoTimeDimensionsWithoutGranularity: true,
    allFiltersWithinSelectedDimensions: true,
    isAdditive: true,
    granularityHierarchies: {},
    hasMultipliedMeasures: false,
    hasCumulativeMeasures: false,
    windowGranularity: null,
    filterDimensionsSingleValueEqual: {},
    ownedDimensions: sortedDimensions,
    ownedTimeDimensionsWithRollupGranularity: [],
    ownedTimeDimensionsAsIs: [],
    allBackAliasMembers: {},
    hasMultiStage: false
  }
}
 
/**
 * Get database type from semantic layer
 */
export function getDatabaseType(semanticLayer: SemanticLayerCompiler): string {
  // Extract from the semantic layer's database executor
  Eif (semanticLayer.hasExecutor()) {
    const executor = (semanticLayer as any).databaseExecutor
    Iif (executor?.engineType) {
      return executor.engineType
    }
  }
  return 'postgres' // default fallback
}
 
/**
 * Helper function to handle dry-run logic for all adapters
 */
export async function handleDryRun(
  query: SemanticQuery, 
  securityContext: SecurityContext,
  semanticLayer: SemanticLayerCompiler
) {
  // Validate query structure and field existence
  const validation = semanticLayer.validateQuery(query)
  if (!validation.isValid) {
    throw new Error(`Query validation failed: ${validation.errors.join(', ')}`)
  }
 
  // Get all referenced cubes from measures and dimensions
  const referencedCubes = new Set<string>()
  
  query.measures?.forEach(measure => {
    const cubeName = measure.split('.')[0]
    referencedCubes.add(cubeName)
  })
  
  query.dimensions?.forEach(dimension => {
    const cubeName = dimension.split('.')[0]
    referencedCubes.add(cubeName)
  })
 
  // Also include cubes from timeDimensions and filters
  query.timeDimensions?.forEach(timeDimension => {
    const cubeName = timeDimension.dimension.split('.')[0]
    referencedCubes.add(cubeName)
  })
 
  query.filters?.forEach(filter => {
    if ('member' in filter) {
      const cubeName = filter.member.split('.')[0]
      referencedCubes.add(cubeName)
    }
  })
 
  // Determine if this is a multi-cube query
  const isMultiCube = referencedCubes.size > 1
 
  // Generate SQL using the semantic layer compiler
  let sqlResult
  Iif (isMultiCube) {
    // For multi-cube queries, use the new multi-cube SQL generation
    sqlResult = await semanticLayer.generateMultiCubeSQL(query, securityContext)
  } else {
    // For single cube queries, use the cube-specific SQL generation
    const cubeName = Array.from(referencedCubes)[0]
    sqlResult = await semanticLayer.generateSQL(cubeName, query, securityContext)
  }
 
  // Create normalized queries array (for Cube.js compatibility)
  const normalizedQueries = Array.from(referencedCubes).map(cubeName => ({
    cube: cubeName,
    query: {
      measures: query.measures?.filter(m => m.startsWith(cubeName + '.')) || [],
      dimensions: query.dimensions?.filter(d => d.startsWith(cubeName + '.')) || [],
      filters: query.filters || [],
      timeDimensions: query.timeDimensions || [],
      order: query.order || {},
      limit: query.limit,
      offset: query.offset
    }
  }))
 
  // Generate query analysis for debugging transparency
  let analysis: QueryAnalysis | undefined
  try {
    analysis = semanticLayer.analyzeQuery(query, securityContext)
  } catch (analysisError) {
    // Analysis is optional - don't fail the dry-run if it fails
    console.warn('Query analysis failed:', analysisError)
  }
 
  // Build comprehensive response
  return {
    queryType: "regularQuery",
    normalizedQueries,
    queryOrder: Array.from(referencedCubes),
    transformedQueries: normalizedQueries,
    pivotQuery: {
      query,
      cubes: Array.from(referencedCubes)
    },
    sql: {
      sql: [sqlResult.sql],
      params: sqlResult.params || []
    },
    complexity: calculateQueryComplexity(query),
    valid: true,
    cubesUsed: Array.from(referencedCubes),
    joinType: isMultiCube ? "multi_cube_join" : "single_cube",
    query,
    // Query analysis for debugging and transparency
    analysis
  }
}
 
/**
 * Format standard Cube.js API response
 */
export function formatCubeResponse(
  query: SemanticQuery,
  result: { data: any[]; annotation?: any },
  semanticLayer: SemanticLayerCompiler
) {
  const dbType = getDatabaseType(semanticLayer)
  const requestId = generateRequestId()
  const lastRefreshTime = new Date().toISOString()
  const transformedQuery = buildTransformedQuery(query)
 
  return {
    queryType: "regularQuery",
    results: [{
      query,
      lastRefreshTime,
      usedPreAggregations: {},
      transformedQuery,
      requestId,
      annotation: result.annotation,
      dataSource: "default",
      dbType,
      extDbType: dbType,
      external: false,
      slowQuery: false,
      data: result.data
    }],
    pivotQuery: {
      ...query,
      queryType: "regularQuery"
    },
    slowQuery: false
  }
}
 
/**
 * Format SQL string using sql-formatter with appropriate dialect
 */
export function formatSqlString(sqlString: string, engineType: 'postgres' | 'mysql' | 'sqlite' | 'singlestore'): string {
  try {
    // Map drizzle-cube engine types to sql-formatter language options
    const dialectMap = {
      postgres: 'postgresql',
      mysql: 'mysql',
      sqlite: 'sqlite',
      singlestore: 'mysql'  // SingleStore uses MySQL dialect for formatting
    } as const
    
    return format(sqlString, {
      language: dialectMap[engineType],
      tabWidth: 2,
      keywordCase: 'upper',
      indentStyle: 'standard'
    })
  } catch (error) {
    // If formatting fails, return original SQL
    console.warn('SQL formatting failed:', error)
    return sqlString
  }
}
 
/**
 * Format SQL generation response
 */
export function formatSqlResponse(
  query: SemanticQuery,
  sqlResult: { sql: string; params?: any[] }
) {
  return {
    sql: sqlResult.sql,
    params: sqlResult.params || [],
    query
  }
}
 
/**
 * Format metadata response
 */
export function formatMetaResponse(metadata: any) {
  return {
    cubes: metadata
  }
}
 
/**
 * Standard error response format
 */
export function formatErrorResponse(error: string | Error, status: number = 500) {
  return {
    error: error instanceof Error ? error.message : error,
    status
  }
}
 
/**
 * Handle batch query requests - wrapper around existing single query execution
 * Executes multiple queries in parallel and returns partial success results
 *
 * @param queries - Array of semantic queries to execute
 * @param securityContext - Security context (extracted once, shared across all queries)
 * @param semanticLayer - Semantic layer compiler instance
 * @returns Array of results matching input query order (successful or error results)
 */
export async function handleBatchRequest(
  queries: SemanticQuery[],
  securityContext: SecurityContext,
  semanticLayer: SemanticLayerCompiler
) {
  // Execute all queries in parallel using Promise.allSettled for partial success
  // This ensures one failing query doesn't affect others
  const settledResults = await Promise.allSettled(
    queries.map(async (query) => {
      // Use EXISTING single query execution logic - NO CODE DUPLICATION
      const result = await semanticLayer.executeMultiCubeQuery(query, securityContext)
 
      // Use EXISTING response formatter - NO CODE DUPLICATION
      return formatCubeResponse(query, result, semanticLayer)
    })
  )
 
  // Transform Promise.allSettled results to match expected format
  const results = settledResults.map((settledResult, index) => {
    if (settledResult.status === 'fulfilled') {
      // Query succeeded - return the formatted result with success flag
      return {
        success: true,
        ...settledResult.value
      }
    } else {
      // Query failed - return error information
      return {
        success: false,
        error: settledResult.reason instanceof Error
          ? settledResult.reason.message
          : String(settledResult.reason),
        query: queries[index] // Include the query that failed for debugging
      }
    }
  })
 
  return { results }
}