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

@@ -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
]

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)