fix(playlist): create a smart playlist through the writer

`CreateSmartPlaylist` issued its `INSERT ... RETURNING` through
`QueryContext`, which routes to the query-only read pool, and failed
with "attempt to write a readonly database (8)". No smart playlist
could be created at all, in any real build.

It was invisible because `NewTestDB` shares one in-memory connection
and leaves `readDB` nil, so `reader()` hands back the *writer* under
test: every unit test of that path exercised a handle production does
not have. `TestNoWritesOnTheReadPool` walks the tree for the whole
class, in the same spirit as `TestNoDirectRuntimeEmits` and for the
same reason — a lint pass only sees one build configuration.
This commit is contained in:
2026-08-12 01:18:07 -04:00
parent a37acfcf84
commit 0cf710cf47
2 changed files with 131 additions and 38 deletions
+22 -38
View File
@@ -2565,35 +2565,6 @@ func (s *Service) CreateSmartPlaylist(
)
}
// SAFETY: Hand-crafted INSERT for smart playlist with
// is_smart and smart_rules columns not yet in sqlc schema.
// All values are parameterized.
rows, err := s.db.QueryContext(
`INSERT INTO playlists (name, is_smart, smart_rules)
VALUES (?, 1, ?)
RETURNING id, name, created_at, updated_at`,
trimmed, rulesJSON,
)
if err != nil {
s.logger.Error(
"Failed to create smart playlist",
"name", trimmed, "err", err,
)
return Summary{}, fmt.Errorf(
"failed to create smart playlist: %w", err,
)
}
if !rows.Next() {
_ = rows.Close()
return Summary{}, fmt.Errorf(
"failed to create smart playlist: %w",
errNoRowReturned,
)
}
var (
id int64
retName string
@@ -2601,25 +2572,38 @@ func (s *Service) CreateSmartPlaylist(
updatedAt string
)
if err := rows.Scan(
&id, &retName, &createdAt, &updatedAt,
); err != nil {
_ = rows.Close()
// SAFETY: Hand-crafted INSERT for smart playlist with
// is_smart and smart_rules columns not yet in sqlc schema.
// All values are parameterized.
//
// QueryRowWriter, not QueryContext: this is an INSERT wearing a
// query's shape, and QueryContext routes to the query-only read
// pool. Through that handle it failed with "attempt to write a
// readonly database", i.e. no smart playlist could be created at
// all. A RETURNING clause does not make a write a read.
if err := s.db.QueryRowWriter(
`INSERT INTO playlists (name, is_smart, smart_rules)
VALUES (?, 1, ?)
RETURNING id, name, created_at, updated_at`,
trimmed, rulesJSON,
).Scan(&id, &retName, &createdAt, &updatedAt); err != nil {
s.logger.Error(
"Failed to create smart playlist",
"name", trimmed, "err", err,
)
if errors.Is(err, sql.ErrNoRows) {
return Summary{}, fmt.Errorf(
"failed to create smart playlist: %w",
errNoRowReturned,
)
}
return Summary{}, fmt.Errorf(
"failed to create smart playlist: %w", err,
)
}
// Close before RefreshSmartPlaylist issues its own queries
// (MaxOpenConns=1 test DBs would deadlock).
_ = rows.Close()
s.logger.Info(
"Smart playlist created",
"id", id, "name", retName,