Skip to content

Commit 9a454fa

Browse files
committed
feat: Use temp table for deletion
Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
1 parent 3def763 commit 9a454fa

1 file changed

Lines changed: 79 additions & 51 deletions

File tree

backend/kernelCI_app/management/commands/prune_db.py

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -98,65 +98,82 @@ def handle(self, *args, **options):
9898
dry_run = options["dry_run"]
9999
origins = options["origins"]
100100

101-
params = {"cutoff": cutoff, "batch_size": options["batch_size"]}
101+
params = {"cutoff": cutoff}
102102
origins_condition = ""
103103
if origins:
104104
params["origins"] = list(origins)
105105
origins_condition = "AND origin = ANY(%(origins)s)"
106106

107107
where_clauses = self._build_where_clauses(origins_condition, protect_incidents)
108+
temp_tables = {t: f"prune_{t}" for t in selected_tables}
108109

109110
with connections["default"].cursor() as cursor:
110-
counts = {
111-
t: self._count(cursor, t, where_clauses[t], params)
112-
for t in selected_tables
113-
}
114-
total = sum(counts.values())
115-
116-
lines = [f"Rows older than {cutoff.isoformat()}:"]
117-
lines += [f"* {t}:\t{counts[t]:>8}" for t in selected_tables]
118-
lines += ["----------------------", f"* total:\t{total:>8}"]
119-
lines.append("Note: counts include children cascaded from pruned parents.")
120-
if protect_incidents:
121-
lines.append("Note: rows linked to an incident are kept.")
122-
self.stdout.write("\n".join(lines))
111+
try:
112+
# Snapshot ids while parents still exist, so child predicates can
113+
# resolve them. Do all tables before deleting anything.
114+
for table in selected_tables:
115+
self._materialize(
116+
cursor, table, temp_tables[table], where_clauses[table], params
117+
)
123118

124-
if total == 0:
125-
self.stdout.write(self.style.SUCCESS("Nothing to prune."))
126-
return
119+
counts = {
120+
t: self._count(cursor, temp_tables[t]) for t in selected_tables
121+
}
122+
total = sum(counts.values())
127123

128-
if dry_run:
129-
self.stdout.write(
130-
self.style.WARNING(
131-
"[DRY RUN] No rows deleted. Run without --dry-run to execute."
132-
)
124+
lines = [f"Rows older than {cutoff.isoformat()}:"]
125+
lines += [f"* {t}:\t{counts[t]:>8}" for t in selected_tables]
126+
lines += ["----------------------", f"* total:\t{total:>8}"]
127+
lines.append(
128+
"Note: counts include children cascaded from pruned parents."
133129
)
134-
return
135-
136-
if not options["yes"]:
137-
summary = ", ".join(f"{counts[t]} {t}" for t in selected_tables)
138-
try:
139-
answer = (
140-
input(f"Delete {summary} ({total} rows total)? [y/N] ")
141-
.strip()
142-
.lower()
130+
if protect_incidents:
131+
lines.append("Note: rows linked to an incident are kept.")
132+
self.stdout.write("\n".join(lines))
133+
134+
if total == 0:
135+
self.stdout.write(self.style.SUCCESS("Nothing to prune."))
136+
return
137+
138+
if dry_run:
139+
self.stdout.write(
140+
self.style.WARNING(
141+
"[DRY RUN] No rows deleted. Run without --dry-run to "
142+
"execute."
143+
)
143144
)
144-
except EOFError:
145-
answer = ""
146-
if answer not in ("y", "yes"):
147-
self.stdout.write("Aborted.")
148145
return
149146

150-
# Delete child-first (reverse of PRUNABLE_TABLES order): each batch commits
151-
# on its own, so a crash mid-run leaves children already gone before their
152-
# parents, never the reverse. Reordering this would risk orphans.
153-
deleted = 0
154-
for table in reversed(selected_tables):
155-
deleted += self._batch_delete(
156-
cursor, table, where_clauses[table], params
157-
)
147+
if not options["yes"]:
148+
summary = ", ".join(f"{counts[t]} {t}" for t in selected_tables)
149+
try:
150+
answer = (
151+
input(f"Delete {summary} ({total} rows total)? [y/N] ")
152+
.strip()
153+
.lower()
154+
)
155+
except EOFError:
156+
answer = ""
157+
if answer not in ("y", "yes"):
158+
self.stdout.write("Aborted.")
159+
return
160+
161+
# Delete child-first (reverse of PRUNABLE_TABLES order): each batch
162+
# commits on its own, so a crash mid-run leaves children already gone
163+
# before their parents, never the reverse. Reordering this would risk
164+
# orphans.
165+
deleted = 0
166+
for table in reversed(selected_tables):
167+
deleted += self._batch_delete(
168+
cursor, table, temp_tables[table], options["batch_size"]
169+
)
158170

159-
self.stdout.write(self.style.SUCCESS(f"Successfully pruned {deleted} rows."))
171+
self.stdout.write(
172+
self.style.SUCCESS(f"Successfully pruned {deleted} rows.")
173+
)
174+
finally:
175+
for temp_table in temp_tables.values():
176+
cursor.execute(f"DROP TABLE IF EXISTS {temp_table}")
160177

161178
def _build_where_clauses(self, origins_condition, protect_incidents):
162179
"""Build the per-table WHERE clauses, chaining the cascade so each child
@@ -200,18 +217,29 @@ def _build_where_clauses(self, origins_condition, protect_incidents):
200217
"tests": test_where,
201218
}
202219

203-
def _count(self, cursor, table, where, params):
204-
cursor.execute(f"SELECT COUNT(*) FROM {table} WHERE {where}", params)
220+
def _materialize(self, cursor, table, temp_table, where, params):
221+
"""Snapshot the doomed ids into a temp table so the nested predicate runs
222+
once instead of per batch."""
223+
cursor.execute(f"DROP TABLE IF EXISTS {temp_table}")
224+
cursor.execute(
225+
f"CREATE TEMP TABLE {temp_table} AS SELECT id FROM {table} WHERE {where}",
226+
params,
227+
)
228+
229+
def _count(self, cursor, temp_table):
230+
cursor.execute(f"SELECT COUNT(*) FROM {temp_table}")
205231
return cursor.fetchone()[0]
206232

207-
def _batch_delete(self, cursor, table, where, params):
208-
delete_sql = (
209-
f"DELETE FROM {table} WHERE id IN "
210-
f"(SELECT id FROM {table} WHERE {where} LIMIT %(batch_size)s)"
233+
def _batch_delete(self, cursor, table, temp_table, batch_size):
234+
sql = (
235+
f"WITH batch AS ("
236+
f"DELETE FROM {temp_table} WHERE id IN "
237+
f"(SELECT id FROM {temp_table} LIMIT %(batch_size)s) RETURNING id"
238+
f") DELETE FROM {table} WHERE id IN (SELECT id FROM batch)"
211239
)
212240
deleted_total = 0
213241
while True:
214-
cursor.execute(delete_sql, params)
242+
cursor.execute(sql, {"batch_size": batch_size})
215243
deleted = cursor.rowcount
216244
if deleted == 0:
217245
break

0 commit comments

Comments
 (0)