You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: prevent PathsGenerator crash when keboola.variables config has multiple parents
When a keboola.variables config is referenced by more than one consumer
config via the API, the relation mapper creates multiple variablesFor
relations on it. The single-pass AfterRemoteOperation removes these
duplicates and emits a warning — but only when consumers are iterated
before the variables config. If the variables config is iterated first
(the order used by the Templates service), the duplicate relations are
not cleaned up before PathsGenerator runs, causing it to crash with
"unexpected state: multiple parents defined by relations".
Fix: change Config.ParentKey() to fall back to the structural parent
(branch) when Relations.ParentKey() returns a multiple-parents error,
instead of propagating the fatal error. This makes PathsGenerator
non-fatal for the shared-variables scenario regardless of iteration
order. The duplicate-relations warning is still emitted correctly by
validateRelations in AfterRemoteOperation.
Also reverts the earlier two-pass link.go change which introduced
different warning messages and broke the variables-used-twice E2E test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/relations-validation.md
+26-16Lines changed: 26 additions & 16 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,35 +29,44 @@ Keboola's Storage API allows a variables config to be referenced by more than on
29
29
30
30
## The `multiple parents` guard
31
31
32
-
`Relations.ParentKey()` (`internal/pkg/model/relation.go:66`) collects all relations that define a parent key. If more than one is found it returns a fatal error:
32
+
`Relations.ParentKey()` (`internal/pkg/model/relation.go:66`) collects all relations that define a parent key. If more than one is found it returns an error:
33
33
34
34
```
35
35
unexpected state: multiple parents defined by "relations" in <config desc>
36
36
```
37
37
38
-
This guard exists to protect the CLI sync engine: without it, path generation would be ambiguous and the local directory structure would be corrupted.
38
+
This guard exists to detect invalid state in the relation graph.
39
39
40
-
## The ordering bug that broke Templates
40
+
## Config.ParentKey() and PathsGenerator
41
41
42
-
The relation mapper (`internal/pkg/mapper/relations/link.go`) processes objects in `AfterRemoteOperation` and `AfterLocalOperation`. It also validates them in the same loop iteration (link → validate per object). This is order-dependent:
42
+
`Config.ParentKey()`(`internal/pkg/model/object.go`) calls `Relations.ParentKey()` to find the relation-defined parent. If that returns an error (multiple parents) or nil (no parent), it falls back to the structural parent — the branch.
43
43
44
-
1. If the variables config Y is iterated **before** its consumers X and Z, `validateRelations(Y)` runs when Y has zero`variablesFor` relations — nothing to clean up.
45
-
2. Later iterations for X and Z call `linkRelations`, which adds `VariablesForRelation` entries to Y.
46
-
3. After the mapper loop, `PathsGenerator.Invoke()` (called in `remote/manager.go`) calls `Y.ParentKey()`, finds two parents, and returns the fatal error.
44
+
`PathsGenerator.doUpdate()` (`internal/pkg/state/local/paths.go`) calls `object.ParentKey()` on every loaded object to build local directory paths. With the branch-fallback in `Config.ParentKey()`, a variables config that has multiple`variablesFor` relations is placed at the branch root (e.g. `main/variables/`) rather than inside any specific parent folder. This is non-fatal: PathsGenerator can complete and remote state loading succeeds.
45
+
46
+
## The ordering issue
47
47
48
-
CLI `pull` was unaffected in practice because the API happened to return consumer configs before variables configs, making consumers iterated first. The Templates service load path had a different iteration order and hit the bug.
48
+
The relation mapper (`internal/pkg/mapper/relations/link.go`) processes objects in `AfterRemoteOperation` and `AfterLocalOperation`. It links and validates each object in a single pass:
49
49
50
-
## The fix: two-pass link and validate
50
+
```
51
+
for each object:
52
+
linkRelations(object) // adds other-side relations to OTHER objects
53
+
validateRelations(object) // validates THIS object's relations
54
+
```
55
+
56
+
This is order-dependent. If the variables config Y is iterated **before** its consumers X and Z:
57
+
58
+
1.`validateRelations(Y)` runs when Y has zero `variablesFor` relations — nothing to clean up.
59
+
2. Later iterations for X and Z call `linkRelations`, which adds `VariablesForRelation` entries to Y.
60
+
3. Y ends up with two `variablesFor` relations in the loaded remote state.
61
+
4.`PathsGenerator.doUpdate()` calls `Y.ParentKey()` — before the branch-fallback fix this was a fatal error.
51
62
52
-
`AfterRemoteOperation` and `AfterLocalOperation` were refactored into two explicit passes:
63
+
CLI `pull` was unaffected in practice because the API happened to return consumer configs before variables configs, making consumers iterated first. The Templates service load path had a different iteration order and triggered step 3-4.
53
64
54
-
**Pass 1 — link all objects**
55
-
Run `linkRelations` for every loaded object. This creates all other-side relations (including adding `variablesFor` entries to every variables config) before any validation happens.
65
+
## The fix: branch fallback in Config.ParentKey()
56
66
57
-
**Pass 2 — validate all objects**
58
-
Run `validateRelations` for every loaded object. With the complete relation graph now in place, duplicates are correctly detected, removed, and logged as warnings — regardless of the order objects appear in the API response.
67
+
`Config.ParentKey()` was changed to ignore the "multiple parents" error from `Relations.ParentKey()` and fall back to the branch (structural parent) instead of propagating the error. This makes PathsGenerator non-fatal for shared-variables configs regardless of iteration order.
59
68
60
-
This makes behaviour order-independent: both CLI and the Templates service emit a warning and continue when a variables config has multiple parents, instead of crashing. No new flags or special-casing were required.
69
+
The `validateRelations` function still detects the duplicate `variablesFor` relations and logs a warning. In CLI `pull`, this warning fires correctly when consumers are iterated before the variables config (the API's typical return order). In the Templates service load path the warning may fire differently (or not at all for the duplicate, depending on iteration order), but Templates is only reading remote state — it never generates a local directory hierarchy — so the warning is not critical there.
61
70
62
71
## Why this is warning-only, not a hard error
63
72
@@ -69,8 +78,9 @@ Templates only reads remote state to discover existing configs and apply templat
0 commit comments