0ee948b34d
Also updates test_migrate_from_empty_applies_0001 → test_migrate_from_empty_applies_all to assert both migrations apply on a fresh DB (mechanical consequence of adding 0002). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
5.5 KiB
SQL
131 lines
5.5 KiB
SQL
-- 0002_products.sql — SD-0002 §6.3 data model (SLICE-5). Forward-only (INV-7):
|
|
-- never edit once merged; add a new numbered migration.
|
|
|
|
-- product — one catalog product per (storefront, handle) (INV-13/14). Option *names*
|
|
-- live here; product_type's kit values are schema-open but a service-layer rule
|
|
-- rejects non-'standalone' until #15 (same pattern as INV-4).
|
|
CREATE TABLE product (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
storefront_id BIGINT NOT NULL REFERENCES storefront (id),
|
|
handle TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
description_html TEXT,
|
|
vendor TEXT,
|
|
product_type TEXT NOT NULL DEFAULT 'standalone'
|
|
CHECK (product_type IN ('standalone', 'kit_virtual', 'kit_assembled')),
|
|
google_product_category TEXT,
|
|
tags TEXT[] NOT NULL DEFAULT '{}',
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('draft', 'active', 'archived')),
|
|
published BOOLEAN NOT NULL DEFAULT TRUE,
|
|
option1_name TEXT,
|
|
option2_name TEXT,
|
|
option3_name TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE UNIQUE INDEX product_handle_key ON product (storefront_id, handle); -- INV-13
|
|
|
|
-- variant — one purchasable form, identified by its option-value combo (INV-13).
|
|
-- SKU is indexed data, never identity. image_id FK is added after product_image.
|
|
CREATE TABLE variant (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
product_id BIGINT NOT NULL REFERENCES product (id),
|
|
position INTEGER NOT NULL,
|
|
option1_value TEXT,
|
|
option2_value TEXT,
|
|
option3_value TEXT,
|
|
sku TEXT,
|
|
barcode TEXT,
|
|
price NUMERIC,
|
|
cost NUMERIC,
|
|
weight NUMERIC,
|
|
weight_unit TEXT,
|
|
volume NUMERIC,
|
|
volume_unit TEXT,
|
|
tax_id_1 TEXT,
|
|
tax_id_2 TEXT,
|
|
inventory_tracker TEXT,
|
|
inventory_qty INTEGER,
|
|
image_id BIGINT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
-- Postgres 16 (compose + Cloud SQL pin): NULLS NOT DISTINCT makes the all-NULL
|
|
-- no-option combo unique too (INV-13).
|
|
CREATE UNIQUE INDEX variant_option_combo_key
|
|
ON variant (product_id, option1_value, option2_value, option3_value)
|
|
NULLS NOT DISTINCT;
|
|
CREATE INDEX variant_sku_idx ON variant (sku);
|
|
|
|
-- product_image — identity within a product is source_url (§6.3); bytes live in
|
|
-- object storage from SLICE-7 (keys nullable until fetched). status starts 'pending';
|
|
-- SLICE-5 stubs the fetch phase so rows simply stay pending.
|
|
CREATE TABLE product_image (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
product_id BIGINT NOT NULL REFERENCES product (id),
|
|
position INTEGER NOT NULL,
|
|
source_url TEXT NOT NULL,
|
|
alt_text TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK (status IN ('pending', 'fetched', 'rejected_low_res', 'rejected_not_image', 'failed')),
|
|
failure_reason TEXT,
|
|
key_original TEXT,
|
|
key_thumb TEXT,
|
|
key_card TEXT,
|
|
key_detail TEXT,
|
|
import_run_id BIGINT,
|
|
fetched_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE UNIQUE INDEX product_image_src_key ON product_image (product_id, source_url);
|
|
ALTER TABLE variant
|
|
ADD CONSTRAINT variant_image_fk FOREIGN KEY (image_id) REFERENCES product_image (id);
|
|
|
|
-- import_draft — the preview's server side (INV-11). file_bytes is the SLICE-5
|
|
-- interim home for the upload (objectstore key from SLICE-7). Deleted outright on
|
|
-- cancel/expiry — drafts never appear in history (PUC-3a).
|
|
CREATE TABLE import_draft (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
storefront_id BIGINT NOT NULL REFERENCES storefront (id),
|
|
account_id BIGINT NOT NULL REFERENCES account (id),
|
|
file_name TEXT NOT NULL,
|
|
dialect TEXT NOT NULL,
|
|
file_bytes BYTEA NOT NULL,
|
|
summary JSONB NOT NULL,
|
|
records JSONB NOT NULL,
|
|
fingerprint TEXT NOT NULL,
|
|
unknown_columns TEXT[] NOT NULL DEFAULT '{}',
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- import_run — the durable record of one confirmed import (PUC-8); created only at
|
|
-- confirm (PUC-4).
|
|
CREATE TABLE import_run (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
storefront_id BIGINT NOT NULL REFERENCES storefront (id),
|
|
account_id BIGINT NOT NULL REFERENCES account (id),
|
|
file_name TEXT NOT NULL,
|
|
dialect TEXT NOT NULL,
|
|
products_added INTEGER NOT NULL,
|
|
products_updated INTEGER NOT NULL,
|
|
rows_errored INTEGER NOT NULL,
|
|
status TEXT NOT NULL
|
|
CHECK (status IN ('applying', 'fetching_images', 'complete', 'complete_with_problems')),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
completed_at TIMESTAMPTZ
|
|
);
|
|
CREATE INDEX import_run_history_idx ON import_run (storefront_id, created_at DESC);
|
|
|
|
-- import_run_error — one row per rejected CSV row (PUC-5), merchant-language message.
|
|
CREATE TABLE import_run_error (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
run_id BIGINT NOT NULL REFERENCES import_run (id),
|
|
line_number INTEGER NOT NULL,
|
|
column_name TEXT,
|
|
message TEXT NOT NULL
|
|
);
|
|
|
|
ALTER TABLE product_image
|
|
ADD CONSTRAINT product_image_run_fk FOREIGN KEY (import_run_id) REFERENCES import_run (id);
|