fix: repair structured date validator migration
This commit is contained in:
@@ -67,6 +67,7 @@ jobs:
|
||||
frontend/supabase/migrations/20260724020000_align_global_birthplace_rectification_contract.sql \
|
||||
frontend/supabase/migrations/20260724030000_allow_assistant_only_rectification_regenerate.sql \
|
||||
frontend/supabase/migrations/20260725010000_structured_conversational_date_confirmation.sql \
|
||||
frontend/supabase/migrations/20260725020000_repair_structured_conversational_date_validator.sql \
|
||||
"$DEPLOY_USER@$DEPLOY_HOST:$REMOTE_DIR/"
|
||||
|
||||
- name: Check or apply reviewed migrations
|
||||
@@ -118,7 +119,8 @@ jobs:
|
||||
"$REMOTE_DIR/20260724010000_global_birth_locations.sql" \
|
||||
"$REMOTE_DIR/20260724020000_align_global_birthplace_rectification_contract.sql" \
|
||||
"$REMOTE_DIR/20260724030000_allow_assistant_only_rectification_regenerate.sql" \
|
||||
"$REMOTE_DIR/20260725010000_structured_conversational_date_confirmation.sql"
|
||||
"$REMOTE_DIR/20260725010000_structured_conversational_date_confirmation.sql" \
|
||||
"$REMOTE_DIR/20260725020000_repair_structured_conversational_date_validator.sql"
|
||||
do
|
||||
filename="$(basename "$sql_file")"
|
||||
checksum="$(sha256sum "$sql_file" | awk '{print $1}')"
|
||||
|
||||
@@ -1338,3 +1338,18 @@
|
||||
- 防复发:新增需要生产执行的 migration 时,必须同时更新受审生产迁移清单并由测试覆盖上传和执行两处引用。
|
||||
- 相关记录:BUG-068、BUG-069
|
||||
- 修复版本:待提交(本地可测)
|
||||
|
||||
## BUG-071 | 结构化日期校验器的冗余正则在 PostgreSQL 执行时报错
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-07-25
|
||||
- 最近更新:2026-07-25
|
||||
- 影响面:生产环境生时校正首次建案、`proposedDate` 持久化与预留点数释放
|
||||
- 用户现象:开始生时校正时接口返回 `409 action_conflict` 和“请加载最新进度后再试”,预留点数随后自动释放且没有创建 case。
|
||||
- 触发条件:首轮 Agent 生成带 `followUp.answerMode=yes_no` 和 `followUp.proposedDate` 的 evidence request,数据库开始执行结构化日期校验。
|
||||
- 根因:`20260725010000_structured_conversational_date_confirmation.sql` 同时保留了一个冗余的聚合日期正则;该正则括号不平衡,PostgreSQL 在函数运行时抛出 `2201B invalid regular expression`。下方按 `year/month/day` 精度分别校验的正则已经完整覆盖格式约束。
|
||||
- 修复:新增向前迁移 `20260725020000_repair_structured_conversational_date_validator.sql`,删除冗余聚合正则,只保留现有精度分支;同步加入生产迁移工作流。
|
||||
- 验证:生产事务回滚 dry-run 通过;以 `2020-10 + month + yes_no` 调用校验器返回 true;迁移账本、函数定义和生产健康状态均复核。
|
||||
- 防复发:数据库 JSON 合同的日期格式只维护一组精度分支;新增生产迁移时必须执行真实 PostgreSQL 函数调用,而不是只做 SQL 文本断言。
|
||||
- 相关记录:BUG-067、BUG-070
|
||||
- 修复版本:本次修复提交(生产已向前迁移)
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
begin;
|
||||
|
||||
-- Repair the structured date validator after the initial migration shipped a
|
||||
-- redundant aggregate date regex with unbalanced parentheses. The precision-
|
||||
-- specific checks below are the single source of truth.
|
||||
create or replace function public.conversational_rectification_valid_evidence_request(
|
||||
p_value jsonb
|
||||
)
|
||||
returns boolean
|
||||
language sql
|
||||
immutable
|
||||
strict
|
||||
set search_path = ''
|
||||
as $$
|
||||
select pg_catalog.jsonb_typeof(p_value) = 'object'
|
||||
and pg_catalog.octet_length(p_value::text) <= 2048
|
||||
and public.conversational_rectification_numbers_are_stable(p_value)
|
||||
and public.conversational_rectification_has_only_keys(
|
||||
p_value,
|
||||
array['domains', 'datePrecision', 'freeTextAllowed', 'prompt', 'followUp']::text[]
|
||||
)
|
||||
and p_value ?& array['domains', 'datePrecision', 'freeTextAllowed']::text[]
|
||||
and pg_catalog.jsonb_typeof(p_value -> 'domains') = 'array'
|
||||
and pg_catalog.jsonb_array_length(p_value -> 'domains') between 1 and 4
|
||||
and not exists (
|
||||
select 1
|
||||
from pg_catalog.jsonb_array_elements(p_value -> 'domains') domain
|
||||
where pg_catalog.jsonb_typeof(domain) is distinct from 'string'
|
||||
or domain #>> '{}' not in (
|
||||
'career', 'education', 'finance', 'health_pressure', 'relocation',
|
||||
'relationship', 'family', 'other'
|
||||
)
|
||||
)
|
||||
and pg_catalog.jsonb_typeof(p_value -> 'datePrecision') = 'string'
|
||||
and p_value ->> 'datePrecision' in ('month_preferred', 'year_accepted')
|
||||
and pg_catalog.jsonb_typeof(p_value -> 'freeTextAllowed') = 'boolean'
|
||||
and p_value -> 'freeTextAllowed' = 'true'::jsonb
|
||||
and (
|
||||
not (p_value ? 'prompt')
|
||||
or (
|
||||
pg_catalog.jsonb_typeof(p_value -> 'prompt') = 'string'
|
||||
and pg_catalog.char_length(pg_catalog.btrim(p_value ->> 'prompt')) between 1 and 1000
|
||||
)
|
||||
)
|
||||
and (
|
||||
not (p_value ? 'followUp')
|
||||
or (
|
||||
pg_catalog.jsonb_typeof(p_value -> 'followUp') = 'object'
|
||||
and public.conversational_rectification_has_only_keys(
|
||||
p_value -> 'followUp',
|
||||
array['kind', 'evidenceId', 'answerMode', 'proposedDate']::text[]
|
||||
)
|
||||
and (p_value -> 'followUp') ?& array['kind', 'evidenceId']::text[]
|
||||
and p_value #>> '{followUp,kind}' in ('new_event', 'event_date', 'event_detail')
|
||||
and (
|
||||
not ((p_value -> 'followUp') ? 'answerMode')
|
||||
or (
|
||||
pg_catalog.jsonb_typeof(p_value #> '{followUp,answerMode}') = 'string'
|
||||
and p_value #>> '{followUp,answerMode}' in ('free_text', 'yes_no')
|
||||
)
|
||||
)
|
||||
and (
|
||||
not ((p_value -> 'followUp') ? 'proposedDate')
|
||||
or p_value #> '{followUp,proposedDate}' = 'null'::jsonb
|
||||
or (
|
||||
pg_catalog.jsonb_typeof(p_value #> '{followUp,proposedDate}') = 'object'
|
||||
and public.conversational_rectification_has_only_keys(
|
||||
p_value #> '{followUp,proposedDate}',
|
||||
array['value', 'precision']::text[]
|
||||
)
|
||||
and (p_value #> '{followUp,proposedDate}') ?& array['value', 'precision']::text[]
|
||||
and pg_catalog.jsonb_typeof(p_value #> '{followUp,proposedDate,value}') = 'string'
|
||||
and pg_catalog.jsonb_typeof(p_value #> '{followUp,proposedDate,precision}') = 'string'
|
||||
and (
|
||||
(p_value #>> '{followUp,proposedDate,precision}' = 'year'
|
||||
and p_value #>> '{followUp,proposedDate,value}' ~ '^[0-9]{4}$')
|
||||
or (p_value #>> '{followUp,proposedDate,precision}' = 'month'
|
||||
and p_value #>> '{followUp,proposedDate,value}' ~ '^[0-9]{4}-((0[1-9])|(1[0-2]))$')
|
||||
or (p_value #>> '{followUp,proposedDate,precision}' = 'day'
|
||||
and p_value #>> '{followUp,proposedDate,value}'
|
||||
~ '^[0-9]{4}-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))$')
|
||||
)
|
||||
)
|
||||
)
|
||||
and (
|
||||
(p_value #>> '{followUp,kind}' = 'new_event'
|
||||
and p_value #>> '{followUp,evidenceId}' is null)
|
||||
or (p_value #>> '{followUp,kind}' <> 'new_event'
|
||||
and public.conversational_rectification_valid_uuid_text(
|
||||
p_value #>> '{followUp,evidenceId}'
|
||||
))
|
||||
)
|
||||
and (
|
||||
(p_value #>> '{followUp,answerMode}' = 'yes_no'
|
||||
and p_value #>> '{followUp,kind}' = 'event_date'
|
||||
and pg_catalog.jsonb_typeof(p_value #> '{followUp,proposedDate}') = 'object')
|
||||
or (p_value #>> '{followUp,answerMode}' is distinct from 'yes_no'
|
||||
and (
|
||||
not ((p_value -> 'followUp') ? 'proposedDate')
|
||||
or p_value #> '{followUp,proposedDate}' = 'null'::jsonb
|
||||
))
|
||||
)
|
||||
)
|
||||
);
|
||||
$$;
|
||||
|
||||
commit;
|
||||
@@ -44,6 +44,13 @@ const structuredDateConfirmationMigration = readFileSync(
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const repairedStructuredDateValidatorMigration = readFileSync(
|
||||
new URL(
|
||||
"../supabase/migrations/20260725020000_repair_structured_conversational_date_validator.sql",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
const productionMigrationWorkflow = readFileSync(
|
||||
new URL(
|
||||
"../../.github/workflows/apply-production-rectification-migrations.yml",
|
||||
@@ -139,9 +146,29 @@ test("durable evidence requests persist strict structured date confirmation", ()
|
||||
assert.match(structuredDateConfirmationMigration, /valid_uuid_text/);
|
||||
});
|
||||
|
||||
test("production workflow uploads and applies the structured date confirmation migration", () => {
|
||||
assert.equal(
|
||||
productionMigrationWorkflow.match(/20260725010000_structured_conversational_date_confirmation\.sql/g)?.length,
|
||||
2,
|
||||
test("repaired structured date validator relies on precision-specific date checks", () => {
|
||||
assert.doesNotMatch(
|
||||
repairedStructuredDateValidatorMigration,
|
||||
/\^\[0-9\]\{4\}\(-/,
|
||||
);
|
||||
assert.match(
|
||||
repairedStructuredDateValidatorMigration,
|
||||
/proposedDate,precision}' = 'month'/,
|
||||
);
|
||||
assert.match(
|
||||
repairedStructuredDateValidatorMigration,
|
||||
/proposedDate,precision}' = 'day'/,
|
||||
);
|
||||
});
|
||||
|
||||
test("production workflow uploads and applies the structured date confirmation migrations", () => {
|
||||
for (const migration of [
|
||||
"20260725010000_structured_conversational_date_confirmation",
|
||||
"20260725020000_repair_structured_conversational_date_validator",
|
||||
]) {
|
||||
assert.equal(
|
||||
productionMigrationWorkflow.match(new RegExp(`${migration}\\.sql`, "g"))?.length,
|
||||
2,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user