适配mysql

This commit is contained in:
2026-05-13 17:46:03 +08:00
parent 81dc55baaf
commit 61cc29d7b3
12 changed files with 456 additions and 114 deletions
+147
View File
@@ -0,0 +1,147 @@
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
}