ghcr-cleanup-manager 1.1.0

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/LICENSE +21 -0
  3. package/README.md +262 -0
  4. package/dist/cleanup-summary/_cleanup-summary-markdown.d.ts +5 -0
  5. package/dist/cleanup-summary/_cleanup-summary-markdown.js +224 -0
  6. package/dist/cleanup-summary/_cleanup-summary.d.ts +56 -0
  7. package/dist/cleanup-summary/_cleanup-summary.js +59 -0
  8. package/dist/cleanup-summary/index.d.ts +2 -0
  9. package/dist/cleanup-summary/index.js +2 -0
  10. package/dist/cli/_args.d.ts +7 -0
  11. package/dist/cli/_args.js +41 -0
  12. package/dist/cli/_cleanup-command.d.ts +1 -0
  13. package/dist/cli/_cleanup-command.js +142 -0
  14. package/dist/cli/_db-merge-command.d.ts +1 -0
  15. package/dist/cli/_db-merge-command.js +41 -0
  16. package/dist/cli/_github-output.d.ts +10 -0
  17. package/dist/cli/_github-output.js +13 -0
  18. package/dist/cli/_json-output.d.ts +1 -0
  19. package/dist/cli/_json-output.js +11 -0
  20. package/dist/cli/_logger.d.ts +10 -0
  21. package/dist/cli/_logger.js +26 -0
  22. package/dist/cli/_older-than.d.ts +5 -0
  23. package/dist/cli/_older-than.js +42 -0
  24. package/dist/cli/_planner-options.d.ts +20 -0
  25. package/dist/cli/_planner-options.js +83 -0
  26. package/dist/cli/_scan-command.d.ts +1 -0
  27. package/dist/cli/_scan-command.js +39 -0
  28. package/dist/cli/_tag-selector-resolver.d.ts +3 -0
  29. package/dist/cli/_tag-selector-resolver.js +132 -0
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.js +52 -0
  32. package/dist/config/_service-constants.d.ts +3 -0
  33. package/dist/config/_service-constants.js +3 -0
  34. package/dist/config/index.d.ts +9 -0
  35. package/dist/config/index.js +9 -0
  36. package/dist/core/_digest-tag.d.ts +2 -0
  37. package/dist/core/_digest-tag.js +12 -0
  38. package/dist/core/_github-package-owner.d.ts +11 -0
  39. package/dist/core/_github-package-owner.js +45 -0
  40. package/dist/core/_http-error.d.ts +6 -0
  41. package/dist/core/_http-error.js +33 -0
  42. package/dist/core/_types.d.ts +54 -0
  43. package/dist/core/_types.js +8 -0
  44. package/dist/core/index.d.ts +6 -0
  45. package/dist/core/index.js +4 -0
  46. package/dist/db/_cleanup-run-writer.d.ts +10 -0
  47. package/dist/db/_cleanup-run-writer.js +113 -0
  48. package/dist/db/_db-merge-cleanup-copy.d.ts +7 -0
  49. package/dist/db/_db-merge-cleanup-copy.js +172 -0
  50. package/dist/db/_db-merge-history.d.ts +2 -0
  51. package/dist/db/_db-merge-history.js +15 -0
  52. package/dist/db/_db-merge-repository.d.ts +8 -0
  53. package/dist/db/_db-merge-repository.js +95 -0
  54. package/dist/db/_db-merge-scan-copy.d.ts +10 -0
  55. package/dist/db/_db-merge-scan-copy.js +70 -0
  56. package/dist/db/_db-merge-types.d.ts +44 -0
  57. package/dist/db/_db-merge-types.js +1 -0
  58. package/dist/db/_github-actions-run-url.d.ts +1 -0
  59. package/dist/db/_github-actions-run-url.js +9 -0
  60. package/dist/db/_manifest-reachability.d.ts +2 -0
  61. package/dist/db/_manifest-reachability.js +158 -0
  62. package/dist/db/_scan-writer.d.ts +20 -0
  63. package/dist/db/_scan-writer.js +184 -0
  64. package/dist/db/_schema.d.ts +2 -0
  65. package/dist/db/_schema.js +19 -0
  66. package/dist/db/_snapshot-repository.d.ts +24 -0
  67. package/dist/db/_snapshot-repository.js +86 -0
  68. package/dist/db/_sql-placeholders.d.ts +2 -0
  69. package/dist/db/_sql-placeholders.js +16 -0
  70. package/dist/db/index.d.ts +10 -0
  71. package/dist/db/index.js +13 -0
  72. package/dist/db/planner/_planner-direct-target-root-options.d.ts +11 -0
  73. package/dist/db/planner/_planner-direct-target-root-options.js +1 -0
  74. package/dist/db/planner/_planner-direct-target-root-tag-filters.d.ts +7 -0
  75. package/dist/db/planner/_planner-direct-target-root-tag-filters.js +41 -0
  76. package/dist/db/planner/_planner-direct-target-roots-combined-sql.d.ts +7 -0
  77. package/dist/db/planner/_planner-direct-target-roots-combined-sql.js +189 -0
  78. package/dist/db/planner/_planner-direct-target-roots-combined.d.ts +4 -0
  79. package/dist/db/planner/_planner-direct-target-roots-combined.js +10 -0
  80. package/dist/db/planner/_planner-direct-target-roots-tagged.d.ts +4 -0
  81. package/dist/db/planner/_planner-direct-target-roots-tagged.js +117 -0
  82. package/dist/db/planner/_planner-direct-target-roots.d.ts +8 -0
  83. package/dist/db/planner/_planner-direct-target-roots.js +17 -0
  84. package/dist/db/planner/_planner-direct-target-tags.d.ts +6 -0
  85. package/dist/db/planner/_planner-direct-target-tags.js +54 -0
  86. package/dist/db/planner/_planner-latest-scan.d.ts +10 -0
  87. package/dist/db/planner/_planner-latest-scan.js +20 -0
  88. package/dist/db/planner/_planner-output.d.ts +5 -0
  89. package/dist/db/planner/_planner-output.js +111 -0
  90. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.d.ts +1 -0
  91. package/dist/db/planner/_planner-plan-artifacts-blocked-roots-sql.js +110 -0
  92. package/dist/db/planner/_planner-plan-artifacts-closure-sql.d.ts +1 -0
  93. package/dist/db/planner/_planner-plan-artifacts-closure-sql.js +188 -0
  94. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.d.ts +1 -0
  95. package/dist/db/planner/_planner-plan-artifacts-supported-untag-only-sql.js +86 -0
  96. package/dist/db/planner/_planner-plan-artifacts.d.ts +7 -0
  97. package/dist/db/planner/_planner-plan-artifacts.js +109 -0
  98. package/dist/db/planner/_planner-repository.d.ts +44 -0
  99. package/dist/db/planner/_planner-repository.js +105 -0
  100. package/dist/db/planner/_planner-sql.d.ts +12 -0
  101. package/dist/db/planner/_planner-sql.js +46 -0
  102. package/dist/db/planner/_planner-tag-selectors.d.ts +8 -0
  103. package/dist/db/planner/_planner-tag-selectors.js +57 -0
  104. package/dist/db/planner/_planner-types.d.ts +144 -0
  105. package/dist/db/planner/_planner-types.js +49 -0
  106. package/dist/db/planner/index.d.ts +3 -0
  107. package/dist/db/planner/index.js +2 -0
  108. package/dist/execute/_http.d.ts +7 -0
  109. package/dist/execute/_http.js +48 -0
  110. package/dist/execute/_manifest-detach.d.ts +4 -0
  111. package/dist/execute/_manifest-detach.js +31 -0
  112. package/dist/execute/_package-version-delete-client.d.ts +4 -0
  113. package/dist/execute/_package-version-delete-client.js +34 -0
  114. package/dist/execute/_package-version-page-client.d.ts +14 -0
  115. package/dist/execute/_package-version-page-client.js +64 -0
  116. package/dist/execute/_package-version-tag-source-client.d.ts +12 -0
  117. package/dist/execute/_package-version-tag-source-client.js +65 -0
  118. package/dist/execute/_plan-executor.d.ts +3 -0
  119. package/dist/execute/_plan-executor.js +69 -0
  120. package/dist/execute/_registry-manifest-client.d.ts +12 -0
  121. package/dist/execute/_registry-manifest-client.js +79 -0
  122. package/dist/execute/_registry-token-client.d.ts +4 -0
  123. package/dist/execute/_registry-token-client.js +37 -0
  124. package/dist/execute/_types.d.ts +34 -0
  125. package/dist/execute/_types.js +1 -0
  126. package/dist/execute/_untag-client.d.ts +2 -0
  127. package/dist/execute/_untag-client.js +30 -0
  128. package/dist/execute/index.d.ts +5 -0
  129. package/dist/execute/index.js +3 -0
  130. package/dist/ingest/github/_manifest-client.d.ts +14 -0
  131. package/dist/ingest/github/_manifest-client.js +108 -0
  132. package/dist/ingest/github/_manifest-ingest.d.ts +3 -0
  133. package/dist/ingest/github/_manifest-ingest.js +90 -0
  134. package/dist/ingest/github/_manifest-kind.d.ts +26 -0
  135. package/dist/ingest/github/_manifest-kind.js +65 -0
  136. package/dist/ingest/github/_package-metadata-load.d.ts +5 -0
  137. package/dist/ingest/github/_package-metadata-load.js +45 -0
  138. package/dist/ingest/github/_package-version-page-load.d.ts +13 -0
  139. package/dist/ingest/github/_package-version-page-load.js +55 -0
  140. package/dist/ingest/github/_packages-client.d.ts +10 -0
  141. package/dist/ingest/github/_packages-client.js +76 -0
  142. package/dist/ingest/github/_parallel-paginated-ingest.d.ts +12 -0
  143. package/dist/ingest/github/_parallel-paginated-ingest.js +49 -0
  144. package/dist/ingest/github/_registry-token-client.d.ts +6 -0
  145. package/dist/ingest/github/_registry-token-client.js +67 -0
  146. package/dist/ingest/github/_shared.d.ts +28 -0
  147. package/dist/ingest/github/_shared.js +70 -0
  148. package/dist/ingest/github/index.d.ts +11 -0
  149. package/dist/ingest/github/index.js +29 -0
  150. package/package.json +78 -0
  151. package/resources/sql/schema/001_schema.sql +222 -0
  152. package/resources/sql/views/001_v_latest_scan_per_package.sql +27 -0
@@ -0,0 +1,222 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ CREATE TABLE IF NOT EXISTS package_scans (
4
+ scan_id INTEGER PRIMARY KEY,
5
+ scan_uuid TEXT NOT NULL UNIQUE,
6
+ owner TEXT NOT NULL,
7
+ package_name TEXT NOT NULL,
8
+ package_metadata_json TEXT NOT NULL,
9
+ github_actions_run_url TEXT,
10
+ scan_started_at TEXT NOT NULL,
11
+ scan_completed_at TEXT,
12
+ status TEXT NOT NULL,
13
+ CHECK(status IN ('running', 'completed', 'failed'))
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS package_versions (
17
+ scan_id INTEGER NOT NULL,
18
+ version_id INTEGER NOT NULL,
19
+ created_at TEXT NOT NULL,
20
+ updated_at TEXT NOT NULL,
21
+ PRIMARY KEY(scan_id, version_id),
22
+ FOREIGN KEY(scan_id) REFERENCES package_scans(scan_id)
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS package_version_payloads (
26
+ scan_id INTEGER NOT NULL,
27
+ version_id INTEGER NOT NULL,
28
+ raw_json TEXT NOT NULL,
29
+ PRIMARY KEY(scan_id, version_id),
30
+ FOREIGN KEY(scan_id, version_id) REFERENCES package_versions(scan_id, version_id)
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS tags (
34
+ scan_id INTEGER NOT NULL,
35
+ tag TEXT NOT NULL,
36
+ version_id INTEGER NOT NULL,
37
+ is_digest_tag INTEGER NOT NULL,
38
+ CHECK(is_digest_tag IN (0, 1)),
39
+ PRIMARY KEY(scan_id, tag),
40
+ FOREIGN KEY(scan_id, version_id) REFERENCES package_versions(scan_id, version_id)
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS manifests (
44
+ scan_id INTEGER NOT NULL,
45
+ version_id INTEGER NOT NULL,
46
+ digest TEXT NOT NULL,
47
+ media_type TEXT NOT NULL,
48
+ artifact_type TEXT,
49
+ config_media_type TEXT,
50
+ subject_digest TEXT,
51
+ annotations_json TEXT,
52
+ manifest_kind TEXT,
53
+ CHECK(manifest_kind IN (
54
+ 'index_manifest',
55
+ 'multi_arch_manifest',
56
+ 'image_manifest',
57
+ 'artifact_manifest',
58
+ 'attestation_manifest',
59
+ 'signature_manifest'
60
+ )),
61
+ PRIMARY KEY(scan_id, version_id),
62
+ UNIQUE(scan_id, digest),
63
+ FOREIGN KEY(scan_id, version_id) REFERENCES package_versions(scan_id, version_id)
64
+ );
65
+
66
+ CREATE TABLE IF NOT EXISTS manifest_descriptors (
67
+ scan_id INTEGER NOT NULL,
68
+ parent_digest TEXT NOT NULL,
69
+ child_digest TEXT NOT NULL,
70
+ media_type TEXT NOT NULL,
71
+ artifact_type TEXT,
72
+ platform_os TEXT,
73
+ platform_architecture TEXT,
74
+ platform_variant TEXT,
75
+ PRIMARY KEY(scan_id, parent_digest, child_digest),
76
+ FOREIGN KEY(scan_id, parent_digest) REFERENCES manifests(scan_id, digest)
77
+ );
78
+
79
+ CREATE TABLE IF NOT EXISTS manifest_payloads (
80
+ scan_id INTEGER NOT NULL,
81
+ digest TEXT NOT NULL,
82
+ raw_json TEXT NOT NULL,
83
+ PRIMARY KEY(scan_id, digest),
84
+ FOREIGN KEY(scan_id, digest) REFERENCES manifests(scan_id, digest)
85
+ );
86
+
87
+ CREATE TABLE IF NOT EXISTS manifest_edges (
88
+ scan_id INTEGER NOT NULL,
89
+ parent_digest TEXT NOT NULL,
90
+ child_digest TEXT NOT NULL,
91
+ edge_kind TEXT NOT NULL,
92
+ CHECK(edge_kind IN ('image-child', 'referrer', 'digest-tag-referrer')),
93
+ PRIMARY KEY(scan_id, parent_digest, child_digest, edge_kind),
94
+ FOREIGN KEY(scan_id, parent_digest) REFERENCES manifests(scan_id, digest),
95
+ FOREIGN KEY(scan_id, child_digest) REFERENCES manifests(scan_id, digest)
96
+ );
97
+
98
+ CREATE TABLE IF NOT EXISTS manifest_reachability (
99
+ scan_id INTEGER NOT NULL,
100
+ ancestor_digest TEXT NOT NULL,
101
+ descendant_digest TEXT NOT NULL,
102
+ min_distance INTEGER NOT NULL,
103
+ PRIMARY KEY(scan_id, ancestor_digest, descendant_digest),
104
+ FOREIGN KEY(scan_id, ancestor_digest) REFERENCES manifests(scan_id, digest),
105
+ FOREIGN KEY(scan_id, descendant_digest) REFERENCES manifests(scan_id, digest),
106
+ CHECK(min_distance >= 0)
107
+ );
108
+
109
+ CREATE TABLE IF NOT EXISTS manifest_graphs (
110
+ scan_id INTEGER NOT NULL,
111
+ digest TEXT NOT NULL,
112
+ graph_id INTEGER NOT NULL,
113
+ PRIMARY KEY(scan_id, digest),
114
+ FOREIGN KEY(scan_id, digest) REFERENCES manifests(scan_id, digest),
115
+ CHECK(graph_id > 0)
116
+ );
117
+
118
+ CREATE TABLE IF NOT EXISTS cleanup_runs (
119
+ cleanup_run_id INTEGER PRIMARY KEY,
120
+ scan_id INTEGER NOT NULL,
121
+ cleanup_uuid TEXT NOT NULL,
122
+ cleanup_started_at TEXT NOT NULL,
123
+ github_actions_run_url TEXT,
124
+ dry_run INTEGER NOT NULL,
125
+ planner_inputs_json TEXT NOT NULL,
126
+ direct_target_tag_count INTEGER NOT NULL,
127
+ direct_target_root_count INTEGER NOT NULL,
128
+ delete_root_candidate_count INTEGER NOT NULL,
129
+ untag_only_root_count INTEGER NOT NULL,
130
+ fully_deletable_root_count INTEGER NOT NULL,
131
+ blocked_delete_root_count INTEGER NOT NULL,
132
+ protected_root_count INTEGER NOT NULL,
133
+ CHECK(dry_run IN (0, 1)),
134
+ UNIQUE(cleanup_run_id, scan_id),
135
+ FOREIGN KEY(scan_id) REFERENCES package_scans(scan_id)
136
+ );
137
+
138
+ CREATE TABLE IF NOT EXISTS cleanup_root_decisions (
139
+ cleanup_run_id INTEGER NOT NULL,
140
+ scan_id INTEGER NOT NULL,
141
+ digest TEXT NOT NULL,
142
+ selection_mode TEXT NOT NULL,
143
+ selection_reason TEXT NOT NULL,
144
+ validation_status TEXT NOT NULL,
145
+ validation_reason_code TEXT NOT NULL,
146
+ validation_reason TEXT NOT NULL,
147
+ blocking_digest TEXT,
148
+ overlap_digest TEXT,
149
+ PRIMARY KEY(cleanup_run_id, digest),
150
+ CHECK(selection_mode IN ('delete-root', 'untag-only')),
151
+ CHECK(selection_reason IN (
152
+ 'delete-tags-all-tags-selected',
153
+ 'delete-tags-partial-tag-match',
154
+ 'delete-untagged',
155
+ 'keep-n-tagged-overflow',
156
+ 'keep-n-untagged-overflow'
157
+ )),
158
+ CHECK(validation_status IN ('fully-deletable', 'blocked', 'untag-only')),
159
+ CHECK(validation_reason_code IN (
160
+ 'untag-only-partial-tag-match',
161
+ 'untag-only-retained-manifest',
162
+ 'fully-deletable-no-retained-overlap',
163
+ 'blocked-overlap-with-retained-root'
164
+ )),
165
+ FOREIGN KEY(cleanup_run_id, scan_id) REFERENCES cleanup_runs(cleanup_run_id, scan_id),
166
+ FOREIGN KEY(scan_id, digest) REFERENCES manifests(scan_id, digest),
167
+ FOREIGN KEY(scan_id, blocking_digest) REFERENCES manifests(scan_id, digest),
168
+ FOREIGN KEY(scan_id, overlap_digest) REFERENCES manifests(scan_id, digest)
169
+ );
170
+
171
+ CREATE TABLE IF NOT EXISTS cleanup_selected_tags (
172
+ cleanup_run_id INTEGER NOT NULL,
173
+ scan_id INTEGER NOT NULL,
174
+ tag TEXT NOT NULL,
175
+ is_deleted INTEGER NOT NULL,
176
+ PRIMARY KEY(cleanup_run_id, tag),
177
+ CHECK(is_deleted IN (0, 1)),
178
+ FOREIGN KEY(cleanup_run_id, scan_id) REFERENCES cleanup_runs(cleanup_run_id, scan_id),
179
+ FOREIGN KEY(scan_id, tag) REFERENCES tags(scan_id, tag)
180
+ );
181
+
182
+ CREATE TABLE IF NOT EXISTS cleanup_protected_root_blocks (
183
+ cleanup_run_id INTEGER NOT NULL,
184
+ scan_id INTEGER NOT NULL,
185
+ protected_digest TEXT NOT NULL,
186
+ blocked_digest TEXT NOT NULL,
187
+ block_reason_code TEXT NOT NULL,
188
+ overlap_digest TEXT NOT NULL,
189
+ PRIMARY KEY(cleanup_run_id, protected_digest, blocked_digest, overlap_digest),
190
+ CHECK(block_reason_code IN ('overlap-with-retained-root')),
191
+ FOREIGN KEY(cleanup_run_id, scan_id) REFERENCES cleanup_runs(cleanup_run_id, scan_id),
192
+ FOREIGN KEY(scan_id, protected_digest, overlap_digest)
193
+ REFERENCES manifest_reachability(scan_id, ancestor_digest, descendant_digest),
194
+ FOREIGN KEY(scan_id, blocked_digest, overlap_digest)
195
+ REFERENCES manifest_reachability(scan_id, ancestor_digest, descendant_digest)
196
+ );
197
+
198
+ CREATE INDEX IF NOT EXISTS idx_package_versions_scan_created_at ON package_versions(scan_id, created_at);
199
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_package_scans_scan_uuid ON package_scans(scan_uuid);
200
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_cleanup_runs_cleanup_uuid ON cleanup_runs(cleanup_uuid);
201
+ CREATE INDEX IF NOT EXISTS idx_package_scans_owner_name_started_at
202
+ ON package_scans(owner, package_name, scan_started_at DESC);
203
+ CREATE INDEX IF NOT EXISTS idx_cleanup_runs_scan_id ON cleanup_runs(scan_id);
204
+ CREATE INDEX IF NOT EXISTS idx_cleanup_protected_root_blocks_run_blocked
205
+ ON cleanup_protected_root_blocks(cleanup_run_id, blocked_digest);
206
+ CREATE INDEX IF NOT EXISTS idx_tags_scan_version ON tags(scan_id, version_id);
207
+ CREATE INDEX IF NOT EXISTS idx_tags_scan_version_nondigest
208
+ ON tags(scan_id, version_id)
209
+ WHERE is_digest_tag = 0;
210
+ CREATE INDEX IF NOT EXISTS idx_manifest_descriptors_scan_child ON manifest_descriptors(scan_id, child_digest);
211
+ CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_parent ON manifest_edges(scan_id, parent_digest);
212
+ CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child ON manifest_edges(scan_id, child_digest);
213
+ CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child_kind ON manifest_edges(scan_id, child_digest, edge_kind);
214
+ CREATE INDEX IF NOT EXISTS idx_manifest_edges_scan_child_nondigest_referrer
215
+ ON manifest_edges(scan_id, child_digest)
216
+ WHERE edge_kind != 'digest-tag-referrer';
217
+ CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant
218
+ ON manifest_reachability(scan_id, descendant_digest);
219
+ CREATE INDEX IF NOT EXISTS idx_manifest_reachability_scan_descendant_distance
220
+ ON manifest_reachability(scan_id, descendant_digest, min_distance);
221
+ CREATE INDEX IF NOT EXISTS idx_manifest_graphs_scan_graph_id
222
+ ON manifest_graphs(scan_id, graph_id);
@@ -0,0 +1,27 @@
1
+ DROP VIEW IF EXISTS v_latest_scan_per_package;
2
+
3
+ CREATE VIEW v_latest_scan_per_package AS
4
+ SELECT scan_id,
5
+ scan_uuid,
6
+ owner,
7
+ package_name,
8
+ scan_started_at,
9
+ scan_completed_at
10
+ FROM (
11
+ SELECT
12
+ ps.scan_id,
13
+ ps.scan_uuid,
14
+ ps.owner,
15
+ ps.package_name,
16
+ ps.scan_started_at,
17
+ ps.scan_completed_at,
18
+ ROW_NUMBER() OVER (
19
+ PARTITION BY ps.owner, ps.package_name
20
+ ORDER BY ps.scan_completed_at DESC, ps.scan_id DESC
21
+ ) AS rn
22
+ FROM package_scans ps
23
+ WHERE ps.scan_completed_at IS NOT NULL
24
+ AND ps.status = 'completed'
25
+ )
26
+ WHERE rn = 1
27
+ ;