All files server/adapters/mysql-adapter.ts

91.89% Statements 34/37
86.95% Branches 20/23
100% Functions 13/13
91.42% Lines 32/35

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                        14x                 283x                       283x     30x     23x   230x 230x       230x                   111x   41x   5x   7x   5x   30x   9x     5x   5x   4x                     9x   3x   3x   3x                       147x               47x   44x 3x   41x               12x               33113x               3x             278x               44379x      
/**
 * MySQL Database Adapter  
 * Implements MySQL-specific SQL generation for time dimensions, string matching, and type casting
 * Provides MySQL equivalents to PostgreSQL functions
 */
 
import { sql, type SQL, type AnyColumn } from 'drizzle-orm'
import type { TimeGranularity } from '../types'
import { BaseDatabaseAdapter } from './base-adapter'
 
export class MySQLAdapter extends BaseDatabaseAdapter {
  getEngineType(): 'mysql' | 'singlestore' {
    return 'mysql'
  }
 
  /**
   * Build MySQL time dimension using DATE_FORMAT function
   * MySQL equivalent to PostgreSQL's DATE_TRUNC
   */
  buildTimeDimension(granularity: TimeGranularity, fieldExpr: AnyColumn | SQL): SQL {
    // MySQL uses DATE_FORMAT with specific format strings for truncation
    const formatMap: Record<TimeGranularity, string> = {
      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'
    }
 
    // Special handling for quarter and week since MySQL doesn't have direct equivalents to PostgreSQL
    switch (granularity) {
      case 'quarter':
        // Calculate quarter start date using QUARTER() function
        return sql`DATE_ADD(MAKEDATE(YEAR(${fieldExpr}), 1), INTERVAL (QUARTER(${fieldExpr}) - 1) * 3 MONTH)`
      case 'week':
        // Get start of week (Monday) using MySQL's week functions
        return sql`DATE_SUB(${fieldExpr}, INTERVAL WEEKDAY(${fieldExpr}) DAY)`
      default:
        const format = formatMap[granularity]
        Iif (!format) {
          // Fallback to original expression if granularity not recognized
          return fieldExpr as SQL
        }
        return sql`STR_TO_DATE(DATE_FORMAT(${fieldExpr}, ${format}), '%Y-%m-%d %H:%i:%s')`
    }
  }
 
  /**
   * 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(fieldExpr: AnyColumn | SQL, operator: 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'like' | 'notLike' | 'ilike' | 'regex' | 'notRegex', value: string): SQL {
    switch (operator) {
      case 'contains':
        return sql`LOWER(${fieldExpr}) LIKE ${`%${value.toLowerCase()}%`}`
      case 'notContains':
        return sql`LOWER(${fieldExpr}) NOT LIKE ${`%${value.toLowerCase()}%`}`
      case 'startsWith':
        return sql`LOWER(${fieldExpr}) LIKE ${`${value.toLowerCase()}%`}`
      case 'endsWith':
        return sql`LOWER(${fieldExpr}) LIKE ${`%${value.toLowerCase()}`}`
      case 'like':
        return sql`${fieldExpr} LIKE ${value}`
      case 'notLike':
        return sql`${fieldExpr} NOT LIKE ${value}`
      case 'ilike':
        // MySQL doesn't have ILIKE, use LOWER() + LIKE for case-insensitive
        return sql`LOWER(${fieldExpr}) LIKE ${value.toLowerCase()}`
      case 'regex':
        return sql`${fieldExpr} REGEXP ${value}`
      case 'notRegex':
        return sql`${fieldExpr} NOT REGEXP ${value}`
      default:
        throw new Error(`Unsupported string operator: ${operator}`)
    }
  }
 
  /**
   * Build MySQL type casting using CAST() function
   * MySQL equivalent to PostgreSQL's :: casting syntax
   */
  castToType(fieldExpr: AnyColumn | SQL, targetType: 'timestamp' | 'decimal' | 'integer'): SQL {
    switch (targetType) {
      case 'timestamp':
        return sql`CAST(${fieldExpr} AS DATETIME)`
      case 'decimal':
        return sql`CAST(${fieldExpr} AS DECIMAL(10,2))`
      case 'integer':
        return sql`CAST(${fieldExpr} AS SIGNED INTEGER)`
      default:
        throw new Error(`Unsupported cast type: ${targetType}`)
    }
  }
 
 
  /**
   * Build MySQL AVG aggregation with IFNULL for NULL handling
   * MySQL AVG returns NULL for empty sets, using IFNULL for consistency
   */
  buildAvg(fieldExpr: AnyColumn | SQL): SQL {
    return sql`IFNULL(AVG(${fieldExpr}), 0)`
  }
 
 
  /**
   * Build MySQL 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}`)
    
    if (elseValue !== undefined) {
      return sql`CASE ${cases} ELSE ${elseValue} END`
    }
    return sql`CASE ${cases} END`
  }
 
  /**
   * Build MySQL boolean literal
   * MySQL uses TRUE/FALSE keywords (equivalent to 1/0)
   */
  buildBooleanLiteral(value: boolean): SQL {
    return value ? sql`TRUE` : sql`FALSE`
  }
 
  /**
   * Convert filter values - MySQL uses native types
   * No conversion needed for MySQL
   */
  convertFilterValue(value: any): any {
    return value
  }
 
  /**
   * Prepare date value for MySQL
   * MySQL accepts Date objects directly
   */
  prepareDateValue(date: Date): any {
    return date
  }
 
  /**
   * MySQL stores timestamps as native timestamp types
   */
  isTimestampInteger(): boolean {
    return false
  }
 
  /**
   * MySQL time dimensions already return proper values
   * No conversion needed
   */
  convertTimeDimensionResult(value: any): any {
    return value
  }
 
}