ParameterizedQueries is not honoured by DB.Scan
GORM Playground Link
https://github.com/go-gorm/playground/pull/858
Description
With a logger configured ParameterizedQueries: true, statements are logged with placeholders as expected — except from DB.Scan, which logs the interpolated parameter values.
Same logger, same WHERE name = ? in all four cases:
| query | logged SQL |
|---|---|
Where("name = ?", secret).Find(&users) |
SELECT * FROM `users` WHERE name = ? |
Model(&User{}).Where("name = ?", secret).Pluck("name", &names) |
SELECT `name` FROM `users` WHERE name = ? |
Raw("SELECT name FROM users WHERE name = ?", secret).Row() |
SELECT name FROM users WHERE name = ? |
Model(&User{}).Where("name = ?", secret).Scan(&out) |
SELECT * FROM `users` WHERE name = "SHOULD_NOT_BE_LOGGED" ⚠️ |
It isn't specific to raw SQL — the Raw query is fine, while the plain model query with Scan is not. Tested against v1.31.2; the playground PR runs against master.
Cause
DB.Scan replaces the connection's logger with a recorder for the inner execution:
// finisher_api.go
currentLogger, newLogger := config.Logger, logger.Recorder.New()
config.Logger = newLoggerlogger.Recorder is seeded at package level from logger.Default, and New() copies that embedded logger:
// logger/logger.go
Recorder = traceRecorder{Interface: Default, BeginAt: time.Now()}
func (l *traceRecorder) New() *traceRecorder {
return &traceRecorder{Interface: l.Interface, BeginAt: time.Now()}
}So the connection's logger, and its Config, aren't consulted while the query runs. callbacks.go asks the current logger for ParamsFilter, which is now the recorder, and traceRecorder.ParamsFilter can only delegate to the package-level RecorderParamsFilter — which defaults to passing params through. By the time the real logger is called, it receives an already-rendered SQL string.
Workaround, and why I'm raising it
Setting RecorderParamsFilter (added in #7336) does cover this path, and it's what we use. Being a package-level variable, it applies process-wide: two connections opened with different ParameterizedQueries settings still behave the same on Scan, and it isn't safe to change once connections are in use.
Possible direction
Seeding the recorder from config.Logger and having traceRecorder.ParamsFilter prefer the wrapped logger's filter, with RecorderParamsFilter kept as the fallback, would honour the connection's setting without affecting anyone relying on the global. I tried this locally against v1.31.2 — the playground test passes, and a logger that doesn't implement ParamsFilter still goes through the global.
One detail if you'd like it done that way: the check needs a small interface declared inside the logger package rather than gorm.ParamsFilter, since referencing that would be an import cycle. Dropping traceRecorder's own ParamsFilter and relying on embedding won't work either, because the embedded field's type is logger.Interface, which doesn't declare the method.
Source: go-gorm/gorm