#7844·gorm

ParameterizedQueries is not honoured by DB.Scan

Author: OrkhanAlikhanovCreated Aug 27, 2026Updated Aug 27, 2026
Labelstype:with reproduction steps

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:

go
// finisher_api.go
currentLogger, newLogger := config.Logger, logger.Recorder.New()
config.Logger = newLogger

logger.Recorder is seeded at package level from logger.Default, and New() copies that embedded logger:

go
// 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.