Default time format breaks SQL equality after a monotonic time.Time round trip

With the default DSN, a time obtained from time.Now() is stored using Time.String(), including its process-local m=... reading. parseTimeString discards that reading when scanning a DATETIME/DATE/TIMESTAMP column. The decoded value represents the same instant, but binding it again in an equality predicate matches no row.

This makes timestamp-guarded queue UPDATE/DELETE operations silently affect zero rows: retry scheduling or removal can fail even without concurrent changes.

Reproduced on current master b122d041 with Go 1.27.1, linux/arm64. The same formatting/parsing paths are present in v1.60.0 and v1.38.2.

Minimal reproducer

package main

import (
    "database/sql"
    "fmt"
    "time"
    _ "modernc.org/sqlite"
)

func check(err error) {
    if err != nil { panic(err) }
}

func main() {
    db, err := sql.Open("sqlite", ":memory:")
    check(err)
    defer db.Close()
    db.SetMaxOpenConns(1)
    _, err = db.Exec("CREATE TABLE jobs (retry_at DATETIME)")
    check(err)
    now := time.Now()
    _, err = db.Exec("INSERT INTO jobs VALUES (?)", now)
    check(err)
    var seen time.Time
    check(db.QueryRow("SELECT retry_at FROM jobs").Scan(&seen))
    fmt.Println("same instant:", seen.Equal(now))
    result, err := db.Exec("DELETE FROM jobs WHERE retry_at = ?", seen)
    check(err)
    rows, err := result.RowsAffected()
    check(err)
    fmt.Println("deleted rows:", rows)
}

Actual output on a platform where time.Now() includes a monotonic reading:

same instant: true
deleted rows: 0

Expected: the rebound value matches the stored row, so deleted rows: 1.

History and compatibility

  • #35 (closed) / 90dac3ac added time scanning and removes the monotonic suffix. Its regression test checks Time.Equal, which does not detect the SQL predicate failure above.
  • #47 (closed) / !30 (merged) deliberately retained the legacy default string layout and added _time_format=sqlite.
  • _time_format=sqlite is a workaround for new writes; existing strings containing the suffix still need application-controlled normalization.

I will send a small MR that keeps the legacy wall-clock layout but uses t.Round(0).String() for default writes. This omits only the non-persistent monotonic component. Explicit formats remain unchanged, and old stored strings remain readable without silently rewriting data. The visible change to default stored text for monotonic time values should be documented.

Regression coverage includes direct/prepared UPDATE and DELETE predicates for DATE/DATETIME/TIMESTAMP, preservation of non-monotonic output, and reads of legacy strings.