Files
gitea/models/migrations/v1_27/v341.go
T
bircni c2f130d352 fix(mssql): convert legacy DATETIME columns to DATETIME2 (#38216)
## Problem

On MSSQL databases created by old Gitea versions, the real datetime
columns `external_login_user.expires_at` and `lfs_lock.created` were
created as `DATETIME`. `DATETIME` parses datetime literals in a
locale-dependent way, so the ISO string `'YYYY-MM-DD HH:MM:SS'` that
xorm sends fails to convert when the session language is not English
(e.g. German defaults to `dmy`):

```
mssql: Bei der Konvertierung eines nvarchar-Datentyps in einen datetime-Datentyp liegt der Wert außerhalb des gültigen Bereichs.
```

This breaks linking an external (OAuth/Keycloak) account to an existing
user, and LFS lock creation, with a 500 error.

## Fix

Current xorm already maps `time.Time` to the locale-independent
`DATETIME2` for new installs, so only legacy databases are affected.
This adds migration `341` that converts these columns to `DATETIME2` on
legacy MSSQL databases (no-op on other databases and on columns already
using `DATETIME2`).

A full audit of persisted `time.Time` columns in `models/` confirmed
these two are the only real datetime columns affected — every other time
value is stored as a unix-timestamp integer.

A regression test (MSSQL-only, mirroring the existing v338 pattern)
downgrades the columns to legacy `DATETIME`, runs the migration, asserts
the type becomes `DATETIME2`, and verifies an ISO datetime insert
succeeds under `SET LANGUAGE German`.

Fixes #38211
2026-06-25 14:38:39 +02:00

81 lines
2.1 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_27
import (
"fmt"
"strings"
"time"
"gitea.dev/models/db"
"gitea.dev/models/migrations/base"
"xorm.io/xorm/schemas"
)
// legacyDateTimeColumns are the persisted real datetime columns that old Gitea
// versions created as MSSQL DATETIME. Every other time value is stored as a
// unix timestamp integer, so these are the only columns affected.
var legacyDateTimeColumns = []struct {
bean any
column string
}{
{new(externalLoginUserWithExpiresAt), "expires_at"},
{new(lfsLockWithCreated), "created"},
}
type externalLoginUserWithExpiresAt struct {
ExpiresAt time.Time
}
func (externalLoginUserWithExpiresAt) TableName() string {
return "external_login_user"
}
type lfsLockWithCreated struct {
Created time.Time `xorm:"created"`
}
func (lfsLockWithCreated) TableName() string {
return "lfs_lock"
}
// FixLegacyMSSQLDateTimeColumns converts legacy locale-dependent DATETIME columns
// to DATETIME2. Databases created by old Gitea versions stored these columns as
// DATETIME, which fails to parse ISO datetime strings ('YYYY-MM-DD HH:MM:SS')
// when the MSSQL session language is not English, breaking external account
// linking and LFS lock creation. New installs already use DATETIME2, so only
// legacy MSSQL columns need converting.
func FixLegacyMSSQLDateTimeColumns(x db.EngineMigration) error {
if x.Dialect().URI().DBType != schemas.MSSQL {
return nil
}
for _, c := range legacyDateTimeColumns {
table, err := x.TableInfo(c.bean)
if err != nil {
return err
}
var dataType string
has, err := x.SQL("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?", table.Name, c.column).Get(&dataType)
if err != nil {
return err
}
if !has || !strings.EqualFold(dataType, "datetime") {
continue
}
column := table.GetColumn(c.column)
if column == nil {
return fmt.Errorf("column %s does not exist in table %s", c.column, table.Name)
}
if err := base.ModifyColumn(x, table.Name, column); err != nil {
return fmt.Errorf("modify %s.%s: %w", table.Name, c.column, err)
}
}
return nil
}