{"record":{"id":"62dec643750e05ff","repo":"siyuan-note/siyuan","slug":"chained-or-cyclic-asset-mappings-are-not-supported","errorCode":null,"errorMessage":"chained or cyclic asset mappings are not supported","messagePattern":"chained or cyclic asset mappings are not supported","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/asset_relink_batch.go","lineNumber":99,"sourceCode":"\t\tif rule.oldPath != \"\" {\n\t\t\tif seen[rule.oldPath] {\n\t\t\t\tvalidationErr = fmt.Errorf(\"duplicate oldPath: %s\", mapping.OldPath)\n\t\t\t}\n\t\t\tseen[rule.oldPath] = true\n\t\t}\n\t\tif err != nil {\n\t\t\titem.inputError = true\n\t\t\tp.fail(item, err.Error())\n\t\t\tcontinue\n\t\t}\n\t\tp.routes[rule.oldPath] = rule\n\t}\n\tif validationErr != nil {\n\t\treturn p, validationErr\n\t}\n\tfor _, item := range p.items {\n\t\tif item.reason == \"\" && item.rule.newPath != item.rule.oldPath && seen[item.rule.newPath] {\n\t\t\treturn p, errors.New(\"chained or cyclic asset mappings are not supported\")\n\t\t}\n\t}\n\treturn p, nil\n}\n\nfunc runAssetRelinks(ctx context.Context, mappings []apicontract.AssetRelinkMapping, dryRun, lookup, batch bool) (apicontract.AssetReferencesData, error) {\n\tp, err := newAssetRelinkPlan(ctx, mappings, dryRun, lookup, batch)\n\tif err != nil {\n\t\tp.failAll(err)\n\t\treturn p.response(err)\n\t}\n\tvar lastProgress time.Time\n\tp.progress = func(path string) {\n\t\tif Conf != nil && time.Since(lastProgress) >= 250*time.Millisecond {\n\t\t\tlastProgress = time.Now()\n\t\t\tlanguage := 70\n\t\t\tif p.saving {\n\t\t\t\tlanguage = 111","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/asset_relink_batch.go#L81-L117","documentation":"Batch mappings must be independent: a mapping's NewPath may not itself be the OldPath of another mapping in the same batch, since that would create a chain (a->b, b->c) or a cycle, requiring ordered application the planner does not support. The constructor scans for this condition and rejects the whole batch.","triggerScenarios":"Calling RelinkAssets with mappings where some NewPath equals another mapping's OldPath, e.g. [{old:a.png,new:b.png},{old:b.png,new:c.png}] or a cycle [{a->b},{b->a}].","commonSituations":"Renaming a sequence of assets by shifting names (a->b, b->c); scripted rotations of file names; refactor tools generating cascading rename plans.","solutions":["Split chained renames into separate sequential batches, waiting for each batch to complete before submitting the next","Rewrite the chain so each mapping's new path is a final destination not used as any source in the same batch","Detect chains client-side by building a graph of oldPath->newPath and checking that no newPath is a source key"],"exampleFix":"// before\nmappings := [{a.png,b.png},{b.png,c.png}] // chained: rejected\nmodel.RelinkAssets(ctx, mappings, false)\n// after\nmodel.RelinkAssets(ctx, [{b.png,c.png}], false) // batch 1\nmodel.RelinkAssets(ctx, [{a.png,b.png}], false) // batch 2 after batch 1 done","handlingStrategy":"validation","validationCode":"srcs := map[string]bool{}\nfor _, m := range mappings { srcs[m.OldPath] = true }\nfor _, m := range mappings {\n\tif m.NewPath != m.OldPath && srcs[m.NewPath] {\n\t\treturn errors.New(\"chained mapping detected: \" + m.NewPath)\n\t}\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Reject chains in the mapping builder UI","Run cascading renames as sequential batches","Model mappings as a graph and assert no newPath is also a source"],"tags":["go","validation","graph-cycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}