All files / server/adapters postgres-adapter.ts

79.48% Statements 31/39
77.77% Branches 21/27
84.61% Functions 11/13
81.08% Lines 30/37

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                        13x                 244x   14x   25x   65x   18x     106x   10x   3x   3x                       86x   38x   2x   4x   2x   27x   6x   3x   3x   1x                     1x   1x                                 145x               38x   38x     38x               8x               33110x                             276x               44378x      
/**
 * PostgreSQL Database Adapter
 * Implements PostgreSQL-specific SQL generation for time dimensions, string matching, and type casting
 * Extracted from hardcoded logic in executor.ts and multi-cube-builder.ts
 */
 
import { sql, type SQL, type AnyColumn } from 'drizzle-orm'
import type { TimeGranularity } from '../types'
import { BaseDatabaseAdapter } from './base-adapter'
 
export class PostgresAdapter extends BaseDatabaseAdapter {
  getEngineType(): 'postgres' {
    return 'postgres'
  }
 
  /**
   * Build PostgreSQL time dimension using DATE_TRUNC function
   * Extracted from executor.ts:649-670 and multi-cube-builder.ts:306-320
   */
  buildTimeDimension(granularity: TimeGranularity, fieldExpr: AnyColumn | SQL): SQL {
    // PostgreSQL uses DATE_TRUNC with explicit timestamp casting
    switch (granularity) {
      case 'year':
        return sql`DATE_TRUNC('year', ${fieldExpr}::timestamp)`
      case 'quarter':
        return sql`DATE_TRUNC('quarter', ${fieldExpr}::timestamp)`
      case 'month':
        return sql`DATE_TRUNC('month', ${fieldExpr}::timestamp)`
      case 'week':
        return sql`DATE_TRUNC('week', ${fieldExpr}::timestamp)`
      case 'day':
        // Ensure we return the truncated date as a timestamp
        return sql`DATE_TRUNC('day', ${fieldExpr}::timestamp)::timestamp`
      case 'hour':
        return sql`DATE_TRUNC('hour', ${fieldExpr}::timestamp)`
      case 'minute':
        return sql`DATE_TRUNC('minute', ${fieldExpr}::timestamp)`
      case 'second':
        return sql`DATE_TRUNC('second', ${fieldExpr}::timestamp)`
      default:
        // Fallback to the original expression if granularity is not recognized
        return fieldExpr as SQL
    }
  }
 
  /**
   * Build PostgreSQL string matching conditions using ILIKE (case-insensitive)
   * Extracted from executor.ts:807-813 and multi-cube-builder.ts:468-474
   */
  buildStringCondition(fieldExpr: AnyColumn | SQL, operator: 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'like' | 'notLike' | 'ilike' | 'regex' | 'notRegex', value: string): SQL {
    switch (operator) {
      case 'contains':
        return sql`${fieldExpr} ILIKE ${`%${value}%`}`
      case 'notContains':
        return sql`${fieldExpr} NOT ILIKE ${`%${value}%`}`
      case 'startsWith':
        return sql`${fieldExpr} ILIKE ${`${value}%`}`
      case 'endsWith':
        return sql`${fieldExpr} ILIKE ${`%${value}`}`
      case 'like':
        return sql`${fieldExpr} LIKE ${value}`
      case 'notLike':
        return sql`${fieldExpr} NOT LIKE ${value}`
      case 'ilike':
        return sql`${fieldExpr} ILIKE ${value}`
      case 'regex':
        return sql`${fieldExpr} ~* ${value}`
      case 'notRegex':
        return sql`${fieldExpr} !~* ${value}`
      default:
        throw new Error(`Unsupported string operator: ${operator}`)
    }
  }
 
  /**
   * Build PostgreSQL type casting using :: syntax
   * Extracted from various locations where ::timestamp was used
   */
  castToType(fieldExpr: AnyColumn | SQL, targetType: 'timestamp' | 'decimal' | 'integer'): SQL {
    switch (targetType) {
      case 'timestamp':
        return sql`${fieldExpr}::timestamp`
      case 'decimal':
        return sql`${fieldExpr}::decimal`
      case 'integer':
        return sql`${fieldExpr}::integer`
      default:
        throw new Error(`Unsupported cast type: ${targetType}`)
    }
  }
 
 
  /**
   * 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(fieldExpr: AnyColumn | SQL): SQL {
    return sql`COALESCE(AVG(${fieldExpr}), 0)`
  }
 
 
  /**
   * Build PostgreSQL CASE WHEN conditional expression
   */
  buildCaseWhen(conditions: Array<{ when: SQL; then: any }>, elseValue?: any): SQL {
    const cases = conditions.map(c => sql`WHEN ${c.when} THEN ${c.then}`).reduce((acc, curr) => sql`${acc} ${curr}`)
    
    Iif (elseValue !== undefined) {
      return sql`CASE ${cases} ELSE ${elseValue} END`
    }
    return sql`CASE ${cases} END`
  }
 
  /**
   * Build PostgreSQL boolean literal
   * PostgreSQL uses TRUE/FALSE keywords
   */
  buildBooleanLiteral(value: boolean): SQL {
    return value ? sql`TRUE` : sql`FALSE`
  }
 
  /**
   * Convert filter values - PostgreSQL uses native types
   * No conversion needed for PostgreSQL
   */
  convertFilterValue(value: any): any {
    return value
  }
 
  /**
   * Prepare date value for PostgreSQL
   * PostgreSQL accepts Date objects directly
   */
  prepareDateValue(date: Date): any {
    return date
  }
 
  /**
   * PostgreSQL stores timestamps as native timestamp types
   */
  isTimestampInteger(): boolean {
    return false
  }
 
  /**
   * PostgreSQL time dimensions already return proper values
   * No conversion needed
   */
  convertTimeDimensionResult(value: any): any {        
    return value
  }
 
}