package dbcompat import ( "strings" "gorm.io/gorm" ) // CalendarSeed is a MySQL-version-neutral replacement for LAG/LEAD over the // trade calendar. Keeping the adjacent dates in Go avoids depending on MySQL 8. type CalendarSeed struct { TradeDate string PreviousTradeDate string PreviousTwoTradeDate string NextTradeDate string } type calendarDateRow struct { TradeDate string `gorm:"column:trade_date"` } type columnCollationRow struct { CollationName string `gorm:"column:collation_name"` } // LoadSSEOpenCalendarSeeds loads all SSE open dates and builds the date window // needed by range queries. The returned seeds are limited to [startDate, endDate]. func LoadSSEOpenCalendarSeeds(db *gorm.DB, startDate, endDate string) ([]CalendarSeed, error) { var rows []calendarDateRow if err := db.Table("trade_calendar"). Select("DATE_FORMAT(cal_date, '%Y-%m-%d') AS trade_date"). Where("exchange = ? AND is_open = ?", "SSE", 1). Order("cal_date ASC"). Scan(&rows).Error; err != nil { return nil, err } seeds := make([]CalendarSeed, 0) for i, row := range rows { if row.TradeDate < startDate || row.TradeDate > endDate { continue } seed := CalendarSeed{TradeDate: row.TradeDate} if i > 0 { seed.PreviousTradeDate = rows[i-1].TradeDate } if i > 1 { seed.PreviousTwoTradeDate = rows[i-2].TradeDate } if i+1 < len(rows) { seed.NextTradeDate = rows[i+1].TradeDate } seeds = append(seeds, seed) } return seeds, nil } // CalendarSeedTable converts calendar seeds into a derived table that works on // MySQL 5.7, MySQL 8, and MariaDB without CTE/window-function support. func CalendarSeedTable(seeds []CalendarSeed) (string, []any) { if len(seeds) == 0 { return `SELECT CAST(NULL AS DATE) AS trade_date, CAST(NULL AS DATE) AS previous_trade_date, CAST(NULL AS DATE) AS previous_two_trade_date, CAST(NULL AS DATE) AS next_trade_date WHERE 1 = 0`, nil } parts := make([]string, 0, len(seeds)) args := make([]any, 0, len(seeds)*4) for _, seed := range seeds { parts = append(parts, `SELECT CAST(? AS DATE) AS trade_date, CAST(? AS DATE) AS previous_trade_date, CAST(? AS DATE) AS previous_two_trade_date, CAST(? AS DATE) AS next_trade_date`) args = append(args, nullableDate(seed.TradeDate), nullableDate(seed.PreviousTradeDate), nullableDate(seed.PreviousTwoTradeDate), nullableDate(seed.NextTradeDate), ) } return strings.Join(parts, "\nUNION ALL\n"), args } // TsCodeEqual keeps joins free of hard-coded COLLATE clauses. This avoids // binding SQL to MySQL-8-only collations while preserving index-friendly joins. func TsCodeEqual(left, right string) string { return left + " = " + right } // ColumnCollation returns the current database collation for one column. func ColumnCollation(db *gorm.DB, tableName, columnName string) (string, error) { var row columnCollationRow err := db.Raw(` SELECT COLLATION_NAME AS collation_name FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1`, tableName, columnName).Scan(&row).Error if err != nil { return "", err } return row.CollationName, nil } // TsCodeEqualUnicode compares a utf8mb4_unicode_ci column with a column that // may be utf8mb4_0900_ai_ci. utf8mb4_unicode_ci exists on both MySQL 5.7 and 8. func TsCodeEqualUnicode(left, right string) string { return left + " = " + right + " COLLATE utf8mb4_unicode_ci" } // TsCodeEqualRightCollation compares two stock-code columns using a collation // known to exist on the connected database. This avoids MySQL-version-specific // literals while preserving the fast side of joins such as stock_daily.ts_code. func TsCodeEqualRightCollation(left, right, collation string) string { if !safeCollationName(collation) { return TsCodeEqual(left, right) } return left + " = " + right + " COLLATE " + collation } func safeCollationName(collation string) bool { if collation == "" { return false } for _, r := range collation { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { continue } return false } return true } func nullableDate(date string) any { if strings.TrimSpace(date) == "" { return nil } return date }