Make a partly-undone import retryable, and refuse a plan it cannot deliver
This commit is contained in:
@@ -34,6 +34,8 @@ from datetime import datetime, timezone
|
||||
|
||||
BATCH_APPLIED = 'applied'
|
||||
BATCH_UNDONE = 'undone'
|
||||
# See shopdb/core/models/importbatch.py for why this exists.
|
||||
BATCH_PARTIAL = 'partially-undone'
|
||||
|
||||
OP_UPDATE = 'update'
|
||||
OP_INSERT = 'insert'
|
||||
@@ -150,6 +152,9 @@ def undo_batch(connection, batchid, references=None):
|
||||
{'batchid': batchid}).fetchone()
|
||||
if batch is None:
|
||||
raise ValueError('unknown batchid: %s' % batchid)
|
||||
# Only a FULLY undone batch is finished. A partially-undone one still has
|
||||
# entries with isreverted = 0, and the whole point of retrying is to pick
|
||||
# them up once whatever conflicted has been resolved.
|
||||
if batch[0] == BATCH_UNDONE:
|
||||
return UndoResult(batchid, 0, [])
|
||||
|
||||
@@ -184,10 +189,20 @@ def undo_batch(connection, batchid, references=None):
|
||||
else:
|
||||
conflicts.append(conflict)
|
||||
|
||||
# Record what actually happened, not what was attempted. Writing 'undone'
|
||||
# unconditionally is what made a conflicted entry unrevertable: this call
|
||||
# is the only way back, and the early return above refuses a second attempt
|
||||
# on an undone batch.
|
||||
#
|
||||
# undoneat is stamped only when the batch is genuinely finished; on a
|
||||
# partial it stays null, so "when was this reversed" keeps its meaning.
|
||||
status = BATCH_PARTIAL if conflicts else BATCH_UNDONE
|
||||
connection.execute(text(
|
||||
'UPDATE importbatches SET status = :status, undoneat = :undoneat '
|
||||
'WHERE batchid = :batchid'),
|
||||
{'status': BATCH_UNDONE, 'undoneat': _utcnow(), 'batchid': batchid})
|
||||
{'status': status,
|
||||
'undoneat': None if conflicts else _utcnow(),
|
||||
'batchid': batchid})
|
||||
|
||||
return UndoResult(batchid, reverted, conflicts)
|
||||
|
||||
@@ -218,10 +233,19 @@ def _revert_update(connection, table, targetid, column, oldvalue, newvalue):
|
||||
def _revert_insert(connection, batchid, table, targetid, references):
|
||||
"""Hard-delete a row this batch created, only if nothing outside the batch
|
||||
still references it. Returns a Conflict when it is not safe, else None."""
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
pk = _primary_key(connection, table)
|
||||
# A reference table may not exist here. Some referrers are plugin-owned -
|
||||
# notifications, for one - and a lean site drops the tables of plugins it
|
||||
# does not install (ADR-014). Querying an absent table raises 1146 on MySQL,
|
||||
# which rolls the whole undo back and leaves the batch permanently
|
||||
# un-undoable on that site. Skip what is not there; a table that does not
|
||||
# exist cannot hold a reference.
|
||||
present = set(inspect(connection).get_table_names())
|
||||
for reftable, refcolumn in (references or {}).get(table, []):
|
||||
if reftable not in present:
|
||||
continue
|
||||
referencing = connection.execute(text(
|
||||
'SELECT COUNT(*) FROM %s WHERE %s = :targetid'
|
||||
% (reftable, refcolumn)), {'targetid': targetid}).fetchone()[0]
|
||||
|
||||
Reference in New Issue
Block a user