fix: deliver validated AI insight responses

This commit is contained in:
2026-08-22 15:47:38 +08:00
parent ca55c2fb3d
commit 3fba4261ee
68 changed files with 6870 additions and 493 deletions

View File

@@ -0,0 +1,192 @@
ALTER TABLE ai_insight_snapshots ADD COLUMN IF NOT EXISTS deterministic_copy jsonb;
-- statement-break
ALTER TABLE ai_insight_snapshots ADD COLUMN IF NOT EXISTS provider_request_id text;
-- statement-break
UPDATE ai_insight_snapshots SET deterministic_copy=ai_response WHERE deterministic_copy IS NULL;
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_profiles (
id text PRIMARY KEY,
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
terminal_source_key text NOT NULL,
version integer NOT NULL CHECK (version > 0),
is_current boolean NOT NULL DEFAULT true,
business_description text NOT NULL CHECK (char_length(business_description) BETWEEN 20 AND 2000),
niches jsonb NOT NULL,
sales_channels jsonb NOT NULL,
primary_goal text NOT NULL CHECK (char_length(primary_goal) BETWEEN 3 AND 500),
constraints_text text NOT NULL DEFAULT '' CHECK (char_length(constraints_text) <= 1000),
difficulty smallint NOT NULL DEFAULT 2 CHECK (difficulty BETWEEN 1 AND 5),
actor_user_id bigint NOT NULL REFERENCES merchant_users(id),
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (merchant_id,terminal_id,version)
);
-- statement-break
CREATE UNIQUE INDEX IF NOT EXISTS growth_coach_profile_current_idx ON growth_coach_profiles(merchant_id,terminal_id) WHERE is_current;
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_plans (
id text PRIMARY KEY,
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
terminal_source_key text NOT NULL,
profile_id text NOT NULL REFERENCES growth_coach_profiles(id),
parent_id text REFERENCES growth_coach_plans(id),
cadence text NOT NULL CHECK (cadence IN ('DAILY','WEEKLY','MONTHLY')),
status text NOT NULL CHECK (status IN ('GENERATING','READY','PARTIAL','FAILED','CLOSED')),
period_start date NOT NULL,
period_end date NOT NULL,
daily_brief text,
difficulty smallint NOT NULL CHECK (difficulty BETWEEN 1 AND 5),
prompt_version text NOT NULL,
schema_version text NOT NULL,
model text NOT NULL,
provider_request_id text,
input_tokens bigint NOT NULL DEFAULT 0,
output_tokens bigint NOT NULL DEFAULT 0,
latency_ms bigint NOT NULL DEFAULT 0,
provider_requests bigint NOT NULL DEFAULT 0,
estimated_cost text,
failure_code text,
created_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
CHECK (period_end >= period_start)
);
-- statement-break
CREATE INDEX IF NOT EXISTS growth_coach_plans_scope_idx ON growth_coach_plans(merchant_id,terminal_id,cadence,period_start DESC);
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_recommendations (
id text PRIMARY KEY,
plan_id text NOT NULL REFERENCES growth_coach_plans(id) ON DELETE CASCADE,
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
rank smallint NOT NULL CHECK (rank BETWEEN 1 AND 5),
status text NOT NULL CHECK (status IN ('OFFERED','ACCEPTED','REJECTED','EXPIRED')),
action_type text NOT NULL CHECK (action_type IN ('MANUAL','METRIC')),
title text NOT NULL,
rationale text NOT NULL,
expected_impact text NOT NULL,
effort text NOT NULL CHECK (effort IN ('LOW','MEDIUM','HIGH')),
evidence_ids jsonb NOT NULL,
metric_id text,
metric_direction text CHECK (metric_direction IN ('INCREASE','DECREASE')),
action_level text CHECK (action_level IN ('SMALL','MEDIUM','STRETCH')),
baseline numeric(30,6),
target numeric(30,6),
sample_size bigint,
deadline date NOT NULL,
decision_reason text,
decision_note text,
decided_by bigint REFERENCES merchant_users(id),
decided_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(plan_id,rank)
);
-- statement-break
CREATE TABLE IF NOT EXISTS golden_steps (
id text PRIMARY KEY,
recommendation_id text NOT NULL UNIQUE REFERENCES growth_coach_recommendations(id),
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
terminal_source_key text NOT NULL,
action_type text NOT NULL CHECK (action_type IN ('MANUAL','METRIC')),
title text NOT NULL,
status text NOT NULL CHECK (status IN ('NOT_STARTED','IN_PROGRESS','COMPLETED','ABANDONED','MISSED')),
metric_id text,
metric_direction text,
baseline numeric(30,6),
target numeric(30,6),
current_value numeric(30,6),
sample_size bigint,
deadline date NOT NULL,
started_at timestamptz,
completed_at timestamptz,
actor_user_id bigint NOT NULL REFERENCES merchant_users(id),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- statement-break
CREATE INDEX IF NOT EXISTS golden_steps_scope_idx ON golden_steps(merchant_id,terminal_id,status,deadline);
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_jobs (
id text PRIMARY KEY,
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
terminal_source_key text NOT NULL,
profile_id text NOT NULL REFERENCES growth_coach_profiles(id),
cadence text NOT NULL CHECK (cadence IN ('DAILY','WEEKLY','MONTHLY')),
trigger_reason text NOT NULL,
period_start date NOT NULL,
period_end date NOT NULL,
fingerprint text NOT NULL UNIQUE,
idempotency_key text,
request_hash text,
status text NOT NULL CHECK (status IN ('QUEUED','RUNNING','COMPLETED','FAILED')),
attempts integer NOT NULL DEFAULT 0,
scheduled_at timestamptz NOT NULL DEFAULT now(),
lease_token text,
lease_expires_at timestamptz,
failure_code text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
UNIQUE(merchant_id,idempotency_key)
);
-- statement-break
CREATE INDEX IF NOT EXISTS growth_coach_jobs_pending_idx ON growth_coach_jobs(scheduled_at,created_at) WHERE status IN ('QUEUED','RUNNING');
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_events (
id bigserial PRIMARY KEY,
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
actor_user_id bigint REFERENCES merchant_users(id),
entity_type text NOT NULL,
entity_id text NOT NULL,
event_type text NOT NULL,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
occurred_at timestamptz NOT NULL DEFAULT now()
);
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_progress_snapshots (
id bigserial PRIMARY KEY,
action_id text NOT NULL REFERENCES golden_steps(id) ON DELETE CASCADE,
watermark text NOT NULL,
value numeric(30,6) NOT NULL,
sample_size bigint NOT NULL,
evaluated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(action_id,watermark)
);
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_difficulty_proposals (
id text PRIMARY KEY,
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
from_level smallint NOT NULL CHECK (from_level BETWEEN 1 AND 5),
to_level smallint NOT NULL CHECK (to_level BETWEEN 1 AND 5),
reason text NOT NULL,
status text NOT NULL CHECK (status IN ('OFFERED','ACCEPTED','REJECTED','EXPIRED')),
decided_by bigint REFERENCES merchant_users(id),
decided_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_outcome_reviews (
id text PRIMARY KEY,
plan_id text NOT NULL UNIQUE REFERENCES growth_coach_plans(id),
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
summary text NOT NULL,
completed_on_time integer NOT NULL DEFAULT 0,
missed integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
-- statement-break
CREATE TABLE IF NOT EXISTS growth_coach_idempotency (
merchant_id bigint NOT NULL REFERENCES merchants(id),
idempotency_key text NOT NULL,
request_hash text NOT NULL,
response jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY(merchant_id,idempotency_key)
);
-- statement-break
INSERT INTO schema_migrations(version) VALUES (5) ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,19 @@
BEGIN;
UPDATE golden_steps
SET status = 'IN_PROGRESS',
completed_at = NULL,
updated_at = now()
WHERE action_type = 'METRIC'
AND status = 'COMPLETED'
AND (
current_value IS NULL
OR target IS NULL
OR metric_direction IS NULL
OR (metric_direction = 'INCREASE' AND current_value < target)
OR (metric_direction = 'DECREASE' AND current_value > target)
);
INSERT INTO schema_migrations(version) VALUES (6);
COMMIT;

View File

@@ -0,0 +1,20 @@
BEGIN;
UPDATE golden_steps AS action
SET status = 'IN_PROGRESS',
completed_at = NULL,
updated_at = now()
WHERE action.action_type = 'MANUAL'
AND action.status = 'COMPLETED'
AND NOT EXISTS (
SELECT 1
FROM growth_coach_events AS event
WHERE event.entity_type = 'ACTION'
AND event.entity_id = action.id
AND event.event_type = 'action.transitioned'
AND event.payload ->> 'status' = 'COMPLETED'
);
INSERT INTO schema_migrations(version) VALUES (7);
COMMIT;

View File

@@ -0,0 +1,154 @@
BEGIN;
ALTER TABLE ai_insight_snapshots
ADD COLUMN IF NOT EXISTS section_id text NOT NULL DEFAULT 'full';
ALTER TABLE ai_insight_jobs
ADD COLUMN IF NOT EXISTS section_id text NOT NULL DEFAULT 'full';
ALTER TABLE growth_coach_profiles
ADD COLUMN IF NOT EXISTS changed_fields jsonb NOT NULL DEFAULT '[]'::jsonb;
CREATE TABLE IF NOT EXISTS ai_generation_holds (
terminal_id bigint PRIMARY KEY REFERENCES terminals(id) ON DELETE CASCADE,
merchant_id bigint NOT NULL REFERENCES merchants(id),
reason text NOT NULL CHECK (reason IN ('ONBOARDING_REQUIRED')),
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE ai_insight_jobs DROP CONSTRAINT IF EXISTS ai_insight_jobs_trigger_reason_check;
ALTER TABLE ai_insight_jobs ADD CONSTRAINT ai_insight_jobs_trigger_reason_check
CHECK (trigger_reason IN ('BACKFILL','NIGHTLY','IMPORT','CALENDAR','ANOMALY','MANUAL','PROFILE'));
ALTER TABLE ai_insight_snapshots DROP CONSTRAINT IF EXISTS ai_insight_snapshots_section_id_check;
ALTER TABLE ai_insight_snapshots ADD CONSTRAINT ai_insight_snapshots_section_id_check
CHECK (section_id IN ('full','past','latest-observed','future-7-days','future-30-days','future-90-days','findings','options'));
ALTER TABLE ai_insight_jobs DROP CONSTRAINT IF EXISTS ai_insight_jobs_section_id_check;
ALTER TABLE ai_insight_jobs ADD CONSTRAINT ai_insight_jobs_section_id_check
CHECK (section_id IN ('full','past','latest-observed','future-7-days','future-30-days','future-90-days','findings','options'));
DROP INDEX IF EXISTS ai_insight_manual_refresh_idx;
CREATE TABLE IF NOT EXISTS ai_insight_refresh_attempts (
id bigserial PRIMARY KEY,
job_id text NOT NULL REFERENCES ai_insight_jobs(id) ON DELETE CASCADE,
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_source_key text,
module text NOT NULL,
section_id text NOT NULL CHECK (section_id IN ('past','latest-observed','future-7-days','future-30-days','future-90-days','findings','options')),
tehran_refresh_day date NOT NULL,
accepted_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(job_id,section_id)
);
CREATE INDEX IF NOT EXISTS ai_insight_refresh_scope_idx
ON ai_insight_refresh_attempts(merchant_id,coalesce(terminal_source_key,''),module,section_id,tehran_refresh_day,accepted_at DESC);
ALTER TABLE growth_coach_recommendations
ADD COLUMN IF NOT EXISTS subtasks jsonb NOT NULL DEFAULT '[]'::jsonb;
ALTER TABLE growth_coach_recommendations
ADD COLUMN IF NOT EXISTS event_ids jsonb NOT NULL DEFAULT '[]'::jsonb;
CREATE TABLE IF NOT EXISTS monthly_goals (
id text PRIMARY KEY,
recommendation_id text NOT NULL UNIQUE REFERENCES growth_coach_recommendations(id),
merchant_id bigint NOT NULL REFERENCES merchants(id),
terminal_id bigint NOT NULL REFERENCES terminals(id),
terminal_source_key text NOT NULL,
profile_id text NOT NULL REFERENCES growth_coach_profiles(id),
title text NOT NULL,
expected_impact text NOT NULL,
status text NOT NULL CHECK (status IN ('ACTIVE','COMPLETED','MISSED','ABANDONED')),
metric_id text NOT NULL,
metric_direction text NOT NULL CHECK (metric_direction IN ('INCREASE','DECREASE')),
baseline numeric(30,6) NOT NULL,
target numeric(30,6) NOT NULL,
current_value numeric(30,6) NOT NULL,
sample_size bigint NOT NULL,
baseline_watermark text,
latest_watermark text,
progress_bps integer NOT NULL DEFAULT 0 CHECK (progress_bps BETWEEN 0 AND 10000),
event_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
deadline date NOT NULL,
actor_user_id bigint NOT NULL REFERENCES merchant_users(id),
accepted_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (target <> baseline)
);
CREATE INDEX IF NOT EXISTS monthly_goals_scope_idx
ON monthly_goals(merchant_id,terminal_id,status,deadline);
CREATE TABLE IF NOT EXISTS goal_subtasks (
id text PRIMARY KEY,
goal_id text NOT NULL REFERENCES monthly_goals(id) ON DELETE CASCADE,
merchant_id bigint NOT NULL REFERENCES merchants(id),
position smallint NOT NULL CHECK (position BETWEEN 1 AND 7),
title text NOT NULL,
status text NOT NULL CHECK (status IN ('TODO','DONE')),
deadline date NOT NULL,
event_id text,
completed_by bigint REFERENCES merchant_users(id),
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(goal_id,position)
);
CREATE INDEX IF NOT EXISTS goal_subtasks_goal_idx ON goal_subtasks(goal_id,position);
CREATE TABLE IF NOT EXISTS goal_deadline_proposals (
id text PRIMARY KEY,
goal_id text NOT NULL REFERENCES monthly_goals(id) ON DELETE CASCADE,
subtask_id text NOT NULL REFERENCES goal_subtasks(id) ON DELETE CASCADE,
merchant_id bigint NOT NULL REFERENCES merchants(id),
event_id text NOT NULL,
current_deadline date NOT NULL,
proposed_deadline date NOT NULL,
reason text NOT NULL,
status text NOT NULL CHECK (status IN ('OFFERED','ACCEPTED','REJECTED','EXPIRED')),
decided_by bigint REFERENCES merchant_users(id),
decided_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS goal_deadline_proposals_open_idx
ON goal_deadline_proposals(subtask_id,proposed_deadline) WHERE status='OFFERED';
-- Preserve non-reset merchants' accepted work as legacy goals without inventing subtasks.
INSERT INTO monthly_goals(id,recommendation_id,merchant_id,terminal_id,terminal_source_key,profile_id,title,expected_impact,status,metric_id,metric_direction,baseline,target,current_value,sample_size,baseline_watermark,deadline,actor_user_id,accepted_at,completed_at,updated_at)
SELECT a.id,a.recommendation_id,a.merchant_id,a.terminal_id,a.terminal_source_key,p.profile_id,a.title,r.expected_impact,
CASE a.status WHEN 'COMPLETED' THEN 'COMPLETED' WHEN 'MISSED' THEN 'MISSED' WHEN 'ABANDONED' THEN 'ABANDONED' ELSE 'ACTIVE' END,
a.metric_id,a.metric_direction,a.baseline,a.target,coalesce(a.current_value,a.baseline),coalesce(a.sample_size,0),
coalesce((SELECT source_sha256 FROM import_runs WHERE status='COMPLETED' ORDER BY completed_at DESC LIMIT 1),'no-data'),
a.deadline,a.actor_user_id,a.created_at,a.completed_at,a.updated_at
FROM golden_steps a
JOIN growth_coach_recommendations r ON r.id=a.recommendation_id
JOIN growth_coach_plans p ON p.id=r.plan_id
WHERE a.action_type='METRIC' AND a.metric_id IS NOT NULL AND a.metric_direction IS NOT NULL
AND a.baseline IS NOT NULL AND a.target IS NOT NULL AND a.target<>a.baseline
ON CONFLICT DO NOTHING;
-- Repair impossible completions in both the compatibility table and the new goal table.
UPDATE golden_steps
SET status='IN_PROGRESS',completed_at=NULL,updated_at=now()
WHERE status='COMPLETED' AND (
baseline IS NULL OR target IS NULL OR target=baseline OR current_value IS NULL OR
NOT EXISTS(SELECT 1 FROM growth_coach_progress_snapshots p WHERE p.action_id=golden_steps.id) OR
(metric_direction='INCREASE' AND current_value<target) OR
(metric_direction='DECREASE' AND current_value>target)
);
-- Treat the currently imported dataset as the baseline evidence for repaired or
-- pre-existing compatibility actions. A later watermark is required to evaluate.
INSERT INTO growth_coach_progress_snapshots(action_id,watermark,value,sample_size)
SELECT a.id,
coalesce((SELECT source_sha256 FROM import_runs WHERE status='COMPLETED' ORDER BY completed_at DESC LIMIT 1),'no-data'),
coalesce(a.current_value,a.baseline),coalesce(a.sample_size,0)
FROM golden_steps a
WHERE a.action_type='METRIC' AND a.status='IN_PROGRESS' AND a.baseline IS NOT NULL AND a.target IS NOT NULL AND a.target<>a.baseline
ON CONFLICT DO NOTHING;
UPDATE monthly_goals
SET status='ACTIVE',completed_at=NULL,progress_bps=0,updated_at=now()
WHERE status='COMPLETED' AND (
baseline_watermark IS NULL OR latest_watermark IS NULL OR latest_watermark=baseline_watermark OR
(metric_direction='INCREASE' AND current_value<target) OR
(metric_direction='DECREASE' AND current_value>target)
);
INSERT INTO schema_migrations(version) VALUES (8) ON CONFLICT DO NOTHING;
COMMIT;

View File

@@ -0,0 +1,27 @@
ALTER TABLE calendar_events
ADD COLUMN IF NOT EXISTS occasion_key text,
ADD COLUMN IF NOT EXISTS event_variant text NOT NULL DEFAULT 'PRIMARY',
ADD COLUMN IF NOT EXISTS aliases jsonb NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS themes jsonb NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS campaign_tags jsonb NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS importance_bps integer;
-- statement-break
UPDATE calendar_events
SET occasion_key = coalesce(occasion_key,event_key),
importance_bps = coalesce(importance_bps,0)
WHERE occasion_key IS NULL OR importance_bps IS NULL;
-- statement-break
ALTER TABLE calendar_events
ALTER COLUMN occasion_key SET NOT NULL,
ALTER COLUMN importance_bps SET DEFAULT 0,
ALTER COLUMN importance_bps SET NOT NULL,
ADD CONSTRAINT calendar_events_importance_bps_check CHECK (importance_bps BETWEEN 0 AND 10000),
ADD CONSTRAINT calendar_events_variant_check CHECK (event_variant IN ('PRIMARY','CAMPAIGN_WINDOW','OPERATIONAL_WINDOW'));
-- statement-break
CREATE UNIQUE INDEX IF NOT EXISTS calendar_events_canonical_occurrence_idx
ON calendar_events (catalog_version_id, occasion_key, event_variant, start_date, end_date);
-- statement-break
CREATE INDEX IF NOT EXISTS calendar_events_ranked_context_idx
ON calendar_events (catalog_version_id, importance_bps DESC, relevance_bps DESC, start_date);
-- statement-break
INSERT INTO schema_migrations(version) VALUES (9) ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,23 @@
BEGIN;
ALTER TABLE ai_insight_snapshots ADD COLUMN IF NOT EXISTS period_id text;
ALTER TABLE ai_insight_snapshots ADD COLUMN IF NOT EXISTS period_type text;
ALTER TABLE ai_insight_snapshots ADD COLUMN IF NOT EXISTS period_start_at timestamptz;
ALTER TABLE ai_insight_snapshots ADD COLUMN IF NOT EXISTS period_end_at timestamptz;
ALTER TABLE ai_insight_snapshots ADD COLUMN IF NOT EXISTS period_label_fa text;
ALTER TABLE ai_insight_refresh_attempts ADD COLUMN IF NOT EXISTS period_id text;
ALTER TABLE ai_insight_snapshots DROP CONSTRAINT IF EXISTS ai_insight_snapshots_period_scope_check;
ALTER TABLE ai_insight_snapshots ADD CONSTRAINT ai_insight_snapshots_period_scope_check CHECK (
(period_id IS NULL AND period_type IS NULL AND period_start_at IS NULL AND period_end_at IS NULL AND period_label_fa IS NULL)
OR
(period_id IS NOT NULL AND period_type IN ('DAY','WEEK','JALALI_MONTH','PERSIAN_SEASON') AND period_start_at IS NOT NULL AND period_end_at IS NOT NULL AND period_label_fa IS NOT NULL AND period_end_at > period_start_at)
);
CREATE INDEX IF NOT EXISTS ai_insight_latest_period_scope_idx
ON ai_insight_snapshots (merchant_id,terminal_source_key,module,period_id,revision DESC,created_at DESC);
CREATE INDEX IF NOT EXISTS ai_insight_refresh_period_scope_idx
ON ai_insight_refresh_attempts (merchant_id,coalesce(terminal_source_key,''),module,coalesce(period_id,''),section_id,tehran_refresh_day,accepted_at DESC);
INSERT INTO schema_migrations(version) VALUES (10) ON CONFLICT DO NOTHING;
COMMIT;