积分冻结释放
This commit is contained in:
+772
@@ -0,0 +1,772 @@
|
||||
"""repair generation record frozen config
|
||||
|
||||
Revision ID: 6a3ea8d0b4c8
|
||||
Revises: 7cf645f7c418
|
||||
Create Date: 2026-07-23 14:01:54.109369
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "6a3ea8d0b4c8"
|
||||
down_revision: Union[str, None] = "7cf645f7c418"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
REPAIR_KEY = "repair_generation_record_frozen_config_20260723"
|
||||
BACKUP_TABLE = "generation_records_repair_backup_20260723"
|
||||
|
||||
|
||||
_TARGET_CONDITION = """
|
||||
gr.deleted_at IS NULL
|
||||
AND p.deleted_at IS NULL
|
||||
AND gr.status = 'prompt_optimized'
|
||||
AND gr.gen_type IN ('video', 'image')
|
||||
AND (
|
||||
gr.engine_id IS NULL
|
||||
OR gr.engine_id = ''
|
||||
OR gr.engine_snapshot_json IS NULL
|
||||
OR gr.engine_snapshot_json = ''
|
||||
OR gr.engine_snapshot_json IS NOT JSON
|
||||
OR (
|
||||
gr.gen_type = 'video'
|
||||
AND (
|
||||
gr.duration IS NULL
|
||||
OR gr.aspect_ratio IS NULL
|
||||
OR gr.aspect_ratio = ''
|
||||
OR gr.resolution IS NULL
|
||||
OR gr.resolution = ''
|
||||
)
|
||||
)
|
||||
OR (
|
||||
gr.gen_type = 'image'
|
||||
AND (
|
||||
gr.image_size IS NULL
|
||||
OR gr.image_size = ''
|
||||
OR gr.image_proportion IS NULL
|
||||
OR gr.image_proportion = ''
|
||||
OR gr.image_px IS NULL
|
||||
OR gr.image_px = ''
|
||||
)
|
||||
)
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
_CREATE_BACKUP_TABLE_SQL = f"""
|
||||
CREATE TABLE IF NOT EXISTS {BACKUP_TABLE} (
|
||||
repair_key VARCHAR(96) NOT NULL,
|
||||
record_id VARCHAR(32) NOT NULL,
|
||||
gen_type VARCHAR(16),
|
||||
old_engine_id VARCHAR(32),
|
||||
old_engine_snapshot_json TEXT,
|
||||
old_duration INTEGER,
|
||||
old_aspect_ratio VARCHAR(8),
|
||||
old_resolution VARCHAR(8),
|
||||
old_provider_generation_resolution VARCHAR(16),
|
||||
old_video_upscale_enabled_snapshot BOOLEAN,
|
||||
old_video_upscale_snapshot_json TEXT,
|
||||
old_image_size VARCHAR(8),
|
||||
old_image_proportion VARCHAR(8),
|
||||
old_image_px VARCHAR(10),
|
||||
old_include_media_references BOOLEAN,
|
||||
old_updated_at TIMESTAMP WITH TIME ZONE,
|
||||
backed_up_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (repair_key, record_id)
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
_INSERT_BACKUP_SQL = f"""
|
||||
INSERT INTO {BACKUP_TABLE} (
|
||||
repair_key,
|
||||
record_id,
|
||||
gen_type,
|
||||
old_engine_id,
|
||||
old_engine_snapshot_json,
|
||||
old_duration,
|
||||
old_aspect_ratio,
|
||||
old_resolution,
|
||||
old_provider_generation_resolution,
|
||||
old_video_upscale_enabled_snapshot,
|
||||
old_video_upscale_snapshot_json,
|
||||
old_image_size,
|
||||
old_image_proportion,
|
||||
old_image_px,
|
||||
old_include_media_references,
|
||||
old_updated_at
|
||||
)
|
||||
SELECT
|
||||
:repair_key,
|
||||
gr.id,
|
||||
gr.gen_type,
|
||||
gr.engine_id,
|
||||
gr.engine_snapshot_json,
|
||||
gr.duration,
|
||||
gr.aspect_ratio,
|
||||
gr.resolution,
|
||||
gr.provider_generation_resolution,
|
||||
gr.video_upscale_enabled_snapshot,
|
||||
gr.video_upscale_snapshot_json,
|
||||
gr.image_size,
|
||||
gr.image_proportion,
|
||||
gr.image_px,
|
||||
gr.include_media_references,
|
||||
gr.updated_at
|
||||
FROM generation_records gr
|
||||
JOIN projects p ON p.id = gr.project_id
|
||||
WHERE {_TARGET_CONDITION}
|
||||
ON CONFLICT (repair_key, record_id) DO NOTHING
|
||||
"""
|
||||
|
||||
|
||||
# PostgreSQL JSON helper snippets used inside SQL expressions. These avoid calling
|
||||
# jsonb_array_elements_text/jsonb_object_keys on malformed JSON or the wrong JSON type.
|
||||
_VIDEO_RATIOS_JSON = """
|
||||
CASE
|
||||
WHEN ve.supported_ratios IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(ve.supported_ratios::jsonb) = 'array'
|
||||
THEN ve.supported_ratios::jsonb
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
"""
|
||||
|
||||
_VIDEO_RESOLUTIONS_JSON = """
|
||||
CASE
|
||||
WHEN ve.supported_resolutions IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(ve.supported_resolutions::jsonb) = 'array'
|
||||
THEN ve.supported_resolutions::jsonb
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
"""
|
||||
|
||||
_VIDEO_DURATIONS_JSON = """
|
||||
CASE
|
||||
WHEN ve.supported_durations IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(ve.supported_durations::jsonb) = 'array'
|
||||
THEN ve.supported_durations::jsonb
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
"""
|
||||
|
||||
_IMAGE_SUPPORTED_MODELS_JSON = """
|
||||
CASE
|
||||
WHEN px_resolved.supported_models IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(px_resolved.supported_models::jsonb) = 'array'
|
||||
THEN px_resolved.supported_models::jsonb
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
"""
|
||||
|
||||
_IMAGE_SUPPORTED_SIZES_JSON_IE = """
|
||||
CASE
|
||||
WHEN ie.supported_sizes IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(ie.supported_sizes::jsonb) = 'object'
|
||||
THEN ie.supported_sizes::jsonb
|
||||
ELSE '{}'::jsonb
|
||||
END
|
||||
ELSE '{}'::jsonb
|
||||
END
|
||||
"""
|
||||
|
||||
_IMAGE_SUPPORTED_SIZES_JSON_ES = """
|
||||
CASE
|
||||
WHEN ie.supported_sizes IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(ie.supported_sizes::jsonb) = 'object'
|
||||
THEN ie.supported_sizes::jsonb
|
||||
ELSE '{}'::jsonb
|
||||
END
|
||||
ELSE '{}'::jsonb
|
||||
END
|
||||
"""
|
||||
|
||||
|
||||
_REPAIR_VIDEO_SQL = f"""
|
||||
WITH target_records AS (
|
||||
SELECT gr.*
|
||||
FROM generation_records gr
|
||||
JOIN projects p ON p.id = gr.project_id
|
||||
WHERE {_TARGET_CONDITION}
|
||||
AND gr.gen_type = 'video'
|
||||
),
|
||||
resolved AS (
|
||||
SELECT
|
||||
tr.id AS record_id,
|
||||
ve.id AS selected_engine_id,
|
||||
ve.name AS engine_name,
|
||||
ve.provider,
|
||||
ve.api_base,
|
||||
ve.api_key,
|
||||
ve.model_name,
|
||||
ve.generate_url,
|
||||
ve.query_url,
|
||||
ve.supported_ratios,
|
||||
ve.supported_resolutions,
|
||||
ve.supported_durations,
|
||||
ve.max_duration,
|
||||
ve.max_image_count,
|
||||
ve.max_video_count,
|
||||
ve.max_audio_count,
|
||||
ve.supports_universal_reference,
|
||||
ve.supports_first_last_frame,
|
||||
ve.multi_generation_enabled,
|
||||
ve.max_generation_count,
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN tr.duration IS NOT NULL
|
||||
AND tr.duration > 0
|
||||
AND (
|
||||
jsonb_array_length({_VIDEO_DURATIONS_JSON}) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements_text({_VIDEO_DURATIONS_JSON}) d(value)
|
||||
WHERE d.value ~ '^[0-9]+$'
|
||||
AND d.value::int = tr.duration
|
||||
)
|
||||
)
|
||||
AND (
|
||||
ve.max_duration IS NULL
|
||||
OR ve.max_duration <= 0
|
||||
OR tr.duration <= ve.max_duration
|
||||
)
|
||||
THEN tr.duration
|
||||
END,
|
||||
(
|
||||
SELECT d.value::int
|
||||
FROM jsonb_array_elements_text({_VIDEO_DURATIONS_JSON}) WITH ORDINALITY d(value, ord)
|
||||
WHERE d.value ~ '^[0-9]+$'
|
||||
AND d.value::int > 0
|
||||
AND (
|
||||
ve.max_duration IS NULL
|
||||
OR ve.max_duration <= 0
|
||||
OR d.value::int <= ve.max_duration
|
||||
)
|
||||
ORDER BY d.ord
|
||||
LIMIT 1
|
||||
),
|
||||
CASE
|
||||
WHEN ve.max_duration IS NOT NULL AND ve.max_duration > 0 THEN LEAST(4, ve.max_duration)
|
||||
ELSE 4
|
||||
END
|
||||
) AS final_duration,
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN tr.aspect_ratio IS NOT NULL
|
||||
AND tr.aspect_ratio <> ''
|
||||
AND (
|
||||
jsonb_array_length({_VIDEO_RATIOS_JSON}) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements_text({_VIDEO_RATIOS_JSON}) r(value)
|
||||
WHERE r.value = tr.aspect_ratio
|
||||
)
|
||||
)
|
||||
THEN tr.aspect_ratio
|
||||
END,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements_text({_VIDEO_RATIOS_JSON}) r(value)
|
||||
WHERE r.value = '16:9'
|
||||
)
|
||||
THEN '16:9'
|
||||
END,
|
||||
(
|
||||
SELECT r.value
|
||||
FROM jsonb_array_elements_text({_VIDEO_RATIOS_JSON}) WITH ORDINALITY r(value, ord)
|
||||
WHERE r.value <> ''
|
||||
ORDER BY r.ord
|
||||
LIMIT 1
|
||||
),
|
||||
'16:9'
|
||||
) AS final_aspect_ratio,
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN tr.resolution IS NOT NULL
|
||||
AND tr.resolution <> ''
|
||||
AND (
|
||||
jsonb_array_length({_VIDEO_RESOLUTIONS_JSON}) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements_text({_VIDEO_RESOLUTIONS_JSON}) r(value)
|
||||
WHERE r.value = tr.resolution
|
||||
)
|
||||
)
|
||||
THEN tr.resolution
|
||||
END,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements_text({_VIDEO_RESOLUTIONS_JSON}) r(value)
|
||||
WHERE r.value = '480p'
|
||||
)
|
||||
THEN '480p'
|
||||
END,
|
||||
(
|
||||
SELECT r.value
|
||||
FROM jsonb_array_elements_text({_VIDEO_RESOLUTIONS_JSON}) WITH ORDINALITY r(value, ord)
|
||||
WHERE r.value <> ''
|
||||
ORDER BY r.ord
|
||||
LIMIT 1
|
||||
),
|
||||
'480p'
|
||||
) AS final_resolution
|
||||
FROM target_records tr
|
||||
JOIN LATERAL (
|
||||
SELECT ve.*
|
||||
FROM video_engines ve
|
||||
WHERE ve.is_active IS TRUE
|
||||
AND ve.deleted_at IS NULL
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN (
|
||||
(
|
||||
tr.duration IS NULL
|
||||
OR tr.duration <= 0
|
||||
OR jsonb_array_length({_VIDEO_DURATIONS_JSON}) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements_text({_VIDEO_DURATIONS_JSON}) d(value)
|
||||
WHERE d.value ~ '^[0-9]+$'
|
||||
AND d.value::int = tr.duration
|
||||
)
|
||||
)
|
||||
AND (
|
||||
tr.aspect_ratio IS NULL
|
||||
OR tr.aspect_ratio = ''
|
||||
OR jsonb_array_length({_VIDEO_RATIOS_JSON}) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements_text({_VIDEO_RATIOS_JSON}) r(value)
|
||||
WHERE r.value = tr.aspect_ratio
|
||||
)
|
||||
)
|
||||
AND (
|
||||
tr.resolution IS NULL
|
||||
OR tr.resolution = ''
|
||||
OR jsonb_array_length({_VIDEO_RESOLUTIONS_JSON}) = 0
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_array_elements_text({_VIDEO_RESOLUTIONS_JSON}) r(value)
|
||||
WHERE r.value = tr.resolution
|
||||
)
|
||||
)
|
||||
)
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
ve.priority DESC,
|
||||
ve.created_at ASC,
|
||||
ve.id ASC
|
||||
LIMIT 1
|
||||
) ve ON TRUE
|
||||
)
|
||||
UPDATE generation_records gr
|
||||
SET
|
||||
engine_id = resolved.selected_engine_id,
|
||||
duration = resolved.final_duration,
|
||||
aspect_ratio = resolved.final_aspect_ratio,
|
||||
resolution = resolved.final_resolution,
|
||||
provider_generation_resolution = resolved.final_resolution,
|
||||
video_upscale_enabled_snapshot = FALSE,
|
||||
video_upscale_snapshot_json = NULL,
|
||||
engine_snapshot_json = jsonb_build_object(
|
||||
'engine_type', 'video',
|
||||
'id', resolved.selected_engine_id,
|
||||
'name', resolved.engine_name,
|
||||
'provider', resolved.provider,
|
||||
'api_base', resolved.api_base,
|
||||
'api_key_masked', CASE WHEN COALESCE(resolved.api_key, '') <> '' THEN '****' ELSE '' END,
|
||||
'model_name', resolved.model_name,
|
||||
'generate_url', resolved.generate_url,
|
||||
'query_url', resolved.query_url,
|
||||
'supported_ratios', CASE
|
||||
WHEN resolved.supported_ratios IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(resolved.supported_ratios::jsonb) = 'array'
|
||||
THEN resolved.supported_ratios::jsonb
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
ELSE '[]'::jsonb
|
||||
END,
|
||||
'supported_resolutions', CASE
|
||||
WHEN resolved.supported_resolutions IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(resolved.supported_resolutions::jsonb) = 'array'
|
||||
THEN resolved.supported_resolutions::jsonb
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
ELSE '[]'::jsonb
|
||||
END,
|
||||
'supported_durations', CASE
|
||||
WHEN resolved.supported_durations IS JSON THEN
|
||||
CASE
|
||||
WHEN jsonb_typeof(resolved.supported_durations::jsonb) = 'array'
|
||||
THEN resolved.supported_durations::jsonb
|
||||
ELSE '[]'::jsonb
|
||||
END
|
||||
ELSE '[]'::jsonb
|
||||
END,
|
||||
'max_duration', resolved.max_duration,
|
||||
'max_image_count', COALESCE(resolved.max_image_count, 0),
|
||||
'max_video_count', COALESCE(resolved.max_video_count, 0),
|
||||
'max_audio_count', COALESCE(resolved.max_audio_count, 0),
|
||||
'supports_universal_reference', COALESCE(resolved.supports_universal_reference, FALSE),
|
||||
'supports_first_last_frame', COALESCE(resolved.supports_first_last_frame, FALSE),
|
||||
'multi_generation_enabled', COALESCE(resolved.multi_generation_enabled, FALSE),
|
||||
'max_generation_count', LEAST(5, GREATEST(1, COALESCE(resolved.max_generation_count, 1))),
|
||||
'selected_ratio', resolved.final_aspect_ratio,
|
||||
'selected_resolution', resolved.final_resolution,
|
||||
'selected_duration', resolved.final_duration
|
||||
)::text,
|
||||
include_media_references = COALESCE(gr.include_media_references, FALSE),
|
||||
updated_at = NOW()
|
||||
FROM resolved
|
||||
WHERE gr.id = resolved.record_id
|
||||
"""
|
||||
|
||||
|
||||
_REPAIR_IMAGE_SQL = f"""
|
||||
WITH target_records AS (
|
||||
SELECT gr.*
|
||||
FROM generation_records gr
|
||||
JOIN projects p ON p.id = gr.project_id
|
||||
WHERE {_TARGET_CONDITION}
|
||||
AND gr.gen_type = 'image'
|
||||
),
|
||||
engine_selected AS (
|
||||
SELECT
|
||||
tr.*,
|
||||
ie.id AS selected_engine_id,
|
||||
ie.name AS engine_name,
|
||||
ie.provider,
|
||||
ie.api_base,
|
||||
ie.api_key,
|
||||
ie.model_name,
|
||||
ie.generate_url,
|
||||
ie.supported_models,
|
||||
ie.supported_sizes,
|
||||
ie.default_size,
|
||||
ie.multi_generation_enabled,
|
||||
ie.max_generation_count,
|
||||
ie.multi_image_max_images,
|
||||
ie.max_reference_image_count,
|
||||
ie.output_format,
|
||||
CASE
|
||||
WHEN ie.supported_sizes IS JSON AND jsonb_typeof(ie.supported_sizes::jsonb) = 'object'
|
||||
THEN ie.supported_sizes::jsonb
|
||||
ELSE '{{}}'::jsonb
|
||||
END AS sizes_json
|
||||
FROM target_records tr
|
||||
JOIN LATERAL (
|
||||
SELECT ie.*
|
||||
FROM image_engines ie
|
||||
WHERE ie.is_active IS TRUE
|
||||
AND ie.deleted_at IS NULL
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN (
|
||||
ie.supported_sizes IS NULL
|
||||
OR ie.supported_sizes = ''
|
||||
OR ie.supported_sizes IS NOT JSON
|
||||
OR jsonb_typeof(ie.supported_sizes::jsonb) <> 'object'
|
||||
OR ie.supported_sizes::jsonb = '{{}}'::jsonb
|
||||
OR (
|
||||
(
|
||||
tr.image_size IS NULL
|
||||
OR tr.image_size = ''
|
||||
OR ie.supported_sizes::jsonb ? tr.image_size
|
||||
)
|
||||
AND (
|
||||
tr.image_size IS NULL
|
||||
OR tr.image_size = ''
|
||||
OR tr.image_proportion IS NULL
|
||||
OR tr.image_proportion = ''
|
||||
OR (
|
||||
jsonb_typeof(ie.supported_sizes::jsonb -> tr.image_size) = 'object'
|
||||
AND (ie.supported_sizes::jsonb -> tr.image_size) ? tr.image_proportion
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
ie.priority DESC,
|
||||
ie.created_at ASC,
|
||||
ie.id ASC
|
||||
LIMIT 1
|
||||
) ie ON TRUE
|
||||
),
|
||||
size_selected AS (
|
||||
SELECT
|
||||
es.*,
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN es.image_size IS NOT NULL
|
||||
AND es.image_size <> ''
|
||||
AND (es.sizes_json = '{{}}'::jsonb OR es.sizes_json ? es.image_size)
|
||||
THEN es.image_size
|
||||
END,
|
||||
CASE
|
||||
WHEN es.default_size IS NOT NULL
|
||||
AND es.default_size <> ''
|
||||
AND (es.sizes_json = '{{}}'::jsonb OR es.sizes_json ? es.default_size)
|
||||
THEN es.default_size
|
||||
END,
|
||||
CASE WHEN es.sizes_json ? '2K' THEN '2K' END,
|
||||
(
|
||||
SELECT key
|
||||
FROM jsonb_object_keys(es.sizes_json) AS key
|
||||
ORDER BY key
|
||||
LIMIT 1
|
||||
),
|
||||
'2K'
|
||||
) AS final_image_size
|
||||
FROM engine_selected es
|
||||
),
|
||||
ratio_selected AS (
|
||||
SELECT
|
||||
ss.*,
|
||||
CASE
|
||||
WHEN jsonb_typeof(ss.sizes_json -> ss.final_image_size) = 'object'
|
||||
THEN ss.sizes_json -> ss.final_image_size
|
||||
ELSE '{{}}'::jsonb
|
||||
END AS ratio_json
|
||||
FROM size_selected ss
|
||||
),
|
||||
final_resolved AS (
|
||||
SELECT
|
||||
rs.*,
|
||||
COALESCE(
|
||||
CASE
|
||||
WHEN rs.image_proportion IS NOT NULL
|
||||
AND rs.image_proportion <> ''
|
||||
AND (rs.ratio_json = '{{}}'::jsonb OR rs.ratio_json ? rs.image_proportion)
|
||||
THEN rs.image_proportion
|
||||
END,
|
||||
CASE WHEN rs.ratio_json ? '1:1' THEN '1:1' END,
|
||||
(
|
||||
SELECT key
|
||||
FROM jsonb_object_keys(rs.ratio_json) AS key
|
||||
ORDER BY key
|
||||
LIMIT 1
|
||||
),
|
||||
'1:1'
|
||||
) AS final_image_proportion
|
||||
FROM ratio_selected rs
|
||||
),
|
||||
px_resolved AS (
|
||||
SELECT
|
||||
fr.*,
|
||||
regexp_replace(
|
||||
lower(
|
||||
replace(
|
||||
COALESCE(
|
||||
NULLIF(fr.ratio_json ->> fr.final_image_proportion, ''),
|
||||
NULLIF(fr.image_px, ''),
|
||||
'2048x2048'
|
||||
),
|
||||
'×',
|
||||
'x'
|
||||
)
|
||||
),
|
||||
'x+',
|
||||
'x',
|
||||
'g'
|
||||
) AS final_image_px
|
||||
FROM final_resolved fr
|
||||
)
|
||||
UPDATE generation_records gr
|
||||
SET
|
||||
engine_id = px_resolved.selected_engine_id,
|
||||
image_size = px_resolved.final_image_size,
|
||||
image_proportion = px_resolved.final_image_proportion,
|
||||
image_px = LEFT(px_resolved.final_image_px, 10),
|
||||
provider_generation_resolution = NULL,
|
||||
video_upscale_enabled_snapshot = FALSE,
|
||||
video_upscale_snapshot_json = NULL,
|
||||
engine_snapshot_json = jsonb_build_object(
|
||||
'engine_type', 'image',
|
||||
'id', px_resolved.selected_engine_id,
|
||||
'name', px_resolved.engine_name,
|
||||
'provider', px_resolved.provider,
|
||||
'api_base', px_resolved.api_base,
|
||||
'api_key_masked', CASE WHEN COALESCE(px_resolved.api_key, '') <> '' THEN '****' ELSE '' END,
|
||||
'model_name', px_resolved.model_name,
|
||||
'generate_url', px_resolved.generate_url,
|
||||
'supported_models', {_IMAGE_SUPPORTED_MODELS_JSON},
|
||||
'default_size', px_resolved.default_size,
|
||||
'multi_generation_enabled', COALESCE(px_resolved.multi_generation_enabled, FALSE),
|
||||
'max_generation_count', LEAST(5, GREATEST(1, COALESCE(px_resolved.max_generation_count, 1))),
|
||||
'multi_image_max_images', COALESCE(px_resolved.multi_image_max_images, 15),
|
||||
'max_reference_image_count', COALESCE(px_resolved.max_reference_image_count, 0),
|
||||
'output_format', lower(trim(COALESCE(px_resolved.output_format, ''))),
|
||||
'selected_size', px_resolved.final_image_size,
|
||||
'selected_proportion', px_resolved.final_image_proportion,
|
||||
'selected_px', LEFT(px_resolved.final_image_px, 10)
|
||||
)::text,
|
||||
include_media_references = COALESCE(gr.include_media_references, FALSE),
|
||||
updated_at = NOW()
|
||||
FROM px_resolved
|
||||
WHERE gr.id = px_resolved.id
|
||||
"""
|
||||
|
||||
|
||||
_DOWNGRADE_SQL = f"""
|
||||
UPDATE generation_records gr
|
||||
SET
|
||||
engine_id = b.old_engine_id,
|
||||
engine_snapshot_json = b.old_engine_snapshot_json,
|
||||
duration = b.old_duration,
|
||||
aspect_ratio = b.old_aspect_ratio,
|
||||
resolution = b.old_resolution,
|
||||
provider_generation_resolution = b.old_provider_generation_resolution,
|
||||
video_upscale_enabled_snapshot = COALESCE(b.old_video_upscale_enabled_snapshot, FALSE),
|
||||
video_upscale_snapshot_json = b.old_video_upscale_snapshot_json,
|
||||
image_size = b.old_image_size,
|
||||
image_proportion = b.old_image_proportion,
|
||||
image_px = b.old_image_px,
|
||||
include_media_references = COALESCE(b.old_include_media_references, FALSE),
|
||||
updated_at = b.old_updated_at
|
||||
FROM {BACKUP_TABLE} b
|
||||
WHERE b.repair_key = :repair_key
|
||||
AND b.record_id = gr.id
|
||||
AND gr.status = 'prompt_optimized'
|
||||
AND gr.deleted_at IS NULL
|
||||
"""
|
||||
|
||||
|
||||
_DROP_BACKUP_TABLE_SQL = f"DROP TABLE IF EXISTS {BACKUP_TABLE}"
|
||||
|
||||
|
||||
def _scalar_int(sql: str, **params: object) -> int:
|
||||
bind = op.get_bind()
|
||||
value = bind.execute(sa.text(sql), params).scalar()
|
||||
return int(value or 0)
|
||||
|
||||
|
||||
def _ensure_required_engines() -> None:
|
||||
video_targets = _scalar_int(
|
||||
f"""
|
||||
SELECT COUNT(*)
|
||||
FROM generation_records gr
|
||||
JOIN projects p ON p.id = gr.project_id
|
||||
WHERE {_TARGET_CONDITION}
|
||||
AND gr.gen_type = 'video'
|
||||
"""
|
||||
)
|
||||
image_targets = _scalar_int(
|
||||
f"""
|
||||
SELECT COUNT(*)
|
||||
FROM generation_records gr
|
||||
JOIN projects p ON p.id = gr.project_id
|
||||
WHERE {_TARGET_CONDITION}
|
||||
AND gr.gen_type = 'image'
|
||||
"""
|
||||
)
|
||||
video_engines = _scalar_int(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM video_engines
|
||||
WHERE is_active IS TRUE
|
||||
AND deleted_at IS NULL
|
||||
"""
|
||||
)
|
||||
image_engines = _scalar_int(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM image_engines
|
||||
WHERE is_active IS TRUE
|
||||
AND deleted_at IS NULL
|
||||
"""
|
||||
)
|
||||
|
||||
if video_targets > 0 and video_engines <= 0:
|
||||
raise RuntimeError("存在待修复的视频生成记录,但没有可用的视频引擎")
|
||||
if image_targets > 0 and image_engines <= 0:
|
||||
raise RuntimeError("存在待修复的图片生成记录,但没有可用的图片引擎")
|
||||
|
||||
|
||||
def _ensure_postgresql() -> None:
|
||||
bind = op.get_bind()
|
||||
dialect_name = getattr(bind.dialect, "name", "")
|
||||
if dialect_name != "postgresql":
|
||||
raise RuntimeError("本迁移只支持 PostgreSQL,当前数据库类型不支持此数据修复")
|
||||
|
||||
|
||||
def _execute(sql: str, **params: object) -> None:
|
||||
bind = op.get_bind()
|
||||
bind.execute(sa.text(sql), params)
|
||||
|
||||
|
||||
def _backup_table_exists() -> bool:
|
||||
return (
|
||||
_scalar_int(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = :table_name
|
||||
""",
|
||||
table_name=BACKUP_TABLE,
|
||||
)
|
||||
> 0
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Repair historical prompt_optimized GenerationRecord frozen config.
|
||||
|
||||
This migration only fills missing frozen generation configuration for old
|
||||
records. It does not change status, charge credits, create tasks, enqueue
|
||||
Celery jobs, or call application services.
|
||||
"""
|
||||
|
||||
_ensure_postgresql()
|
||||
_execute(_CREATE_BACKUP_TABLE_SQL)
|
||||
_ensure_required_engines()
|
||||
|
||||
# Keep the original values for a guarded downgrade. The ON CONFLICT clause
|
||||
# makes this migration safe to re-run inside a partially repaired database.
|
||||
_execute(_INSERT_BACKUP_SQL, repair_key=REPAIR_KEY)
|
||||
|
||||
# Repair by generation type. The update SQL only targets records that are
|
||||
# still prompt_optimized and still incomplete, so already repaired records
|
||||
# are skipped.
|
||||
_execute(_REPAIR_VIDEO_SQL)
|
||||
_execute(_REPAIR_IMAGE_SQL)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Restore backed-up values for records that are still not generated.
|
||||
|
||||
Records that moved past prompt_optimized are intentionally not restored;
|
||||
reverting those after users have generated media would corrupt production
|
||||
state.
|
||||
"""
|
||||
|
||||
_ensure_postgresql()
|
||||
if not _backup_table_exists():
|
||||
return
|
||||
|
||||
_execute(_DOWNGRADE_SQL, repair_key=REPAIR_KEY)
|
||||
_execute(_DROP_BACKUP_TABLE_SQL)
|
||||
Reference in New Issue
Block a user