[object Object]

← back to Cli Printing Press

fix(cli): honor --resources filter in dependent sync fan-out (#1047)

e2f3a53c004d42b94a3ab8957d2817221c23d3c3 · 2026-05-11 03:38:29 -0700 · Trevin Chow

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

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)
 	}

← ed693b0d fix(cli): hide raw resource groups when api browser is gener  ·  back to Cli Printing Press  ·  fix(cli): honor auth.prefix on bearer_token specs (#1054) d2770e16 →