feat(products): SD-0002 §6.3 schema — migration 0002 + nh3/multipart deps (SLICE-5)
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>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
-- 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);
|
||||
Reference in New Issue
Block a user