From 582621afa04095f6b0625e56f1a282bbe22173ae Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 19 Sep 2026 02:52:14 +0200 Subject: [PATCH] fix(db): force parseTime in user-provided MySQL DSNs A custom DatabaseURL is passed verbatim to sql.Open; without parseTime=True the driver returns DATETIME columns as strings, which fail to scan into time.Time fields after a v3 migration. Closes Dvorinka/cloudreve#40 (upstream cloudreve/cloudreve#2872). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- inventory/client.go | 22 +++++++++++++++++++++- inventory/mysql_dsn_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 inventory/mysql_dsn_test.go diff --git a/inventory/client.go b/inventory/client.go index f339d94a..223386c3 100644 --- a/inventory/client.go +++ b/inventory/client.go @@ -5,6 +5,7 @@ import ( rawsql "database/sql" "database/sql/driver" "fmt" + "strings" "time" "entgo.io/ent/dialect/sql" @@ -74,7 +75,7 @@ func NewRawEntClient(l logging.Logger, config conf.ConfigProvider) (*ent.Client, // If Database connection string provided, use it directly. if dbConfig.DatabaseURL != "" { l.Info("Connect to database with connection string") - client, err = sql.Open(string(confDBType), dbConfig.DatabaseURL) + client, err = sql.Open(string(confDBType), ensureMySQLParseTime(confDBType, dbConfig.DatabaseURL)) } else { switch confDBType { @@ -142,6 +143,25 @@ func NewRawEntClient(l logging.Logger, config conf.ConfigProvider) (*ent.Client, return ent.NewClient(driverOpt), nil } +// ensureMySQLParseTime appends parseTime=True to a user-provided MySQL DSN +// when absent. Without it the driver returns DATETIME columns as strings, +// which fail to scan into time.Time fields (cloudreve/cloudreve#2872). +func ensureMySQLParseTime(dbType conf.DBType, dsn string) string { + if dbType != conf.MySqlDB { + return dsn + } + idx := strings.IndexByte(dsn, '?') + if idx < 0 { + return dsn + "?parseTime=True" + } + for _, p := range strings.Split(dsn[idx+1:], "&") { + if strings.HasPrefix(strings.ToLower(p), "parsetime=") { + return dsn + } + } + return dsn + "&parseTime=True" +} + type sqlite3Driver struct { *sqlite.Driver } diff --git a/inventory/mysql_dsn_test.go b/inventory/mysql_dsn_test.go new file mode 100644 index 00000000..1ae5e959 --- /dev/null +++ b/inventory/mysql_dsn_test.go @@ -0,0 +1,27 @@ +package inventory + +import ( + "testing" + + "github.com/cloudreve/Cloudreve/v4/pkg/conf" + "github.com/stretchr/testify/require" +) + +func TestEnsureMySQLParseTime(t *testing.T) { + dsn := "user:pass@tcp(127.0.0.1:3306)/cloudreve" + require.Equal(t, dsn+"?parseTime=True", ensureMySQLParseTime(conf.MySqlDB, dsn)) + + withParams := dsn + "?charset=utf8mb4" + require.Equal(t, withParams+"&parseTime=True", ensureMySQLParseTime(conf.MySqlDB, withParams)) + + existing := dsn + "?charset=utf8mb4&parseTime=false" + require.Equal(t, existing, ensureMySQLParseTime(conf.MySqlDB, existing)) + + urlForm := "mysql://user:pass@host:3306/cloudreve?tls=true" + require.Equal(t, urlForm+"&parseTime=True", ensureMySQLParseTime(conf.MySqlDB, urlForm)) + + // Non-MySQL DSNs pass through untouched. + pg := "postgresql://user:pass@host:5432/cloudreve?sslmode=require" + require.Equal(t, pg, ensureMySQLParseTime(conf.PostgresDB, pg)) + require.Equal(t, dsn, ensureMySQLParseTime(conf.SQLiteDB, dsn)) +}