mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-26 21:17:13 +09:00
fix(models): cascade one-to-one child rows on application purge
Purging an app from the Recycle Bin crashed with IntegrityError (NOT NULL constraint failed: container_scale_policies.application_id) when the app had an auto-scale policy: with no delete cascade, SQLAlchemy nulls the child FK on parent delete, and these one-to-one rows declare it NOT NULL. Scale policy, sleep policy and wp_site all share the shape; all three now ride the purge. Reported on Discord (vaultwarden/hedgedoc delete-reinstall flow). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b966f57533
commit
c1d4c0af12
4 files changed
+42
-3
No files matched your search
@@ -23,7 +23,10 @@ class ContainerScalePolicy(db.Model):
|
||||
last_scaled_at = db.Column(db.DateTime)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
application = db.relationship('Application', backref=db.backref('scale_policy', uselist=False))
|
||||
# cascade: application_id is NOT NULL, so a real delete of the app (Recycle
|
||||
# Bin purge) must take this row with it — the default null-out IntegrityErrors.
|
||||
application = db.relationship('Application', backref=db.backref(
|
||||
'scale_policy', uselist=False, cascade='all, delete-orphan'))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
||||
@@ -17,7 +17,10 @@ class ContainerSleepPolicy(db.Model):
|
||||
asleep = db.Column(db.Boolean, default=False, nullable=False)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
application = db.relationship('Application', backref=db.backref('sleep_policy', uselist=False))
|
||||
# cascade: application_id is NOT NULL, so a real delete of the app (Recycle
|
||||
# Bin purge) must take this row with it — the default null-out IntegrityErrors.
|
||||
application = db.relationship('Application', backref=db.backref(
|
||||
'sleep_policy', uselist=False, cascade='all, delete-orphan'))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
||||
@@ -100,7 +100,10 @@ class WordPressSite(JsonColumnMixin, TimestampMixin, db.Model):
|
||||
# Timestamps
|
||||
|
||||
# Relationships
|
||||
application = db.relationship('Application', backref=db.backref('wp_site', uselist=False))
|
||||
# cascade: application_id is NOT NULL, so a real delete of the app (Recycle
|
||||
# Bin purge) must take this row with it — the default null-out IntegrityErrors.
|
||||
application = db.relationship('Application', backref=db.backref(
|
||||
'wp_site', uselist=False, cascade='all, delete-orphan'))
|
||||
environments = db.relationship(
|
||||
'WordPressSite',
|
||||
backref=db.backref('production_site', remote_side=[id]),
|
||||
|
||||
@@ -157,6 +157,36 @@ def test_purge_is_what_removes_the_volumes(app, dead_app, monkeypatch):
|
||||
assert calls and calls[0].get('volumes') is True
|
||||
|
||||
|
||||
def test_purge_takes_the_one_to_one_policy_rows_with_it(app, dead_app, monkeypatch):
|
||||
"""Field report: purging an app that had an auto-scale policy died with
|
||||
`IntegrityError: NOT NULL constraint failed: container_scale_policies
|
||||
.application_id`. Without a delete cascade SQLAlchemy's default on parent
|
||||
delete is to NULL the child FK — and these one-to-one rows declare the FK
|
||||
NOT NULL, so the purge itself crashed. The policy rows must ride along."""
|
||||
from app.services import docker_service
|
||||
monkeypatch.setattr(docker_service.DockerService, 'compose_down',
|
||||
classmethod(lambda cls, path, **kw: {'success': True}))
|
||||
monkeypatch.setattr(application_restore.CronService, 'clear_application',
|
||||
classmethod(lambda cls, _id: 0))
|
||||
|
||||
from app.models.container_scale_policy import ContainerScalePolicy
|
||||
from app.models.container_sleep_policy import ContainerSleepPolicy
|
||||
|
||||
with app.app_context():
|
||||
db.session.add(ContainerScalePolicy(application_id=dead_app.id))
|
||||
db.session.add(ContainerSleepPolicy(application_id=dead_app.id))
|
||||
db.session.commit()
|
||||
|
||||
ok, warning = recycle_bin_service.purge('application', dead_app.id)
|
||||
assert ok
|
||||
assert warning is None, warning
|
||||
assert Application.query.get(dead_app.id) is None
|
||||
assert ContainerScalePolicy.query.filter_by(
|
||||
application_id=dead_app.id).first() is None
|
||||
assert ContainerSleepPolicy.query.filter_by(
|
||||
application_id=dead_app.id).first() is None
|
||||
|
||||
|
||||
def test_purge_honours_an_explicit_remove_data_false(app, dead_app, monkeypatch):
|
||||
"""Database engines default to preserving their volumes — losing the data is
|
||||
the only irreversible part of that uninstall."""
|
||||
|
||||
Reference in new issue
Block a user