fix(migrations): eid 301~305 multi-statement → 1-statement/파일 분리 (301~316)
asyncpg 러너가 exec_driver_sql 을 prepared statement(extended protocol)로 처리해 multi-statement 를 거부(cannot insert multiple commands) → fastapi init_db crash. (001 등 초기 multi-stmt 는 postgres initdb=psql simple protocol 로 적용됐던 것 — 작성자 가정 오류.) 301~305(각 2~4 stmt)를 내용 불변으로 16개 single-statement 파일(301~316)로 분리: eid_study_weakness(table/rule2/idx)·eid_review_set_draft(동)·eid_weekly_recap(동) ·approval_requests(table/idx)·eid_schedule_views(view2). 원순서·FK 의존성 보존. 프로덕션 pkm DB 대상 트랜잭션 dry-run(ROLLBACK) 16/16 무오류 통과. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
-- 301_eid_study_weakness.sql
|
||||
-- 이드 학습 약점 스냅샷 (append-only derived-fact). eid_study_weakness 워커가 study_question_progress
|
||||
-- + study_quiz_sessions 집계로 산출(LLM 0). study_diagnosis 표면이 최신 행을 읽어 코치 발화.
|
||||
--
|
||||
-- ★ append-only 구조강제 (project_eid_persona_substrate 불변식 #8) — 2중:
|
||||
-- (1) INSERT 스탬프 누락 거부: actor·source_generated_at = NOT NULL·DEFAULT 없음
|
||||
-- → 스탬프 없는 INSERT 를 DB 가 거부. NOT NULL 은 owner 포함 모든 role 에 적용(role 독립).
|
||||
-- (2) UPDATE/DELETE 차단: CREATE RULE ... DO INSTEAD NOTHING → 행 불변(owner·superuser 독립).
|
||||
--
|
||||
-- ★ 설계 원안 'REVOKE UPDATE,DELETE' 정정(load-bearing): 단일 DB role `pkm` 이 테이블 OWNER 라
|
||||
-- REVOKE 가 무효(owner 는 GRANT/REVOKE 우회). plpgsql trigger(RAISE)는 migration 검증기가
|
||||
-- 본문의 BEGIN 키워드를 거부(_validate_sql_content)해 불가. → RULE 이 owner 독립 + 검증기 통과하는
|
||||
-- 유일한 구조 enforcement(silent no-op, 행은 구조적으로 불변). 별도 read-only role 미존재.
|
||||
--
|
||||
-- ★ '현재' 스냅샷 = 최신 created_at 행(WHERE status='active'). 상태전이 UPDATE 없음(append-only).
|
||||
-- dispute = status='disputed' + supersedes_id 로 특정 스냅샷 무효화(새 INSERT). 표면이 disputed 제외.
|
||||
--
|
||||
-- runner = exec_driver_sql(simple protocol) → multi-statement 처리(001_initial_schema 선례, 18 stmt).
|
||||
-- BEGIN/COMMIT/ROLLBACK 없음(검증기 통과). CREATE RULE 은 IF NOT EXISTS 미지원 → OR REPLACE 로 idempotent.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eid_study_weakness (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
weaknesses JSONB NOT NULL, -- [{topic_id,topic,chronic,relapsed,leech,coverage_gap,unsure,overdue,trend,tier}]
|
||||
habit_signals JSONB NOT NULL, -- {avoidance_topics,session_abandon_rate,stale_due_count,skew_topics}
|
||||
trend_label VARCHAR(20) NOT NULL, -- overall 개선|정체|악화 (코드 산출)
|
||||
sample_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
is_shallow_sample BOOLEAN NOT NULL DEFAULT false,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 'active' | 'disputed'(dispute marker)
|
||||
supersedes_id BIGINT REFERENCES eid_study_weakness(id) ON DELETE SET NULL,
|
||||
actor VARCHAR(20) NOT NULL, -- 스탬프(no default) = 'eid'
|
||||
source_generated_at TIMESTAMPTZ NOT NULL, -- 스탬프(no default) = 집계 시점
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE OR REPLACE RULE eid_study_weakness_no_update AS ON UPDATE TO eid_study_weakness DO INSTEAD NOTHING;
|
||||
CREATE OR REPLACE RULE eid_study_weakness_no_delete AS ON DELETE TO eid_study_weakness DO INSTEAD NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_eid_weakness_user_current
|
||||
ON eid_study_weakness (user_id, created_at DESC) WHERE status = 'active';
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 301_eid_study_weakness_table.sql — 301_eid_study_weakness.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eid_study_weakness (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
weaknesses JSONB NOT NULL, -- [{topic_id,topic,chronic,relapsed,leech,coverage_gap,unsure,overdue,trend,tier}]
|
||||
habit_signals JSONB NOT NULL, -- {avoidance_topics,session_abandon_rate,stale_due_count,skew_topics}
|
||||
trend_label VARCHAR(20) NOT NULL, -- overall 개선|정체|악화 (코드 산출)
|
||||
sample_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
is_shallow_sample BOOLEAN NOT NULL DEFAULT false,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 'active' | 'disputed'(dispute marker)
|
||||
supersedes_id BIGINT REFERENCES eid_study_weakness(id) ON DELETE SET NULL,
|
||||
actor VARCHAR(20) NOT NULL, -- 스탬프(no default) = 'eid'
|
||||
source_generated_at TIMESTAMPTZ NOT NULL, -- 스탬프(no default) = 집계 시점
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -1,26 +0,0 @@
|
||||
-- 302_eid_review_set_draft.sql
|
||||
-- 이드 복습세트 초안 (append-only derived-fact). 워커가 약점 스냅샷에서 권장 복습세트를 '제안'만 한다.
|
||||
-- study overlay 항목6: "복습세트를 실제 복습 큐에 편성은 자율로 못 한다 — 초안만 제시, 사용자 1클릭".
|
||||
-- 실제 편성(study_question_progress.due_at 편집)은 별도 T2 액션 — 이 draft 는 불변 제안 기록.
|
||||
--
|
||||
-- append-only 구조강제(=301 동일): actor·source_generated_at NOT NULL no-default(스탬프) + RULE(불변).
|
||||
-- 상태전이 없음 — '현재 제안' = 최신 created_at. 새 제안은 supersedes_id 로 이전 것 가리킴(새 INSERT).
|
||||
-- question_ids = ordered list[int] snapshot (study_quiz_sessions.question_ids 패턴, junction 안 씀).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eid_review_set_draft (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
study_topic_id BIGINT REFERENCES study_topics(id) ON DELETE CASCADE, -- nullable = cross-topic 세트
|
||||
question_ids JSONB NOT NULL, -- ordered list[int]
|
||||
reason VARCHAR(40) NOT NULL, -- chronic | relapse | coverage | overdue
|
||||
actor VARCHAR(20) NOT NULL, -- 스탬프 = 'eid'
|
||||
source_weakness_id BIGINT REFERENCES eid_study_weakness(id) ON DELETE SET NULL,
|
||||
source_generated_at TIMESTAMPTZ NOT NULL, -- 스탬프
|
||||
supersedes_id BIGINT REFERENCES eid_review_set_draft(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE OR REPLACE RULE eid_review_set_draft_no_update AS ON UPDATE TO eid_review_set_draft DO INSTEAD NOTHING;
|
||||
CREATE OR REPLACE RULE eid_review_set_draft_no_delete AS ON DELETE TO eid_review_set_draft DO INSTEAD NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_eid_review_draft_user ON eid_review_set_draft (user_id, created_at DESC);
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 302_eid_study_weakness_no_update.sql — 301_eid_study_weakness.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE OR REPLACE RULE eid_study_weakness_no_update AS ON UPDATE TO eid_study_weakness DO INSTEAD NOTHING;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 303_eid_study_weakness_no_delete.sql — 301_eid_study_weakness.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE OR REPLACE RULE eid_study_weakness_no_delete AS ON DELETE TO eid_study_weakness DO INSTEAD NOTHING;
|
||||
@@ -1,27 +0,0 @@
|
||||
-- 303_eid_weekly_recap.sql
|
||||
-- 이드 주간 회고 카드 (append-only derived-fact). 회고 워커(scaffold, 미배선 — W4/Phase2)가 산출.
|
||||
-- recap overlay: 'T1 write 자율 eid_weekly_recap(append-only)'. 미결 액션아이템 open/done UPDATE 는
|
||||
-- events 측(가변)이지 이 카드가 아님 — 카드 자체는 불변 스냅샷.
|
||||
-- 현재는 통합 migration 의 scaffold 테이블(dispatch enum WRITE_WEEKLY_RECAP 의 write target 예약).
|
||||
--
|
||||
-- append-only 구조강제(=301 동일): 스탬프 NOT NULL no-default + RULE(불변). '현재' = 최신 created_at.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eid_weekly_recap (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
period_start DATE NOT NULL,
|
||||
period_end DATE NOT NULL,
|
||||
recap JSONB NOT NULL, -- {activity_summary, open_action_items:[event_id], recurring_topics}
|
||||
trend_label VARCHAR(20),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 'active' | 'disputed'
|
||||
supersedes_id BIGINT REFERENCES eid_weekly_recap(id) ON DELETE SET NULL,
|
||||
actor VARCHAR(20) NOT NULL, -- 스탬프 = 'eid'
|
||||
source_generated_at TIMESTAMPTZ NOT NULL, -- 스탬프
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE OR REPLACE RULE eid_weekly_recap_no_update AS ON UPDATE TO eid_weekly_recap DO INSTEAD NOTHING;
|
||||
CREATE OR REPLACE RULE eid_weekly_recap_no_delete AS ON DELETE TO eid_weekly_recap DO INSTEAD NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_eid_recap_user_current
|
||||
ON eid_weekly_recap (user_id, created_at DESC) WHERE status = 'active';
|
||||
@@ -1,24 +0,0 @@
|
||||
-- 304_approval_requests.sql
|
||||
-- 외부 전송 승인 큐 (★ 가변 workflow queue — append-only 아님). 설계 3-4 명시 카브아웃:
|
||||
-- "approval_requests 는 status 를 pending→approved 로 바꾸는 가변 state 라 eid_* 불변 REVOKE/RULE 대상 아님".
|
||||
-- → 여기엔 RULE(append-only) 안 건다. status 전이(UPDATE) 허용.
|
||||
--
|
||||
-- ★ Phase1 현재: app/eid/tools/dispatch.py 의 request_external_approval = 즉시 거부(INSERT 0).
|
||||
-- dispatcher 워커(유일 egress 집행)는 Phase3. 이 테이블은 그때까지 scaffold(빈 상태).
|
||||
-- ★ payload 는 고정 템플릿 슬롯만(free-form 금지) — app 층이 request_type 별 화이트리스트 검증.
|
||||
-- 승인 UI 는 전송 body 전문 diff 노출. 불변 결정 원장이 필요하면 별도 append-only approval_events(Phase3).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS approval_requests (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
request_type VARCHAR(40) NOT NULL, -- 고정 템플릿 슬롯 타입(app 화이트리스트)
|
||||
payload JSONB NOT NULL, -- 고정 템플릿 슬롯만
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending | approved | rejected (전이 허용)
|
||||
requester VARCHAR(20) NOT NULL, -- 'eid'
|
||||
decided_by VARCHAR(40),
|
||||
decided_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests (status, created_at);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 304_eid_study_weakness_idx.sql — 301_eid_study_weakness.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_eid_weakness_user_current
|
||||
ON eid_study_weakness (user_id, created_at DESC) WHERE status = 'active';
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 305_eid_review_set_draft_table.sql — 302_eid_review_set_draft.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eid_review_set_draft (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
study_topic_id BIGINT REFERENCES study_topics(id) ON DELETE CASCADE, -- nullable = cross-topic 세트
|
||||
question_ids JSONB NOT NULL, -- ordered list[int]
|
||||
reason VARCHAR(40) NOT NULL, -- chronic | relapse | coverage | overdue
|
||||
actor VARCHAR(20) NOT NULL, -- 스탬프 = 'eid'
|
||||
source_weakness_id BIGINT REFERENCES eid_study_weakness(id) ON DELETE SET NULL,
|
||||
source_generated_at TIMESTAMPTZ NOT NULL, -- 스탬프
|
||||
supersedes_id BIGINT REFERENCES eid_review_set_draft(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -1,33 +0,0 @@
|
||||
-- 305_eid_schedule_views.sql
|
||||
-- 이드 일정(schedule_brief, 미래 surface) 파생뷰 2. 신규 schedule 테이블 0 — events/events_history 재활용.
|
||||
-- quadrant(중요×긴급)·D-N 정렬은 app 층(schedule overlay). 뷰는 raw 입력 필드 + today/defer 집계만.
|
||||
-- CREATE VIEW 선례 = 010_soft_delete / 283_corpus_chunks. BEGIN/COMMIT 없음.
|
||||
--
|
||||
-- v_schedule_today: 오늘(Asia/Seoul local day) 활성 일정. active 필터 = events.py:list_today reference.
|
||||
-- today 경계 = Seoul 자정→UTC 변환(date_trunc ... AT TIME ZONE 왕복). LATERAL 로 1회 계산.
|
||||
-- v_schedule_defer_pattern: events_history change_kind IN(defer,reschedule) 를 event_id 별 COUNT.
|
||||
-- '반복 미룸' 임계 3회+ (schedule overlay 판단근거 #5). reactivate 는 제외.
|
||||
|
||||
CREATE OR REPLACE VIEW v_schedule_today AS
|
||||
SELECT e.id, e.user_id, e.title, e.kind, e.status, e.priority,
|
||||
e.due_at, e.start_at, e.end_at, e.started_at, e.defer_until, e.project_tag
|
||||
FROM events e
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT (date_trunc('day', now() AT TIME ZONE 'Asia/Seoul') AT TIME ZONE 'Asia/Seoul') AS lo
|
||||
) b
|
||||
WHERE (e.status IN ('inbox','next','scheduled','in_progress')
|
||||
OR (e.status = 'deferred' AND e.defer_until IS NOT NULL AND e.defer_until <= now()))
|
||||
AND (
|
||||
(e.kind = 'task' AND e.due_at >= b.lo AND e.due_at < b.lo + interval '1 day')
|
||||
OR (e.kind = 'calendar_event' AND e.start_at >= b.lo AND e.start_at < b.lo + interval '1 day')
|
||||
OR (e.kind = 'activity_log' AND e.started_at >= b.lo AND e.started_at < b.lo + interval '1 day')
|
||||
);
|
||||
|
||||
CREATE OR REPLACE VIEW v_schedule_defer_pattern AS
|
||||
SELECT eh.event_id,
|
||||
COUNT(*)::int AS defer_reschedule_count,
|
||||
MAX(eh.changed_at) AS last_changed_at,
|
||||
(COUNT(*) >= 3) AS is_repeat_defer
|
||||
FROM events_history eh
|
||||
WHERE eh.change_kind IN ('defer','reschedule')
|
||||
GROUP BY eh.event_id;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 306_eid_review_set_draft_no_update.sql — 302_eid_review_set_draft.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE OR REPLACE RULE eid_review_set_draft_no_update AS ON UPDATE TO eid_review_set_draft DO INSTEAD NOTHING;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 307_eid_review_set_draft_no_delete.sql — 302_eid_review_set_draft.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE OR REPLACE RULE eid_review_set_draft_no_delete AS ON DELETE TO eid_review_set_draft DO INSTEAD NOTHING;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 308_eid_review_set_draft_idx.sql — 302_eid_review_set_draft.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_eid_review_draft_user ON eid_review_set_draft (user_id, created_at DESC);
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 309_eid_weekly_recap_table.sql — 303_eid_weekly_recap.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS eid_weekly_recap (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
period_start DATE NOT NULL,
|
||||
period_end DATE NOT NULL,
|
||||
recap JSONB NOT NULL, -- {activity_summary, open_action_items:[event_id], recurring_topics}
|
||||
trend_label VARCHAR(20),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 'active' | 'disputed'
|
||||
supersedes_id BIGINT REFERENCES eid_weekly_recap(id) ON DELETE SET NULL,
|
||||
actor VARCHAR(20) NOT NULL, -- 스탬프 = 'eid'
|
||||
source_generated_at TIMESTAMPTZ NOT NULL, -- 스탬프
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 310_eid_weekly_recap_no_update.sql — 303_eid_weekly_recap.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE OR REPLACE RULE eid_weekly_recap_no_update AS ON UPDATE TO eid_weekly_recap DO INSTEAD NOTHING;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 311_eid_weekly_recap_no_delete.sql — 303_eid_weekly_recap.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE OR REPLACE RULE eid_weekly_recap_no_delete AS ON DELETE TO eid_weekly_recap DO INSTEAD NOTHING;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 312_eid_weekly_recap_idx.sql — 303_eid_weekly_recap.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_eid_recap_user_current
|
||||
ON eid_weekly_recap (user_id, created_at DESC) WHERE status = 'active';
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 313_approval_requests_table.sql — 304_approval_requests.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS approval_requests (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
request_type VARCHAR(40) NOT NULL, -- 고정 템플릿 슬롯 타입(app 화이트리스트)
|
||||
payload JSONB NOT NULL, -- 고정 템플릿 슬롯만
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending | approved | rejected (전이 허용)
|
||||
requester VARCHAR(20) NOT NULL, -- 'eid'
|
||||
decided_by VARCHAR(40),
|
||||
decided_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 314_approval_requests_idx.sql — 304_approval_requests.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests (status, created_at);
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 315_eid_schedule_views_v_schedule_today.sql — 305_eid_schedule_views.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE OR REPLACE VIEW v_schedule_today AS
|
||||
SELECT e.id, e.user_id, e.title, e.kind, e.status, e.priority,
|
||||
e.due_at, e.start_at, e.end_at, e.started_at, e.defer_until, e.project_tag
|
||||
FROM events e
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT (date_trunc('day', now() AT TIME ZONE 'Asia/Seoul') AT TIME ZONE 'Asia/Seoul') AS lo
|
||||
) b
|
||||
WHERE (e.status IN ('inbox','next','scheduled','in_progress')
|
||||
OR (e.status = 'deferred' AND e.defer_until IS NOT NULL AND e.defer_until <= now()))
|
||||
AND (
|
||||
(e.kind = 'task' AND e.due_at >= b.lo AND e.due_at < b.lo + interval '1 day')
|
||||
OR (e.kind = 'calendar_event' AND e.start_at >= b.lo AND e.start_at < b.lo + interval '1 day')
|
||||
OR (e.kind = 'activity_log' AND e.started_at >= b.lo AND e.started_at < b.lo + interval '1 day')
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 316_eid_schedule_views_v_schedule_defer_pattern.sql — 305_eid_schedule_views.sql 분리본 (single-statement). asyncpg 러너가 prepared statement 로 처리해 multi-statement 거부 → 1 stmt/파일. 내용 불변.
|
||||
|
||||
CREATE OR REPLACE VIEW v_schedule_defer_pattern AS
|
||||
SELECT eh.event_id,
|
||||
COUNT(*)::int AS defer_reschedule_count,
|
||||
MAX(eh.changed_at) AS last_changed_at,
|
||||
(COUNT(*) >= 3) AS is_repeat_defer
|
||||
FROM events_history eh
|
||||
WHERE eh.change_kind IN ('defer','reschedule')
|
||||
GROUP BY eh.event_id;
|
||||
Reference in New Issue
Block a user