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:
@@ -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'],
|
||||
)
|
||||
@@ -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'],
|
||||
)
|
||||
@@ -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'],
|
||||
)
|
||||
@@ -437,6 +437,7 @@ async def get_consolidated_settings(
|
||||
"api_key": "********",
|
||||
"default_model": p.default_model,
|
||||
"is_active": p.is_active,
|
||||
"is_multimodal": bool(p.is_multimodal) if p.is_multimodal is not None else False,
|
||||
}
|
||||
for p in providers
|
||||
]
|
||||
|
||||
@@ -74,7 +74,7 @@ class DatasetReviewSession(Base):
|
||||
|
||||
session_id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(String, ForeignKey("users.id"), nullable=False)
|
||||
environment_id = Column(String, ForeignKey("environments.id"), nullable=False)
|
||||
environment_id = Column(String, ForeignKey("environments.id", ondelete="CASCADE"), nullable=False)
|
||||
source_kind = Column(String, nullable=False)
|
||||
source_input = Column(String, nullable=False)
|
||||
dataset_ref = Column(String, nullable=False)
|
||||
|
||||
@@ -58,8 +58,8 @@ class DatabaseMapping(Base):
|
||||
__tablename__ = "database_mappings"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
source_env_id = Column(String, ForeignKey("environments.id"), nullable=False)
|
||||
target_env_id = Column(String, ForeignKey("environments.id"), nullable=False)
|
||||
source_env_id = Column(String, ForeignKey("environments.id", ondelete="CASCADE"), nullable=False)
|
||||
target_env_id = Column(String, ForeignKey("environments.id", ondelete="CASCADE"), nullable=False)
|
||||
source_db_uuid = Column(String, nullable=False)
|
||||
target_db_uuid = Column(String, nullable=False)
|
||||
source_db_name = Column(String, nullable=False)
|
||||
@@ -73,8 +73,8 @@ class MigrationJob(Base):
|
||||
__tablename__ = "migration_jobs"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
source_env_id = Column(String, ForeignKey("environments.id"), nullable=False)
|
||||
target_env_id = Column(String, ForeignKey("environments.id"), nullable=False)
|
||||
source_env_id = Column(String, ForeignKey("environments.id", ondelete="CASCADE"), nullable=False)
|
||||
target_env_id = Column(String, ForeignKey("environments.id", ondelete="CASCADE"), nullable=False)
|
||||
status = Column(SQLEnum(MigrationStatus), default=MigrationStatus.PENDING)
|
||||
replace_db = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -88,7 +88,7 @@ class ResourceMapping(Base):
|
||||
__tablename__ = "resource_mappings"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
environment_id = Column(String, ForeignKey("environments.id"), nullable=False)
|
||||
environment_id = Column(String, ForeignKey("environments.id", ondelete="CASCADE"), nullable=False)
|
||||
resource_type = Column(SQLEnum(ResourceType), nullable=False)
|
||||
uuid = Column(String, nullable=False)
|
||||
remote_integer_id = Column(String, nullable=False) # Stored as string to handle potentially large or composite IDs safely, though Superset usually uses integers.
|
||||
|
||||
@@ -22,7 +22,7 @@ class TaskRecord(Base):
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
type = Column(String, nullable=False) # e.g., "backup", "migration"
|
||||
status = Column(String, nullable=False) # Enum: "PENDING", "RUNNING", "SUCCESS", "FAILED"
|
||||
environment_id = Column(String, ForeignKey("environments.id"), nullable=True)
|
||||
environment_id = Column(String, ForeignKey("environments.id", ondelete="SET NULL"), nullable=True)
|
||||
started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
finished_at = Column(DateTime(timezone=True), nullable=True)
|
||||
logs = Column(JSON, nullable=True) # Store structured logs as JSON (legacy, kept for backward compatibility)
|
||||
|
||||
@@ -71,7 +71,7 @@ class TranslationRun(Base):
|
||||
__tablename__ = "translation_runs"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id"), nullable=False, index=True)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
status = Column(String, nullable=False, default="PENDING") # PENDING, RUNNING, COMPLETED, FAILED, CANCELLED
|
||||
trigger_type = Column(String, nullable=True, comment="manual, scheduled, retry, baseline_expired")
|
||||
started_at = Column(DateTime, nullable=True)
|
||||
@@ -101,7 +101,7 @@ class TranslationBatch(Base):
|
||||
__tablename__ = "translation_batches"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
batch_index = Column(Integer, nullable=False)
|
||||
status = Column(String, nullable=False, default="PENDING")
|
||||
total_records = Column(Integer, default=0)
|
||||
@@ -119,8 +119,8 @@ class TranslationRecord(Base):
|
||||
__tablename__ = "translation_records"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
batch_id = Column(String, ForeignKey("translation_batches.id"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id"), nullable=False, index=True)
|
||||
batch_id = Column(String, ForeignKey("translation_batches.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
source_sql = Column(Text, nullable=True)
|
||||
target_sql = Column(Text, nullable=True)
|
||||
source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset
|
||||
@@ -151,8 +151,8 @@ class TranslationEvent(Base):
|
||||
__tablename__ = "translation_events"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id"), nullable=True, index=True)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
event_type = Column(String, nullable=False) # JOB_CREATED, JOB_STARTED, JOB_COMPLETED, JOB_FAILED, etc.
|
||||
event_data = Column(JSON, nullable=True)
|
||||
created_by = Column(String, nullable=True)
|
||||
@@ -166,8 +166,8 @@ class TranslationPreviewSession(Base):
|
||||
__tablename__ = "translation_preview_sessions"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id"), nullable=True)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="SET NULL"), nullable=True)
|
||||
status = Column(String, nullable=False, default="ACTIVE") # ACTIVE, APPLIED, DISCARDED, EXPIRED
|
||||
created_by = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
||||
@@ -181,7 +181,7 @@ class TranslationPreviewRecord(Base):
|
||||
__tablename__ = "translation_preview_records"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
session_id = Column(String, ForeignKey("translation_preview_sessions.id"), nullable=False, index=True)
|
||||
session_id = Column(String, ForeignKey("translation_preview_sessions.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
source_sql = Column(Text, nullable=True)
|
||||
target_sql = Column(Text, nullable=True)
|
||||
source_object_type = Column(String, nullable=True)
|
||||
@@ -219,7 +219,7 @@ class DictionaryEntry(Base):
|
||||
__tablename__ = "dictionary_entries"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
dictionary_id = Column(String, ForeignKey("terminology_dictionaries.id"), nullable=False, index=True)
|
||||
dictionary_id = Column(String, ForeignKey("terminology_dictionaries.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
source_term = Column(String, nullable=False)
|
||||
source_term_normalized = Column(String, nullable=False)
|
||||
target_term = Column(String, nullable=False)
|
||||
@@ -254,7 +254,7 @@ class TranslationSchedule(Base):
|
||||
__tablename__ = "translation_schedules"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id"), nullable=False, index=True)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
cron_expression = Column(String, nullable=False)
|
||||
timezone = Column(String, nullable=False, default="UTC")
|
||||
is_active = Column(Boolean, default=True)
|
||||
@@ -273,8 +273,8 @@ class TranslationJobDictionary(Base):
|
||||
__tablename__ = "translation_job_dictionaries"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id"), nullable=False, index=True)
|
||||
dictionary_id = Column(String, ForeignKey("terminology_dictionaries.id"), nullable=False, index=True)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
dictionary_id = Column(String, ForeignKey("terminology_dictionaries.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
||||
|
||||
__table_args__ = (
|
||||
@@ -292,8 +292,8 @@ class MetricSnapshot(Base):
|
||||
__tablename__ = "translation_metric_snapshots"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id"), nullable=True, index=True)
|
||||
job_id = Column(String, ForeignKey("translation_jobs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||
key_hash = Column(String, nullable=False, comment="Hash of dimension key fields for aggregation")
|
||||
config_hash = Column(String, nullable=True, comment="Hash of translation configuration state")
|
||||
dict_snapshot_hash = Column(String, nullable=True, comment="Hash of dictionary state at capture time")
|
||||
@@ -324,7 +324,7 @@ class TranslationLanguage(Base):
|
||||
__tablename__ = "translation_languages"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
record_id = Column(String, ForeignKey("translation_records.id"), nullable=False)
|
||||
record_id = Column(String, ForeignKey("translation_records.id", ondelete="CASCADE"), nullable=False)
|
||||
language_code = Column(String, nullable=False, comment="BCP-47 language code")
|
||||
source_language_detected = Column(String, nullable=True, comment="BCP-47 or 'und' for undetermined")
|
||||
translated_value = Column(Text, nullable=True, comment="LLM-generated translation")
|
||||
@@ -352,7 +352,7 @@ class TranslationPreviewLanguage(Base):
|
||||
__tablename__ = "translation_preview_languages"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
preview_record_id = Column(String, ForeignKey("translation_preview_records.id"), nullable=False)
|
||||
preview_record_id = Column(String, ForeignKey("translation_preview_records.id", ondelete="CASCADE"), nullable=False)
|
||||
language_code = Column(String, nullable=False, comment="BCP-47 language code")
|
||||
source_language_detected = Column(String, nullable=True, comment="BCP-47 or 'und'")
|
||||
translated_value = Column(Text, nullable=True)
|
||||
@@ -376,7 +376,7 @@ class TranslationRunLanguageStats(Base):
|
||||
__tablename__ = "translation_run_language_stats"
|
||||
|
||||
id = Column(String, primary_key=True, default=generate_uuid)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id"), nullable=False)
|
||||
run_id = Column(String, ForeignKey("translation_runs.id", ondelete="CASCADE"), nullable=False)
|
||||
language_code = Column(String, nullable=False, comment="BCP-47 language code")
|
||||
total_rows = Column(Integer, default=0)
|
||||
translated_rows = Column(Integer, default=0)
|
||||
|
||||
Reference in New Issue
Block a user