ghcr-manager 0.9.7 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ghcr-manager might be problematic. Click here for more details.

Files changed (56) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/LICENSE +1 -1
  3. package/README.md +37 -56
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.js +7 -10
  5. package/dist/cleanup-summary/_cleanup-summary.d.ts +3 -4
  6. package/dist/cleanup-summary/_cleanup-summary.js +2 -3
  7. package/dist/cli/_cleanup-command.js +1 -1
  8. package/dist/cli/_tag-selector-resolver.js +29 -9
  9. package/dist/cli/index.js +0 -4
  10. package/dist/core/_types.d.ts +1 -6
  11. package/dist/core/_types.js +1 -1
  12. package/dist/db/_db-merge-scan-copy.js +3 -2
  13. package/dist/db/_manifest-reachability.js +47 -8
  14. package/dist/db/_scan-writer.js +1 -13
  15. package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
  16. package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
  17. package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +9 -0
  18. package/dist/db/planner/_planner-direct-target-root-tag-filters.js +42 -0
  19. package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
  20. package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +198 -0
  21. package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
  22. package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
  23. package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
  24. package/dist/db/planner/_planner-direct-target-roots-tagged.js +125 -0
  25. package/dist/db/planner/_planner-direct-target-roots.d.ts +2 -12
  26. package/dist/db/planner/_planner-direct-target-roots.js +8 -203
  27. package/dist/db/planner/_planner-direct-target-tags.js +3 -4
  28. package/dist/db/planner/_planner-output.js +28 -8
  29. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
  30. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +65 -0
  31. package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
  32. package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +195 -0
  33. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
  34. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
  35. package/dist/db/planner/_planner-plan-artifacts.js +26 -128
  36. package/dist/db/planner/_planner-sql.js +13 -2
  37. package/dist/db/planner/_planner-types.d.ts +2 -0
  38. package/dist/db/planner/_planner-types.js +1 -0
  39. package/dist/execute/_plan-executor.js +7 -11
  40. package/dist/execute/_types.d.ts +2 -19
  41. package/dist/execute/_untag-client.d.ts +2 -2
  42. package/dist/execute/_untag-client.js +1 -42
  43. package/dist/execute/index.d.ts +1 -1
  44. package/dist/ingest/github/_manifest-kind.d.ts +6 -0
  45. package/dist/ingest/github/_manifest-kind.js +16 -2
  46. package/package.json +16 -10
  47. package/resources/sql/schema/001_schema.sql +14 -4
  48. package/dist/cli/_untag-command.d.ts +0 -1
  49. package/dist/cli/_untag-command.js +0 -58
  50. package/dist/db/_manifest-kind-refinement.d.ts +0 -2
  51. package/dist/db/_manifest-kind-refinement.js +0 -43
  52. package/resources/sql/views/002_v_scan_root_manifests.sql +0 -44
  53. package/resources/sql/views/003_v_digest_tag_relations.sql +0 -50
  54. package/resources/sql/views/004_v_cleanup_root_closure_members.sql +0 -101
  55. package/resources/sql/views/005_v_cleanup_blocking_overlaps.sql +0 -42
  56. package/resources/sql/views/006_v_cleanup_root_decision_readable.sql +0 -67
@@ -1,20 +1,28 @@
1
1
  import { DeletePlanValidationReasonCodes, DeletePlanValidationStatuses } from "./_planner-types.js";
2
2
  export function buildPlanOutputs(directTargetTags, directTargetRoots, planArtifacts) {
3
3
  const rootDecisions = buildRootDecisions(directTargetRoots, planArtifacts);
4
- const protectedRoots = buildProtectedRoots(planArtifacts.blockedRoots);
4
+ const fullyDeletableDigests = new Set(rootDecisions
5
+ .filter((decision) => decision.validationStatus === DeletePlanValidationStatuses.fullyDeletable)
6
+ .map((decision) => decision.digest));
7
+ const blockedDigests = new Set(rootDecisions
8
+ .filter((decision) => decision.validationStatus === DeletePlanValidationStatuses.blocked)
9
+ .map((decision) => decision.digest));
10
+ const blockedRoots = planArtifacts.blockedRoots.filter((blockedRoot) => blockedDigests.has(blockedRoot.blockedDigest));
11
+ const protectedRoots = buildProtectedRoots(blockedRoots);
5
12
  return {
6
13
  directTargetTags,
7
14
  directTargetRoots,
8
15
  rootDecisions,
9
16
  protectedRoots,
10
17
  closureManifests: planArtifacts.closureManifests,
11
- blockedRoots: planArtifacts.blockedRoots,
12
- fullyDeletableRoots: planArtifacts.fullyDeletableRoots,
18
+ blockedRoots,
19
+ fullyDeletableRoots: planArtifacts.fullyDeletableRoots.filter((root) => fullyDeletableDigests.has(root.digest)),
13
20
  collateralTags: []
14
21
  };
15
22
  }
16
23
  export function buildRootDecisions(directTargetRoots, planArtifacts) {
17
24
  const fullyDeletableDigests = new Set(planArtifacts.fullyDeletableRoots.map((root) => root.digest));
25
+ const supportedUntagOnlyRootDigests = planArtifacts.supportedUntagOnlyRootDigests;
18
26
  const blockedRootByDigest = new Map();
19
27
  for (const blockedRoot of planArtifacts.blockedRoots) {
20
28
  if (!blockedRootByDigest.has(blockedRoot.blockedDigest)) {
@@ -22,16 +30,21 @@ export function buildRootDecisions(directTargetRoots, planArtifacts) {
22
30
  }
23
31
  }
24
32
  return directTargetRoots.map((root) => {
25
- if (root.selectionMode === "untag-only") {
33
+ const blockedRoot = blockedRootByDigest.get(root.digest);
34
+ if (_isUntagOnly(root, blockedRoot, supportedUntagOnlyRootDigests)) {
26
35
  return {
27
36
  versionId: root.versionId,
28
37
  digest: root.digest,
29
38
  manifestKind: root.manifestKind,
30
- selectionMode: root.selectionMode,
39
+ selectionMode: "untag-only",
31
40
  selectionReason: root.reason,
32
41
  validationStatus: DeletePlanValidationStatuses.untagOnly,
33
- validationReasonCode: DeletePlanValidationReasonCodes.untagOnlyPartialTagMatch,
34
- validationReason: "matched tags cover only part of this root's tag set, so the version is retained and only those tags can be detached"
42
+ validationReasonCode: root.selectionMode === "untag-only"
43
+ ? DeletePlanValidationReasonCodes.untagOnlyPartialTagMatch
44
+ : DeletePlanValidationReasonCodes.untagOnlyRetainedManifest,
45
+ validationReason: root.selectionMode === "untag-only"
46
+ ? "matched tags cover only part of this root's tag set, so the version is retained and only those tags can be detached"
47
+ : "selected tags can be detached, but the manifest itself must remain because surviving tags still need it"
35
48
  };
36
49
  }
37
50
  if (fullyDeletableDigests.has(root.digest)) {
@@ -46,7 +59,6 @@ export function buildRootDecisions(directTargetRoots, planArtifacts) {
46
59
  validationReason: "selected tags cover the whole root and its manifest closure does not overlap any retained root"
47
60
  };
48
61
  }
49
- const blockedRoot = blockedRootByDigest.get(root.digest);
50
62
  return {
51
63
  versionId: root.versionId,
52
64
  digest: root.digest,
@@ -89,3 +101,11 @@ export function buildBlockedValidationReason(blockedRoot) {
89
101
  }
90
102
  return `blocked because retained root ${blockedRoot.blockingDigest} still requires shared manifest ${blockedRoot.overlapDigest}`;
91
103
  }
104
+ function _isUntagOnly(root, blockedRoot, supportedUntagOnlyRootDigests) {
105
+ if (root.selectionMode === "untag-only") {
106
+ return true;
107
+ }
108
+ return (root.reason === "delete-tags-all-tags-selected" &&
109
+ ((blockedRoot !== undefined && blockedRoot.overlapDigest === root.digest) ||
110
+ supportedUntagOnlyRootDigests.has(root.digest)));
111
+ }
@@ -0,0 +1 @@
1
+ export declare const _LIST_BLOCKED_ROOTS_SQL = "\n WITH selected_graphs AS (\n SELECT DISTINCT\n manifest_graphs.graph_id\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_graphs\n WHERE manifest_graphs.scan_id = ?\n AND manifest_graphs.digest = dtr.root_digest\n ),\n retained_tagged_manifests AS (\n SELECT\n m.version_id AS tagged_version_id,\n m.digest AS tagged_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs\n CROSS JOIN manifests m\n JOIN tags t\n ON t.scan_id = m.scan_id\n AND t.version_id = m.version_id\n AND t.is_digest_tag = 0\n WHERE manifest_graphs.scan_id = m.scan_id\n AND selected_graphs.graph_id = manifest_graphs.graph_id\n AND manifest_graphs.digest = m.digest\n AND m.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM temp_direct_target_roots dtr\n WHERE dtr.root_digest = m.digest\n )\n ),\n ranked_blocks AS (\n SELECT\n dtr.root_version_id AS blocked_version_id,\n dtr.root_digest AS blocked_digest,\n retained.tagged_version_id AS blocking_version_id,\n retained.tagged_digest AS blocking_digest,\n dtr.root_digest AS overlap_digest,\n dtr.root_manifest_kind AS overlap_manifest_kind,\n 'overlap-with-retained-root' AS block_reason,\n ROW_NUMBER() OVER (\n PARTITION BY dtr.root_digest, retained.tagged_digest\n ORDER BY\n retained_overlap.min_distance,\n dtr.root_digest\n ) AS rn\n FROM temp_direct_target_roots dtr\n JOIN retained_tagged_manifests retained\n ON retained.tagged_digest <> dtr.root_digest\n JOIN manifest_reachability retained_overlap\n ON retained_overlap.scan_id = ?\n AND retained_overlap.ancestor_digest = retained.tagged_digest\n AND retained_overlap.descendant_digest = dtr.root_digest\n )\n SELECT\n blocked_version_id,\n blocked_digest,\n blocking_version_id,\n blocking_digest,\n overlap_digest,\n overlap_manifest_kind,\n block_reason\n FROM ranked_blocks\n WHERE rn = 1\n ORDER BY blocked_digest, blocking_digest, overlap_digest\n";
@@ -0,0 +1,65 @@
1
+ export const _LIST_BLOCKED_ROOTS_SQL = `
2
+ WITH selected_graphs AS (
3
+ SELECT DISTINCT
4
+ manifest_graphs.graph_id
5
+ FROM temp_direct_target_roots dtr
6
+ CROSS JOIN manifest_graphs
7
+ WHERE manifest_graphs.scan_id = ?
8
+ AND manifest_graphs.digest = dtr.root_digest
9
+ ),
10
+ retained_tagged_manifests AS (
11
+ SELECT
12
+ m.version_id AS tagged_version_id,
13
+ m.digest AS tagged_digest
14
+ FROM selected_graphs
15
+ CROSS JOIN manifest_graphs
16
+ CROSS JOIN manifests m
17
+ JOIN tags t
18
+ ON t.scan_id = m.scan_id
19
+ AND t.version_id = m.version_id
20
+ AND t.is_digest_tag = 0
21
+ WHERE manifest_graphs.scan_id = m.scan_id
22
+ AND selected_graphs.graph_id = manifest_graphs.graph_id
23
+ AND manifest_graphs.digest = m.digest
24
+ AND m.scan_id = ?
25
+ AND NOT EXISTS (
26
+ SELECT 1
27
+ FROM temp_direct_target_roots dtr
28
+ WHERE dtr.root_digest = m.digest
29
+ )
30
+ ),
31
+ ranked_blocks AS (
32
+ SELECT
33
+ dtr.root_version_id AS blocked_version_id,
34
+ dtr.root_digest AS blocked_digest,
35
+ retained.tagged_version_id AS blocking_version_id,
36
+ retained.tagged_digest AS blocking_digest,
37
+ dtr.root_digest AS overlap_digest,
38
+ dtr.root_manifest_kind AS overlap_manifest_kind,
39
+ 'overlap-with-retained-root' AS block_reason,
40
+ ROW_NUMBER() OVER (
41
+ PARTITION BY dtr.root_digest, retained.tagged_digest
42
+ ORDER BY
43
+ retained_overlap.min_distance,
44
+ dtr.root_digest
45
+ ) AS rn
46
+ FROM temp_direct_target_roots dtr
47
+ JOIN retained_tagged_manifests retained
48
+ ON retained.tagged_digest <> dtr.root_digest
49
+ JOIN manifest_reachability retained_overlap
50
+ ON retained_overlap.scan_id = ?
51
+ AND retained_overlap.ancestor_digest = retained.tagged_digest
52
+ AND retained_overlap.descendant_digest = dtr.root_digest
53
+ )
54
+ SELECT
55
+ blocked_version_id,
56
+ blocked_digest,
57
+ blocking_version_id,
58
+ blocking_digest,
59
+ overlap_digest,
60
+ overlap_manifest_kind,
61
+ block_reason
62
+ FROM ranked_blocks
63
+ WHERE rn = 1
64
+ ORDER BY blocked_digest, blocking_digest, overlap_digest
65
+ `;
@@ -0,0 +1 @@
1
+ export declare const _LIST_CLOSURE_MANIFESTS_SQL = "\n WITH selected_graphs AS (\n SELECT DISTINCT\n manifest_graphs.graph_id\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_graphs\n WHERE manifest_graphs.scan_id = ?\n AND manifest_graphs.digest = dtr.root_digest\n ),\n retained_tagged_manifests AS (\n SELECT DISTINCT\n m.version_id,\n m.digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs\n CROSS JOIN manifests m\n JOIN tags t\n ON t.scan_id = m.scan_id\n AND t.version_id = m.version_id\n AND t.is_digest_tag = 0\n WHERE manifest_graphs.scan_id = m.scan_id\n AND selected_graphs.graph_id = manifest_graphs.graph_id\n AND manifest_graphs.digest = m.digest\n AND m.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM temp_direct_target_roots dtr\n WHERE dtr.root_digest = m.digest\n )\n ),\n retained_manifests AS (\n SELECT\n retained.version_id,\n retained.digest\n FROM retained_tagged_manifests retained\n\n UNION\n\n SELECT\n m.version_id,\n m.digest\n FROM retained_tagged_manifests retained\n CROSS JOIN manifest_reachability mr\n CROSS JOIN manifests m\n WHERE mr.scan_id = ?\n AND mr.ancestor_digest = retained.digest\n AND mr.min_distance > 0\n AND m.scan_id = ?\n AND m.digest = mr.descendant_digest\n ),\n direct_target_closure AS (\n SELECT\n dtr.root_version_id AS source_version_id,\n dtr.root_digest AS source_digest,\n dtr.root_version_id AS member_version_id,\n dtr.root_digest AS member_digest,\n dtr.root_manifest_kind AS member_manifest_kind,\n 0 AS hops_from_root\n FROM temp_direct_target_roots dtr\n\n UNION ALL\n\n SELECT\n dtr.root_version_id AS source_version_id,\n dtr.root_digest AS source_digest,\n m.version_id AS member_version_id,\n m.digest AS member_digest,\n m.manifest_kind AS member_manifest_kind,\n mr.min_distance AS hops_from_root\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_reachability mr\n CROSS JOIN manifests m\n WHERE mr.scan_id = ?\n AND mr.ancestor_digest = dtr.root_digest\n AND mr.min_distance > 0\n AND m.scan_id = ?\n AND m.digest = mr.descendant_digest\n ),\n closure_seed AS (\n SELECT\n dtc.source_version_id,\n dtc.source_digest,\n dtc.member_digest,\n dtc.hops_from_root\n FROM direct_target_closure dtc\n WHERE dtc.hops_from_root = 0\n OR NOT EXISTS (\n SELECT 1\n FROM retained_manifests retained\n WHERE retained.digest = dtc.member_digest\n )\n ),\n undirected_edges AS (\n SELECT\n me.parent_digest AS source_digest,\n me.child_digest AS target_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs parent_graph\n CROSS JOIN manifest_edges me INDEXED BY idx_manifest_edges_scan_parent\n WHERE parent_graph.scan_id = me.scan_id\n AND selected_graphs.graph_id = parent_graph.graph_id\n AND parent_graph.digest = me.parent_digest\n AND me.scan_id = ?\n\n UNION\n\n SELECT\n me.child_digest AS source_digest,\n me.parent_digest AS target_digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs child_graph\n CROSS JOIN manifest_edges me INDEXED BY idx_manifest_edges_scan_child\n WHERE child_graph.scan_id = me.scan_id\n AND selected_graphs.graph_id = child_graph.graph_id\n AND child_graph.digest = me.child_digest\n AND me.scan_id = ?\n ),\n delete_component_members AS (\n SELECT\n seed.source_version_id,\n seed.source_digest,\n seed.member_digest\n FROM closure_seed seed\n\n UNION\n\n SELECT\n walk.source_version_id,\n walk.source_digest,\n m.digest AS member_digest\n FROM delete_component_members walk\n JOIN undirected_edges edge\n ON edge.source_digest = walk.member_digest\n JOIN manifests m\n ON m.scan_id = ?\n AND m.digest = edge.target_digest\n WHERE NOT EXISTS (\n SELECT 1\n FROM retained_manifests retained\n WHERE retained.digest = m.digest\n )\n ),\n source_seed_hops AS (\n SELECT\n seed.source_digest,\n MAX(seed.hops_from_root) AS max_seed_hops\n FROM closure_seed seed\n GROUP BY seed.source_digest\n ),\n descendant_hops AS (\n SELECT\n dtc.source_digest,\n dtc.member_digest,\n MIN(dtc.hops_from_root) AS min_hops_from_root\n FROM direct_target_closure dtc\n WHERE dtc.hops_from_root > 0\n GROUP BY dtc.source_digest, dtc.member_digest\n )\n SELECT\n walk.source_version_id,\n walk.source_digest,\n MIN(member_manifest.version_id) AS member_version_id,\n walk.member_digest,\n MIN(member_manifest.manifest_kind) AS member_manifest_kind,\n CASE\n WHEN walk.member_digest = walk.source_digest\n THEN 0\n WHEN descendant_hops.min_hops_from_root IS NOT NULL\n THEN descendant_hops.min_hops_from_root\n ELSE source_seed_hops.max_seed_hops + 1\n END AS hops_from_root,\n CASE\n WHEN walk.member_digest = walk.source_digest\n THEN 'root'\n WHEN descendant_hops.min_hops_from_root IS NOT NULL\n THEN 'descendant'\n ELSE 'connected'\n END AS member_role\n FROM delete_component_members walk\n JOIN manifests member_manifest\n ON member_manifest.scan_id = ?\n AND member_manifest.digest = walk.member_digest\n JOIN source_seed_hops\n ON source_seed_hops.source_digest = walk.source_digest\n LEFT JOIN descendant_hops\n ON descendant_hops.source_digest = walk.source_digest\n AND descendant_hops.member_digest = walk.member_digest\n GROUP BY\n walk.source_version_id,\n walk.source_digest,\n walk.member_digest,\n descendant_hops.min_hops_from_root,\n source_seed_hops.max_seed_hops\n ORDER BY walk.source_digest, hops_from_root, walk.member_digest\n";
@@ -0,0 +1,195 @@
1
+ export const _LIST_CLOSURE_MANIFESTS_SQL = `
2
+ WITH selected_graphs AS (
3
+ SELECT DISTINCT
4
+ manifest_graphs.graph_id
5
+ FROM temp_direct_target_roots dtr
6
+ CROSS JOIN manifest_graphs
7
+ WHERE manifest_graphs.scan_id = ?
8
+ AND manifest_graphs.digest = dtr.root_digest
9
+ ),
10
+ retained_tagged_manifests AS (
11
+ SELECT DISTINCT
12
+ m.version_id,
13
+ m.digest
14
+ FROM selected_graphs
15
+ CROSS JOIN manifest_graphs
16
+ CROSS JOIN manifests m
17
+ JOIN tags t
18
+ ON t.scan_id = m.scan_id
19
+ AND t.version_id = m.version_id
20
+ AND t.is_digest_tag = 0
21
+ WHERE manifest_graphs.scan_id = m.scan_id
22
+ AND selected_graphs.graph_id = manifest_graphs.graph_id
23
+ AND manifest_graphs.digest = m.digest
24
+ AND m.scan_id = ?
25
+ AND NOT EXISTS (
26
+ SELECT 1
27
+ FROM temp_direct_target_roots dtr
28
+ WHERE dtr.root_digest = m.digest
29
+ )
30
+ ),
31
+ retained_manifests AS (
32
+ SELECT
33
+ retained.version_id,
34
+ retained.digest
35
+ FROM retained_tagged_manifests retained
36
+
37
+ UNION
38
+
39
+ SELECT
40
+ m.version_id,
41
+ m.digest
42
+ FROM retained_tagged_manifests retained
43
+ CROSS JOIN manifest_reachability mr
44
+ CROSS JOIN manifests m
45
+ WHERE mr.scan_id = ?
46
+ AND mr.ancestor_digest = retained.digest
47
+ AND mr.min_distance > 0
48
+ AND m.scan_id = ?
49
+ AND m.digest = mr.descendant_digest
50
+ ),
51
+ direct_target_closure AS (
52
+ SELECT
53
+ dtr.root_version_id AS source_version_id,
54
+ dtr.root_digest AS source_digest,
55
+ dtr.root_version_id AS member_version_id,
56
+ dtr.root_digest AS member_digest,
57
+ dtr.root_manifest_kind AS member_manifest_kind,
58
+ 0 AS hops_from_root
59
+ FROM temp_direct_target_roots dtr
60
+
61
+ UNION ALL
62
+
63
+ SELECT
64
+ dtr.root_version_id AS source_version_id,
65
+ dtr.root_digest AS source_digest,
66
+ m.version_id AS member_version_id,
67
+ m.digest AS member_digest,
68
+ m.manifest_kind AS member_manifest_kind,
69
+ mr.min_distance AS hops_from_root
70
+ FROM temp_direct_target_roots dtr
71
+ CROSS JOIN manifest_reachability mr
72
+ CROSS JOIN manifests m
73
+ WHERE mr.scan_id = ?
74
+ AND mr.ancestor_digest = dtr.root_digest
75
+ AND mr.min_distance > 0
76
+ AND m.scan_id = ?
77
+ AND m.digest = mr.descendant_digest
78
+ ),
79
+ closure_seed AS (
80
+ SELECT
81
+ dtc.source_version_id,
82
+ dtc.source_digest,
83
+ dtc.member_digest,
84
+ dtc.hops_from_root
85
+ FROM direct_target_closure dtc
86
+ WHERE dtc.hops_from_root = 0
87
+ OR NOT EXISTS (
88
+ SELECT 1
89
+ FROM retained_manifests retained
90
+ WHERE retained.digest = dtc.member_digest
91
+ )
92
+ ),
93
+ undirected_edges AS (
94
+ SELECT
95
+ me.parent_digest AS source_digest,
96
+ me.child_digest AS target_digest
97
+ FROM selected_graphs
98
+ CROSS JOIN manifest_graphs parent_graph
99
+ CROSS JOIN manifest_edges me INDEXED BY idx_manifest_edges_scan_parent
100
+ WHERE parent_graph.scan_id = me.scan_id
101
+ AND selected_graphs.graph_id = parent_graph.graph_id
102
+ AND parent_graph.digest = me.parent_digest
103
+ AND me.scan_id = ?
104
+
105
+ UNION
106
+
107
+ SELECT
108
+ me.child_digest AS source_digest,
109
+ me.parent_digest AS target_digest
110
+ FROM selected_graphs
111
+ CROSS JOIN manifest_graphs child_graph
112
+ CROSS JOIN manifest_edges me INDEXED BY idx_manifest_edges_scan_child
113
+ WHERE child_graph.scan_id = me.scan_id
114
+ AND selected_graphs.graph_id = child_graph.graph_id
115
+ AND child_graph.digest = me.child_digest
116
+ AND me.scan_id = ?
117
+ ),
118
+ delete_component_members AS (
119
+ SELECT
120
+ seed.source_version_id,
121
+ seed.source_digest,
122
+ seed.member_digest
123
+ FROM closure_seed seed
124
+
125
+ UNION
126
+
127
+ SELECT
128
+ walk.source_version_id,
129
+ walk.source_digest,
130
+ m.digest AS member_digest
131
+ FROM delete_component_members walk
132
+ JOIN undirected_edges edge
133
+ ON edge.source_digest = walk.member_digest
134
+ JOIN manifests m
135
+ ON m.scan_id = ?
136
+ AND m.digest = edge.target_digest
137
+ WHERE NOT EXISTS (
138
+ SELECT 1
139
+ FROM retained_manifests retained
140
+ WHERE retained.digest = m.digest
141
+ )
142
+ ),
143
+ source_seed_hops AS (
144
+ SELECT
145
+ seed.source_digest,
146
+ MAX(seed.hops_from_root) AS max_seed_hops
147
+ FROM closure_seed seed
148
+ GROUP BY seed.source_digest
149
+ ),
150
+ descendant_hops AS (
151
+ SELECT
152
+ dtc.source_digest,
153
+ dtc.member_digest,
154
+ MIN(dtc.hops_from_root) AS min_hops_from_root
155
+ FROM direct_target_closure dtc
156
+ WHERE dtc.hops_from_root > 0
157
+ GROUP BY dtc.source_digest, dtc.member_digest
158
+ )
159
+ SELECT
160
+ walk.source_version_id,
161
+ walk.source_digest,
162
+ MIN(member_manifest.version_id) AS member_version_id,
163
+ walk.member_digest,
164
+ MIN(member_manifest.manifest_kind) AS member_manifest_kind,
165
+ CASE
166
+ WHEN walk.member_digest = walk.source_digest
167
+ THEN 0
168
+ WHEN descendant_hops.min_hops_from_root IS NOT NULL
169
+ THEN descendant_hops.min_hops_from_root
170
+ ELSE source_seed_hops.max_seed_hops + 1
171
+ END AS hops_from_root,
172
+ CASE
173
+ WHEN walk.member_digest = walk.source_digest
174
+ THEN 'root'
175
+ WHEN descendant_hops.min_hops_from_root IS NOT NULL
176
+ THEN 'descendant'
177
+ ELSE 'connected'
178
+ END AS member_role
179
+ FROM delete_component_members walk
180
+ JOIN manifests member_manifest
181
+ ON member_manifest.scan_id = ?
182
+ AND member_manifest.digest = walk.member_digest
183
+ JOIN source_seed_hops
184
+ ON source_seed_hops.source_digest = walk.source_digest
185
+ LEFT JOIN descendant_hops
186
+ ON descendant_hops.source_digest = walk.source_digest
187
+ AND descendant_hops.member_digest = walk.member_digest
188
+ GROUP BY
189
+ walk.source_version_id,
190
+ walk.source_digest,
191
+ walk.member_digest,
192
+ descendant_hops.min_hops_from_root,
193
+ source_seed_hops.max_seed_hops
194
+ ORDER BY walk.source_digest, hops_from_root, walk.member_digest
195
+ `;
@@ -0,0 +1 @@
1
+ export declare const _LIST_SUPPORTED_UNTAG_ONLY_ROOT_DIGESTS_SQL = "\n WITH selected_graphs AS (\n SELECT DISTINCT\n manifest_graphs.graph_id\n FROM temp_direct_target_roots dtr\n CROSS JOIN manifest_graphs\n WHERE manifest_graphs.scan_id = ?\n AND manifest_graphs.digest = dtr.root_digest\n ),\n retained_tagged_manifests AS (\n SELECT DISTINCT\n m.digest\n FROM selected_graphs\n CROSS JOIN manifest_graphs\n CROSS JOIN manifests m\n JOIN tags t\n ON t.scan_id = m.scan_id\n AND t.version_id = m.version_id\n AND t.is_digest_tag = 0\n WHERE manifest_graphs.scan_id = m.scan_id\n AND selected_graphs.graph_id = manifest_graphs.graph_id\n AND manifest_graphs.digest = m.digest\n AND m.scan_id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM temp_direct_target_roots dtr\n WHERE dtr.root_digest = m.digest\n )\n ),\n retained_manifests AS (\n SELECT\n retained.digest\n FROM retained_tagged_manifests retained\n\n UNION\n\n SELECT\n mr.descendant_digest AS digest\n FROM retained_tagged_manifests retained\n CROSS JOIN manifest_reachability mr\n WHERE mr.scan_id = ?\n AND mr.ancestor_digest = retained.digest\n AND mr.min_distance > 0\n )\n SELECT DISTINCT\n dtr.root_digest\n FROM temp_direct_target_roots dtr\n WHERE dtr.root_manifest_kind = 'index_manifest'\n AND EXISTS (\n SELECT 1\n FROM manifest_edges me\n JOIN manifests child\n ON child.scan_id = ?\n AND child.digest = me.child_digest\n WHERE me.scan_id = ?\n AND me.parent_digest = dtr.root_digest\n AND me.edge_kind = 'referrer'\n AND child.manifest_kind = 'signature_manifest'\n )\n AND EXISTS (\n SELECT 1\n FROM manifest_edges me\n JOIN manifests child\n ON child.scan_id = ?\n AND child.digest = me.child_digest\n WHERE me.scan_id = ?\n AND me.parent_digest = dtr.root_digest\n AND me.edge_kind = 'image-child'\n AND child.manifest_kind <> 'signature_manifest'\n )\n AND NOT EXISTS (\n SELECT 1\n FROM manifest_edges me\n JOIN manifests child\n ON child.scan_id = ?\n AND child.digest = me.child_digest\n WHERE me.scan_id = ?\n AND me.parent_digest = dtr.root_digest\n AND me.edge_kind = 'image-child'\n AND child.manifest_kind <> 'signature_manifest'\n AND child.digest NOT IN (\n SELECT digest\n FROM retained_manifests\n )\n )\n";
@@ -0,0 +1,86 @@
1
+ export const _LIST_SUPPORTED_UNTAG_ONLY_ROOT_DIGESTS_SQL = `
2
+ WITH selected_graphs AS (
3
+ SELECT DISTINCT
4
+ manifest_graphs.graph_id
5
+ FROM temp_direct_target_roots dtr
6
+ CROSS JOIN manifest_graphs
7
+ WHERE manifest_graphs.scan_id = ?
8
+ AND manifest_graphs.digest = dtr.root_digest
9
+ ),
10
+ retained_tagged_manifests AS (
11
+ SELECT DISTINCT
12
+ m.digest
13
+ FROM selected_graphs
14
+ CROSS JOIN manifest_graphs
15
+ CROSS JOIN manifests m
16
+ JOIN tags t
17
+ ON t.scan_id = m.scan_id
18
+ AND t.version_id = m.version_id
19
+ AND t.is_digest_tag = 0
20
+ WHERE manifest_graphs.scan_id = m.scan_id
21
+ AND selected_graphs.graph_id = manifest_graphs.graph_id
22
+ AND manifest_graphs.digest = m.digest
23
+ AND m.scan_id = ?
24
+ AND NOT EXISTS (
25
+ SELECT 1
26
+ FROM temp_direct_target_roots dtr
27
+ WHERE dtr.root_digest = m.digest
28
+ )
29
+ ),
30
+ retained_manifests AS (
31
+ SELECT
32
+ retained.digest
33
+ FROM retained_tagged_manifests retained
34
+
35
+ UNION
36
+
37
+ SELECT
38
+ mr.descendant_digest AS digest
39
+ FROM retained_tagged_manifests retained
40
+ CROSS JOIN manifest_reachability mr
41
+ WHERE mr.scan_id = ?
42
+ AND mr.ancestor_digest = retained.digest
43
+ AND mr.min_distance > 0
44
+ )
45
+ SELECT DISTINCT
46
+ dtr.root_digest
47
+ FROM temp_direct_target_roots dtr
48
+ WHERE dtr.root_manifest_kind = 'index_manifest'
49
+ AND EXISTS (
50
+ SELECT 1
51
+ FROM manifest_edges me
52
+ JOIN manifests child
53
+ ON child.scan_id = ?
54
+ AND child.digest = me.child_digest
55
+ WHERE me.scan_id = ?
56
+ AND me.parent_digest = dtr.root_digest
57
+ AND me.edge_kind = 'referrer'
58
+ AND child.manifest_kind = 'signature_manifest'
59
+ )
60
+ AND EXISTS (
61
+ SELECT 1
62
+ FROM manifest_edges me
63
+ JOIN manifests child
64
+ ON child.scan_id = ?
65
+ AND child.digest = me.child_digest
66
+ WHERE me.scan_id = ?
67
+ AND me.parent_digest = dtr.root_digest
68
+ AND me.edge_kind = 'image-child'
69
+ AND child.manifest_kind <> 'signature_manifest'
70
+ )
71
+ AND NOT EXISTS (
72
+ SELECT 1
73
+ FROM manifest_edges me
74
+ JOIN manifests child
75
+ ON child.scan_id = ?
76
+ AND child.digest = me.child_digest
77
+ WHERE me.scan_id = ?
78
+ AND me.parent_digest = dtr.root_digest
79
+ AND me.edge_kind = 'image-child'
80
+ AND child.manifest_kind <> 'signature_manifest'
81
+ AND child.digest NOT IN (
82
+ SELECT digest
83
+ FROM retained_manifests
84
+ )
85
+ )
86
+ `;
@@ -1,4 +1,7 @@
1
1
  import { mapBlockedRootRow, mapClosureManifestRow } from "./_planner-types.js";
2
+ import { _LIST_BLOCKED_ROOTS_SQL } from "./_planner-plan-artifacts-blocked-roots-sql.js";
3
+ import { _LIST_CLOSURE_MANIFESTS_SQL } from "./_planner-plan-artifacts-closure-sql.js";
4
+ import { _LIST_SUPPORTED_UNTAG_ONLY_ROOT_DIGESTS_SQL } from "./_planner-plan-artifacts-supported-untag-only-sql.js";
2
5
  export class PlannerPlanArtifacts {
3
6
  #sql;
4
7
  constructor(sql) {
@@ -10,7 +13,8 @@ export class PlannerPlanArtifacts {
10
13
  return {
11
14
  closureManifests: [],
12
15
  blockedRoots: [],
13
- fullyDeletableRoots: []
16
+ fullyDeletableRoots: [],
17
+ supportedUntagOnlyRootDigests: new Set()
14
18
  };
15
19
  }
16
20
  return this.#withDirectTargetRootsTempTable(deleteRootCandidates, () => {
@@ -18,143 +22,37 @@ export class PlannerPlanArtifacts {
18
22
  const blockedRoots = this.#listBlockedRoots(scanId);
19
23
  const blockedVersionIds = new Set(blockedRoots.map((root) => root.blockedVersionId));
20
24
  const fullyDeletableRoots = deleteRootCandidates.filter((root) => !blockedVersionIds.has(root.versionId));
25
+ const supportedUntagOnlyRootDigests = this.#listSupportedUntagOnlyRootDigests(scanId);
21
26
  return {
22
27
  closureManifests,
23
28
  blockedRoots,
24
- fullyDeletableRoots
29
+ fullyDeletableRoots,
30
+ supportedUntagOnlyRootDigests
25
31
  };
26
32
  });
27
33
  }
34
+ #listSupportedUntagOnlyRootDigests(scanId) {
35
+ const rows = this.#sql.all(_LIST_SUPPORTED_UNTAG_ONLY_ROOT_DIGESTS_SQL, [
36
+ scanId,
37
+ scanId,
38
+ scanId,
39
+ scanId,
40
+ scanId,
41
+ scanId,
42
+ scanId,
43
+ scanId,
44
+ scanId
45
+ ]);
46
+ return new Set(rows.map((row) => row.root_digest));
47
+ }
28
48
  #listClosureManifests(scanId) {
29
- const sql = `
30
- WITH direct_target_closure AS (
31
- SELECT
32
- dtr.root_version_id AS source_version_id,
33
- dtr.root_digest AS source_digest,
34
- dtr.root_version_id AS member_version_id,
35
- dtr.root_digest AS member_digest,
36
- dtr.root_manifest_kind AS member_manifest_kind,
37
- 0 AS hops_from_root,
38
- 'root' AS member_role
39
- FROM temp_direct_target_roots dtr
40
-
41
- UNION ALL
42
-
43
- SELECT
44
- dtr.root_version_id AS source_version_id,
45
- dtr.root_digest AS source_digest,
46
- m.version_id AS member_version_id,
47
- m.digest AS member_digest,
48
- m.manifest_kind AS member_manifest_kind,
49
- mr.min_distance AS hops_from_root,
50
- 'descendant' AS member_role
51
- FROM temp_direct_target_roots dtr
52
- JOIN manifest_reachability mr
53
- ON mr.scan_id = ?
54
- AND mr.ancestor_digest = dtr.root_digest
55
- AND mr.min_distance > 0
56
- JOIN manifests m
57
- ON m.scan_id = ?
58
- AND m.digest = mr.descendant_digest
59
- )
60
- SELECT
61
- source_version_id,
62
- source_digest,
63
- member_version_id,
64
- member_digest,
65
- member_manifest_kind,
66
- hops_from_root,
67
- member_role
68
- FROM direct_target_closure
69
- ORDER BY source_digest, hops_from_root, member_digest
70
- `;
71
- return this.#sql.all(sql, [scanId, scanId]).map(mapClosureManifestRow);
49
+ return this.#sql
50
+ .all(_LIST_CLOSURE_MANIFESTS_SQL, [scanId, scanId, scanId, scanId, scanId, scanId, scanId, scanId, scanId, scanId])
51
+ .map(mapClosureManifestRow);
72
52
  }
73
53
  #listBlockedRoots(scanId) {
74
- const sql = `
75
- WITH retained_roots AS (
76
- SELECT
77
- m.version_id AS root_version_id,
78
- m.digest AS root_digest
79
- FROM manifests m
80
- WHERE m.scan_id = ?
81
- AND NOT EXISTS (
82
- SELECT 1
83
- FROM manifest_reachability mr
84
- WHERE mr.scan_id = m.scan_id
85
- AND mr.descendant_digest = m.digest
86
- AND mr.min_distance > 0
87
- )
88
- AND NOT EXISTS (
89
- SELECT 1
90
- FROM temp_direct_target_roots dtr
91
- WHERE dtr.root_digest = m.digest
92
- )
93
- ),
94
- direct_target_closure AS (
95
- SELECT
96
- dtr.root_version_id AS root_version_id,
97
- dtr.root_digest AS root_digest,
98
- dtr.root_manifest_kind AS member_manifest_kind,
99
- dtr.root_digest AS member_digest,
100
- 0 AS hops_from_root
101
- FROM temp_direct_target_roots dtr
102
-
103
- UNION ALL
104
-
105
- SELECT
106
- dtr.root_version_id AS root_version_id,
107
- dtr.root_digest AS root_digest,
108
- m.manifest_kind AS member_manifest_kind,
109
- m.digest AS member_digest,
110
- mr.min_distance AS hops_from_root
111
- FROM temp_direct_target_roots dtr
112
- JOIN manifest_reachability mr
113
- ON mr.scan_id = ?
114
- AND mr.ancestor_digest = dtr.root_digest
115
- AND mr.min_distance > 0
116
- JOIN manifests m
117
- ON m.scan_id = ?
118
- AND m.digest = mr.descendant_digest
119
- ),
120
- ranked_blocks AS (
121
- SELECT
122
- dtc.root_version_id AS blocked_version_id,
123
- dtc.root_digest AS blocked_digest,
124
- rr.root_version_id AS blocking_version_id,
125
- rr.root_digest AS blocking_digest,
126
- dtc.member_digest AS overlap_digest,
127
- dtc.member_manifest_kind AS overlap_manifest_kind,
128
- 'overlap-with-retained-root' AS block_reason,
129
- ROW_NUMBER() OVER (
130
- PARTITION BY dtc.root_digest, rr.root_digest
131
- ORDER BY
132
- dtc.hops_from_root,
133
- retained_overlap.min_distance,
134
- dtc.member_digest
135
- ) AS rn
136
- FROM direct_target_closure dtc
137
- JOIN retained_roots rr
138
- ON rr.root_digest <> dtc.root_digest
139
- JOIN manifest_reachability retained_overlap
140
- ON retained_overlap.scan_id = ?
141
- AND retained_overlap.ancestor_digest = rr.root_digest
142
- AND retained_overlap.descendant_digest = dtc.member_digest
143
- )
144
- SELECT
145
- blocked_version_id,
146
- blocked_digest,
147
- blocking_version_id,
148
- blocking_digest,
149
- overlap_digest,
150
- overlap_manifest_kind,
151
- block_reason
152
- FROM ranked_blocks
153
- WHERE rn = 1
154
- ORDER BY blocked_digest, blocking_digest, overlap_digest
155
- `;
156
54
  return this.#sql
157
- .all(sql, [scanId, scanId, scanId, scanId])
55
+ .all(_LIST_BLOCKED_ROOTS_SQL, [scanId, scanId, scanId])
158
56
  .map(mapBlockedRootRow);
159
57
  }
160
58
  #withDirectTargetRootsTempTable(directTargetRoots, callback) {
@@ -14,16 +14,24 @@ export class PlannerSql {
14
14
  }
15
15
  exec(sql, params = []) {
16
16
  this.#traceSql(sql, params);
17
+ const startedAt = Date.now();
17
18
  this.#database.prepare(sql).run(...params);
19
+ this.#logger.trace(`SQL exec completed in ${Date.now() - startedAt} ms`);
18
20
  }
19
21
  get(sql, params) {
22
+ this.#debugSql(sql, params);
20
23
  this.#traceSql(sql, params);
21
- return this.#database.prepare(sql).get(...params);
24
+ const startedAt = Date.now();
25
+ const row = this.#database.prepare(sql).get(...params);
26
+ this.#logger.debug(`SQL returned ${row === undefined ? "0" : "1"} row(s) in ${Date.now() - startedAt} ms`);
27
+ return row;
22
28
  }
23
29
  all(sql, params) {
30
+ this.#debugSql(sql, params);
24
31
  this.#traceSql(sql, params);
32
+ const startedAt = Date.now();
25
33
  const rows = this.#database.prepare(sql).all(...params);
26
- this.#logger.debug(`SQL returned ${rows.length} row(s)`);
34
+ this.#logger.debug(`SQL returned ${rows.length} row(s) in ${Date.now() - startedAt} ms`);
27
35
  return rows;
28
36
  }
29
37
  traceSql(sql, params) {
@@ -32,4 +40,7 @@ export class PlannerSql {
32
40
  #traceSql(sql, params) {
33
41
  this.#logger.trace(`SQL:\n${sql.trim()}\nPARAMS: ${JSON.stringify(params)}`);
34
42
  }
43
+ #debugSql(sql, params) {
44
+ this.#logger.debug(`SQL:\n${sql.trim()}\nPARAMS: ${JSON.stringify(params)}`);
45
+ }
35
46
  }
@@ -48,6 +48,7 @@ export declare const DeletePlanValidationStatuses: {
48
48
  export type DeletePlanValidationStatus = (typeof DeletePlanValidationStatuses)[keyof typeof DeletePlanValidationStatuses];
49
49
  export declare const DeletePlanValidationReasonCodes: {
50
50
  readonly untagOnlyPartialTagMatch: "untag-only-partial-tag-match";
51
+ readonly untagOnlyRetainedManifest: "untag-only-retained-manifest";
51
52
  readonly fullyDeletableNoRetainedOverlap: "fully-deletable-no-retained-overlap";
52
53
  readonly blockedOverlapWithRetainedRoot: "blocked-overlap-with-retained-root";
53
54
  };
@@ -106,6 +107,7 @@ export interface PlanArtifacts {
106
107
  closureManifests: DeletePlanClosureManifest[];
107
108
  blockedRoots: DeletePlanBlockedRoot[];
108
109
  fullyDeletableRoots: DeletePlanRoot[];
110
+ supportedUntagOnlyRootDigests: Set<string>;
109
111
  }
110
112
  export interface DeletePlan {
111
113
  owner: string;