fix(db): add ondelete cascade to all FK constraints, fix multimodal flag persistence

- Add ondelete=CASCADE/SET NULL to all environment and translate FK constraints
- Add is_multimodal to GET /api/settings/consolidated response
- Fix toggleActive to not overwrite is_multimodal with false
- New SearchableMultiSelect component for dashboard search with multi-select
- Fix validation task page: task data unwrapping, date formatting, dashboard multi-select
- Fix infinite effect loop on dashboards page via queueMicrotask guard
- 3 new Alembic migrations (merge f0e9d8c7b6a5, translate b0c1d2e3f4a5)
This commit is contained in:
2026-05-22 09:21:24 +03:00
parent 48cc02ea0b
commit 0653a75ee7
13 changed files with 549 additions and 73 deletions

View File

@@ -0,0 +1,50 @@
"""set null ondelete for task_records environment FK
Revision ID: a5b6c7d8e9f0
Revises: c4a3a2f74bfe
Create Date: 2026-05-21 18:55:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'a5b6c7d8e9f0'
down_revision: Union[str, Sequence[str], None] = 'c4a3a2f74bfe'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.drop_constraint(
'task_records_environment_id_fkey',
'task_records',
type_='foreignkey',
)
op.create_foreign_key(
'task_records_environment_id_fkey',
'task_records',
'environments',
['environment_id'],
['id'],
ondelete='SET NULL',
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_constraint(
'task_records_environment_id_fkey',
'task_records',
type_='foreignkey',
)
op.create_foreign_key(
'task_records_environment_id_fkey',
'task_records',
'environments',
['environment_id'],
['id'],
)

View File

@@ -0,0 +1,83 @@
"""cascade ondelete for all translate FK references (translation_jobs/runs/batches chain)
Revision ID: b0c1d2e3f4a5
Revises: f0e9d8c7b6a5
Create Date: 2026-05-21 19:05:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b0c1d2e3f4a5'
down_revision: Union[str, Sequence[str], None] = 'f0e9d8c7b6a5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# Each entry: (table, constraint_name, column, ref_table, ondelete_rule)
FK_FIXES = [
# Direct FKs to translation_jobs.id
('translation_runs', 'translation_runs_job_id_fkey', 'job_id', 'CASCADE'),
('translation_events', 'translation_events_job_id_fkey', 'job_id', 'CASCADE'),
('translation_preview_sessions', 'translation_preview_sessions_job_id_fkey', 'job_id', 'CASCADE'),
('translation_schedules', 'translation_schedules_job_id_fkey', 'job_id', 'CASCADE'),
('translation_job_dictionaries', 'translation_job_dictionaries_job_id_fkey', 'job_id', 'CASCADE'),
('translation_metric_snapshots', 'translation_metric_snapshots_job_id_fkey', 'job_id', 'CASCADE'),
# FKs to translation_runs.id
('translation_batches', 'translation_batches_run_id_fkey', 'run_id', 'CASCADE'),
('translation_records', 'translation_records_run_id_fkey', 'run_id', 'CASCADE'),
('translation_events', 'translation_events_run_id_fkey', 'run_id', 'SET NULL'),
('translation_preview_sessions', 'translation_preview_sessions_run_id_fkey', 'run_id', 'SET NULL'),
('translation_metric_snapshots', 'translation_metric_snapshots_run_id_fkey', 'run_id', 'SET NULL'),
('translation_run_language_stats', 'translation_run_language_stats_run_id_fkey', 'run_id', 'CASCADE'),
# FKs to translation_batches.id
('translation_records', 'translation_records_batch_id_fkey', 'batch_id', 'CASCADE'),
# FKs to translation_records.id
('translation_languages', 'translation_languages_record_id_fkey', 'record_id', 'CASCADE'),
# FKs to translation_preview_sessions.id
('translation_preview_records', 'translation_preview_records_session_id_fkey', 'session_id', 'CASCADE'),
# FKs to translation_preview_records.id
('translation_preview_languages', 'translation_preview_languages_preview_record_id_fkey', 'preview_record_id', 'CASCADE'),
]
def upgrade() -> None:
"""Upgrade schema."""
for table, constraint, column, ondelete in FK_FIXES:
op.drop_constraint(constraint, table, type_='foreignkey')
op.create_foreign_key(
constraint, table, 'translation_jobs' if 'job_id' in column else (
'translation_runs' if 'run_id' in column else (
'translation_batches' if 'batch_id' in column else (
'translation_records' if 'record_id' in column and 'preview' not in constraint else (
'translation_preview_sessions' if 'session_id' in column else (
'translation_preview_records' if 'preview_record_id' in column else 'unknown'
)
)
)
)
),
[column], ['id'],
ondelete=ondelete,
)
def downgrade() -> None:
"""Downgrade schema."""
ref_map = {
'job_id': 'translation_jobs',
'run_id': 'translation_runs',
'batch_id': 'translation_batches',
'record_id': 'translation_records',
'session_id': 'translation_preview_sessions',
'preview_record_id': 'translation_preview_records',
}
for table, constraint, column, _ondelete in reversed(FK_FIXES):
ref_table = ref_map[column]
op.drop_constraint(constraint, table, type_='foreignkey')
op.create_foreign_key(
constraint, table, ref_table,
[column], ['id'],
)

View File

@@ -0,0 +1,53 @@
"""cascade ondelete for all remaining environments FK references
Revision ID: f0e9d8c7b6a5
Revises: a5b6c7d8e9f0, e5f4d3c2b1a
Create Date: 2026-05-21 19:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'f0e9d8c7b6a5'
down_revision: Union[str, Sequence[str], None] = ('a5b6c7d8e9f0', 'e5f4d3c2b1a')
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
FK_DEFS = [
('resource_mappings', 'resource_mappings_environment_id_fkey', 'environment_id'),
('database_mappings', 'database_mappings_source_env_id_fkey', 'source_env_id'),
('database_mappings', 'database_mappings_target_env_id_fkey', 'target_env_id'),
('migration_jobs', 'migration_jobs_source_env_id_fkey', 'source_env_id'),
('migration_jobs', 'migration_jobs_target_env_id_fkey', 'target_env_id'),
('dataset_review_sessions', 'dataset_review_sessions_environment_id_fkey', 'environment_id'),
]
def upgrade() -> None:
"""Upgrade schema."""
for table, constraint_name, column in FK_DEFS:
op.drop_constraint(constraint_name, table, type_='foreignkey')
op.create_foreign_key(
constraint_name,
table,
'environments',
[column],
['id'],
ondelete='CASCADE',
)
def downgrade() -> None:
"""Downgrade schema."""
for table, constraint_name, column in reversed(FK_DEFS):
op.drop_constraint(constraint_name, table, type_='foreignkey')
op.create_foreign_key(
constraint_name,
table,
'environments',
[column],
['id'],
)