fix(cli): honor --resources filter in dependent sync fan-out (#1047)
syncDependentResources iterated dependentResourceDefs() unconditionally, so `sync --resources <name>` only filtered flat resources. Dependents ran on every resource, producing unexpected work and retry storms on parent tables the user never requested. Snapshot the user's --resources slice before defaults expand, then thread it into syncDependentResources. A dependent runs when its parent table or its own name appears in the filter; an empty filter (no --resources flag) preserves the existing "sync everything" default. Wrapped in the existing .DependentSyncResources guard so CLIs without dependents emit no extra code.
Files touched
M internal/generator/generator_test.goM internal/generator/templates/sync.go.tmplM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
Diff
commit e2f3a53c004d42b94a3ab8957d2817221c23d3c3
Author: Trevin Chow <trevin@trevinchow.com>
Date: Mon May 11 03:38:29 2026 -0700
fix(cli): honor --resources filter in dependent sync fan-out (#1047)
syncDependentResources iterated dependentResourceDefs() unconditionally,
so `sync --resources <name>` only filtered flat resources. Dependents
ran on every resource, producing unexpected work and retry storms on
parent tables the user never requested.
Snapshot the user's --resources slice before defaults expand, then thread
it into syncDependentResources. A dependent runs when its parent table
or its own name appears in the filter; an empty filter (no --resources
flag) preserves the existing "sync everything" default. Wrapped in the
existing .DependentSyncResources guard so CLIs without dependents emit
no extra code.
---
internal/generator/generator_test.go | 13 +++++++++++++
internal/generator/templates/sync.go.tmpl | 19 +++++++++++++++++--
.../printing-press-golden/internal/cli/sync.go | 16 ++++++++++++++--
3 files changed, 44 insertions(+), 4 deletions(-)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 943720b5..d9b15871 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -5926,6 +5926,19 @@ func TestGenerateDependentSyncCompiles(t *testing.T) {
assert.Contains(t, syncContent, `"messages"`, "sync.go should reference messages as a dependent resource")
assert.Contains(t, syncContent, `"channels"`, "sync.go should reference channels as the parent")
+ // --resources must filter the dependent fan-out the same way it filters
+ // flat resources. The capture must happen before defaults expand the
+ // slice, and the filter must flow through to syncDependentResources
+ // where it gates each dependent.
+ assert.Contains(t, syncContent, "parentFilter := append([]string(nil), resources...)",
+ "sync.go should capture user --resources filter before default expansion")
+ assert.Contains(t, syncContent, "maxPages, parentFilter",
+ "sync.go should pass the captured filter into syncDependentResources")
+ assert.Contains(t, syncContent, "parentFilter []string",
+ "syncDependentResources should accept a parent filter")
+ assert.Contains(t, syncContent, "!allow[dep.ParentTable] && !allow[dep.Name]",
+ "syncDependentResources should gate dependents on parent name or dep name")
+
// The generated project should compile and the generated store tests
// should pass — including TestUpsertBatch_SetsMessagesParentID, which
// verifies dependent-resource sync fills the typed parent_id column
diff --git a/internal/generator/templates/sync.go.tmpl b/internal/generator/templates/sync.go.tmpl
index 495aafa4..25e5dd6f 100644
--- a/internal/generator/templates/sync.go.tmpl
+++ b/internal/generator/templates/sync.go.tmpl
@@ -111,6 +111,12 @@ Exit codes & warnings:
}
defer db.Close()
+{{- if .DependentSyncResources}}
+ // Snapshot before defaults expand, so an empty user filter stays empty
+ // and dependents inherit "sync everything" instead of the default list.
+ parentFilter := append([]string(nil), resources...)
+{{- end}}
+
// If no specific resources, sync top-level resources
if len(resources) == 0 {
resources = defaultSyncResources()
@@ -224,7 +230,7 @@ Exit codes & warnings:
{{- if .DependentSyncResources}}
// Sync dependent (parent-child) resources sequentially after flat resources.
- depResults := syncDependentResources(c, db, sinceTS, full, maxPages{{if .Pagination.DateRangeParam}}, syncDates{{end}})
+ depResults := syncDependentResources(c, db, sinceTS, full, maxPages, parentFilter{{if .Pagination.DateRangeParam}}, syncDates{{end}})
for _, res := range depResults {
if res.Err != nil {
if humanFriendly {
@@ -1056,12 +1062,21 @@ func dependentResourceDefs() []dependentResourceDef {
}
// syncDependentResources iterates parent tables and syncs child resources per parent ID.
+// parentFilter mirrors the user's --resources flag (empty = sync everything). A dependent
+// runs when its parent table or its own name appears in the filter.
func syncDependentResources(c {{if .HasTierRouting}}*client.Client{{else}}interface {
Get(string, map[string]string) (json.RawMessage, error)
RateLimit() float64
-}{{end}}, db *store.Store, sinceTS string, full bool, maxPages int{{if .Pagination.DateRangeParam}}, dates string{{end}}) []syncResult {
+}{{end}}, db *store.Store, sinceTS string, full bool, maxPages int, parentFilter []string{{if .Pagination.DateRangeParam}}, dates string{{end}}) []syncResult {
+ allow := make(map[string]bool, len(parentFilter))
+ for _, r := range parentFilter {
+ allow[r] = true
+ }
var results []syncResult
for _, dep := range dependentResourceDefs() {
+ if len(allow) > 0 && !allow[dep.ParentTable] && !allow[dep.Name] {
+ continue
+ }
res := syncDependentResource({{if .HasTierRouting}}syncClientForResource(c, dep.Name){{else}}c{{end}}, db, dep, sinceTS, full, maxPages{{if .Pagination.DateRangeParam}}, dates{{end}})
results = append(results, res)
}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
index 939112fe..35d48144 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
@@ -99,6 +99,9 @@ Exit codes & warnings:
return fmt.Errorf("opening local database: %w", err)
}
defer db.Close()
+ // Snapshot before defaults expand, so an empty user filter stays empty
+ // and dependents inherit "sync everything" instead of the default list.
+ parentFilter := append([]string(nil), resources...)
// If no specific resources, sync top-level resources
if len(resources) == 0 {
@@ -206,7 +209,7 @@ Exit codes & warnings:
}
}
// Sync dependent (parent-child) resources sequentially after flat resources.
- depResults := syncDependentResources(c, db, sinceTS, full, maxPages)
+ depResults := syncDependentResources(c, db, sinceTS, full, maxPages, parentFilter)
for _, res := range depResults {
if res.Err != nil {
if humanFriendly {
@@ -954,12 +957,21 @@ func dependentResourceDefs() []dependentResourceDef {
}
// syncDependentResources iterates parent tables and syncs child resources per parent ID.
+// parentFilter mirrors the user's --resources flag (empty = sync everything). A dependent
+// runs when its parent table or its own name appears in the filter.
func syncDependentResources(c interface {
Get(string, map[string]string) (json.RawMessage, error)
RateLimit() float64
-}, db *store.Store, sinceTS string, full bool, maxPages int) []syncResult {
+}, db *store.Store, sinceTS string, full bool, maxPages int, parentFilter []string) []syncResult {
+ allow := make(map[string]bool, len(parentFilter))
+ for _, r := range parentFilter {
+ allow[r] = true
+ }
var results []syncResult
for _, dep := range dependentResourceDefs() {
+ if len(allow) > 0 && !allow[dep.ParentTable] && !allow[dep.Name] {
+ continue
+ }
res := syncDependentResource(c, db, dep, sinceTS, full, maxPages)
results = append(results, res)
}