feat: Blog Groups - #27
Conversation
📝 WalkthroughWalkthroughAdds blog groups with database persistence, case-insensitive exact filtering, CLI flags, group-scoped scanning and article listing, migration support, unit tests, and end-to-end coverage. ChangesBlog group support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant Controller
participant Scanner
participant Database
CLI->>Controller: Add blog with group
Controller->>Database: Persist group_name
CLI->>Database: List blogs by group
CLI->>Scanner: Scan all blogs by group
Scanner->>Database: Load matching blogs
Database-->>Scanner: Group-filtered blogs
CLI->>Controller: List articles by group
Controller->>Database: Query group-filtered articles
Database-->>Controller: Matching articles
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/storage/database.go`:
- Around line 163-165: Update both group-name filters in
internal/storage/database.go at lines 163-165 and 395-397 to wrap the
LOWER(group_name) SQL predicates with Squirrel’s expression API, such as
sq.Expr, before passing them to query.Where; preserve the existing conditions
and bound group-name arguments.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d6b04e9f-72f3-49ee-a750-49ac0160a3f8
📒 Files selected for processing (21)
e2e/e2e_test.goe2e/expected/24_add_with_group.txte2e/expected/25_blogs_shows_group.txte2e/expected/26_blogs_filter_group.txte2e/expected/27_blogs_filter_group_no_match.txte2e/expected/28_scan_filter_group.txte2e/expected/29_scan_filter_group_no_match.txte2e/expected/30_articles_filter_group.txte2e/expected/31_blogs_filter_group_multi.txte2e/expected/32_blogs_filter_group_prefix_no_match.txtinternal/cli/commands.gointernal/cli/commands_test.gointernal/controller/controller.gointernal/controller/controller_test.gointernal/model/model.gointernal/scanner/scanner.gointernal/scanner/scanner_test.gointernal/storage/database.gointernal/storage/database_test.gointernal/storage/migrations/000004_add_blog_group.down.sqlinternal/storage/migrations/000004_add_blog_group.up.sql
| if groupName != nil && *groupName != "" { | ||
| query = query.Where("LOWER(group_name) = LOWER(?)", *groupName) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files | rg '(^|/)internal/storage/database\.go$|(^|/)go\.mod$'
echo "== outline =="
ast-grep outline internal/storage/database.go --view expanded || true
echo "== relevant lines =="
cat -n internal/storage/database.go | sed -n '130,180p;370,410p'
echo "== Where usages in file with raw-looking strings =="
rg -n '\.Where\(' internal/storage/database.goRepository: JulienTant/blogwatcher-cli
Length of output: 7129
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== imports and sq package alias =="
cat -n internal/storage/database.go | sed -n '1,40p'
echo "== go module squirrel declarations =="
cat -n go.mod | sed -n '1,160p'
echo "== search for SqExpr / Raw SQL / builder helpers =="
rg -n 'type Sq|func sq::|newSquirrel|RawExpr|Where\("LOWER|Use\(" |Build|SqlString|string\(' .Repository: JulienTant/blogwatcher-cli
Length of output: 5490
🌐 Web query:
Masterminds/squirrel v1.5.4 Where string function signature
💡 Result:
In Masterminds/squirrel version 1.5.4, the Where function signature for builders (such as SelectBuilder and StatementBuilderType) is: func (b Builder) Where(pred interface{}, args...interface{}) Builder The pred argument accepts several types, including a string representing a SQL expression [1][2]. When providing a string, any required SQL placeholders should be passed as subsequent arguments in the args variadic parameter [1][3]. If the pred argument is a string containing placeholders, Squirrel will use the provided args to bind the values to those placeholders [1][3]. If pred is an empty string or nil, the call is ignored [1]. The function will panic if the provided pred is not of a supported type (such as string, map[string]interface{}, or a Sqlizer implementation) [1][4].
Citations:
- 1: https://github.com/Masterminds/squirrel/blob/v1.5.4/select.go
- 2: https://github.com/Masterminds/squirrel/blob/v1.5.4/statement.go
- 3: https://pkg.go.dev/github.com/Masterminds/squirrel@v1.5.4
- 4: https://github.com/Masterminds/squirrel/blob/v1.5.4/where.go
🌐 Web query:
Masterminds/squirrel v1.5.4 Where string accepts SQL function documentation
💡 Result:
In Masterminds/squirrel v1.5.4, the Where method does not have a specific "SQL function" mode, but it fully supports passing custom SQL strings, which allows you to use SQL functions directly [1]. When using the Where method with a string argument, Squirrel treats the string as a raw SQL expression [1]. If the expression contains placeholders (such as the standard? character), you must provide the corresponding arguments as additional parameters to the Where method [1]. For example, to use a SQL function like UPPER or a custom function: users.Where("UPPER(name) =?", "ALICE") If your SQL function requires complex syntax or does not use standard placeholders, you can embed it directly into the string: users.Where("created_at > NOW - INTERVAL '1 day'") The library's documentation explicitly notes that when you pass a string to Where, it is treated as a SQL expression, and it will handle placeholder substitution (and placeholder formatting, if configured) as needed [1][2]. If your use case is highly complex or dynamic, you can also consider creating custom types that implement the Sqlizer interface, which allows for more programmatic control over how the SQL is generated [3].
Citations:
- 1: https://github.com/Masterminds/squirrel/blob/v1.5.4/select.go
- 2: https://github.com/masterminds/squirrel
- 3: https://github.com/Masterminds/squirrel/blob/v1.5.4/where.go
🌐 Web query:
Masterminds/squirrel v1.5.4 pkg.go.dev select.go Where SQL template handling
💡 Result:
The Masterminds/squirrel library (v1.5.4) does not use traditional "SQL templates" (such as Go's text/template or html/template packages) to handle SQL generation [1][2]. Instead, it uses a fluent, builder-based API to construct SQL queries programmatically [2][3]. In select.go, the ToSql method generates a SQL string and a slice of arguments by concatenating various SQL clauses (like SELECT, FROM, WHERE, JOIN) that have been added to the builder [1][4]. The internal logic relies on: 1. Builder Composition: The SelectBuilder accumulates different parts of the query (e.g., columns, joins, where clauses) into a selectData struct [1][5]. 2. Placeholder Substitution: Once the query parts are concatenated into a raw SQL string, Squirrel uses the PlaceholderFormat associated with the builder to replace placeholders (typically "?") with the specific format required by the database driver, such as Dollar signs ($1, $2, etc., for PostgreSQL) [1][4][6]. 3. Argument Handling: Parameters provided to methods like Where or Columns are tracked in a slice of interfaces (args []interface{}), which are returned alongside the final SQL string to ensure safe query execution [1][7][6]. If you are looking for ways to handle complex SQL structures, Squirrel allows you to inject raw SQL fragments using methods like Expr, or by using custom types that implement the Sqlizer interface, which requires a ToSql method [7][8][5]. It is not a template engine; it is a query builder [2][3].
Citations:
- 1: https://github.com/Masterminds/squirrel/blob/v1.5.4/select.go
- 2: https://github.com/masterminds/squirrel
- 3: https://github.com/Masterminds/squirrel/blob/v1.5.0/README.md
- 4: https://github.com/Masterminds/squirrel/blob/master/select.go
- 5: https://chromium.googlesource.com/external/github.com/Masterminds/squirrel/+/d6fdf8c945ecc3706ee4cd333b9139d2e3791efe/select.go
- 6: https://chromium.googlesource.com/external/github.com/Masterminds/squirrel/+/refs/tags/v1.5.2
- 7: https://github.com/Masterminds/squirrel/blob/master/select_test.go
- 8: https://github.com/Masterminds/squirrel/blob/v1.5.4/squirrel.go
Use a Squirrel SQL expression for the group filters.
Both group predicates pass raw SQL strings into Where. Wrap them with Squirrel’s SQL-expression API/converter instead; for example, query.Where(sq.Expr("LOWER(group_name) = LOWER(?)", *groupName)) so the grouped-blog article filter is not evaluated as an existing string predicate.
📍 Affects 1 file
internal/storage/database.go#L163-L165(this comment)internal/storage/database.go#L395-L397
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/storage/database.go` around lines 163 - 165, Update both group-name
filters in internal/storage/database.go at lines 163-165 and 395-397 to wrap the
LOWER(group_name) SQL predicates with Squirrel’s expression API, such as
sq.Expr, before passing them to query.Where; preserve the existing conditions
and bound group-name arguments.
Source: Coding guidelines
Summary
Adds support for blog groups, allowing users to quickly pull articles from many specific blogs at once. Good for organization as well.
Test plan
golangci-lint runpassesgotestsum -- ./...passesSummary by CodeRabbit