groceries: nightly AH/Jumbo catalogue import, unit-price normalisation, product groups and compare UI
All checks were successful
Deploy production / deploy (push) Successful in 2m48s

This commit is contained in:
2026-10-24 14:01:25 +00:00
parent 8dde5aa302
commit 427f4ed095
20 changed files with 34891 additions and 699 deletions

View File

@@ -0,0 +1,11 @@
ALTER TABLE "groceries"."prices" DROP CONSTRAINT "prices_product_id_products_id_fkey";--> statement-breakpoint
DROP TABLE "groceries"."prices";--> statement-breakpoint
DROP TABLE "groceries"."products";--> statement-breakpoint
-- The hand-entered products/prices tables were never used (0 rows); replaced by store items
-- and groups in the next migration. Unregister them from the /lists database browser.
DO $$
BEGIN
IF to_regclass('meta.tables') IS NOT NULL THEN
DELETE FROM meta.tables WHERE schema_name = 'groceries' AND table_name IN ('products','prices');
END IF;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
-- Trigram search for group/item names (trusted extension; the database owner may create it).
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
--> statement-breakpoint
CREATE TABLE "groceries"."groups" (
"id" serial PRIMARY KEY,
"key" text,
"name" text NOT NULL,
"brand" text,
"category" text,
"unit" text,
"family_key" text,
"favorite" boolean DEFAULT false NOT NULL,
"manual" boolean DEFAULT false NOT NULL,
"merged_into_id" integer,
"notes" text,
"item_count" integer DEFAULT 0 NOT NULL,
"store_count" integer DEFAULT 0 NOT NULL,
"store_ids" integer[] DEFAULT '{}'::integer[] NOT NULL,
"min_unit_price_cents" integer,
"min_price_cents" integer,
"promo_count" integer DEFAULT 0 NOT NULL,
"image_url" text,
"search_text" text DEFAULT '' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "groups_name_check" CHECK (length(btrim("name")) > 0),
CONSTRAINT "groups_unit_check" CHECK ("unit" IS NULL OR "unit" IN ('piece','kg','l')),
CONSTRAINT "groups_not_self_merged" CHECK ("merged_into_id" IS NULL OR "merged_into_id" <> "id")
);
--> statement-breakpoint
CREATE TABLE "groceries"."import_runs" (
"id" serial PRIMARY KEY,
"store_id" integer NOT NULL,
"started_at" timestamp with time zone DEFAULT now() NOT NULL,
"finished_at" timestamp with time zone,
"status" text DEFAULT 'running' NOT NULL,
"requests" integer DEFAULT 0 NOT NULL,
"fetched" integer DEFAULT 0 NOT NULL,
"inserted" integer DEFAULT 0 NOT NULL,
"price_changes" integer DEFAULT 0 NOT NULL,
"error" text
);
--> statement-breakpoint
CREATE TABLE "groceries"."item_prices" (
"id" serial PRIMARY KEY,
"item_id" integer NOT NULL,
"observed_on" date NOT NULL,
"price_cents" integer,
"promo_price_cents" integer,
"promo_label" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "groceries"."items" (
"id" serial PRIMARY KEY,
"store_id" integer NOT NULL,
"store_product_id" text NOT NULL,
"name" text NOT NULL,
"brand" text,
"size_text" text,
"unit" text,
"unit_amount" numeric(12,4),
"unit_source" text,
"price_cents" integer,
"promo_price_cents" integer,
"promo_label" text,
"unit_price_cents" integer,
"promo_unit_price_cents" integer,
"category" text,
"category_path" text,
"gtin" text,
"gtin_checked_at" timestamp with time zone,
"image_url" text,
"url" text,
"available" boolean DEFAULT true NOT NULL,
"first_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
"group_id" integer,
"match_key" text,
"match_method" text,
"match_confidence" real,
"match_locked" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "items_unit_check" CHECK ("unit" IS NULL OR "unit" IN ('piece','kg','l')),
CONSTRAINT "items_price_check" CHECK ("price_cents" IS NULL OR "price_cents" >= 0)
);
--> statement-breakpoint
CREATE UNIQUE INDEX "groups_key_key" ON "groceries"."groups" ("key");--> statement-breakpoint
CREATE INDEX "groups_family_idx" ON "groceries"."groups" ("family_key");--> statement-breakpoint
CREATE INDEX "groups_category_idx" ON "groceries"."groups" ("category");--> statement-breakpoint
CREATE INDEX "groups_store_ids_idx" ON "groceries"."groups" USING gin ("store_ids");--> statement-breakpoint
CREATE INDEX "groups_search_trgm_idx" ON "groceries"."groups" USING gin ("search_text" gin_trgm_ops);--> statement-breakpoint
CREATE INDEX "groups_list_idx" ON "groceries"."groups" ("store_count","name");--> statement-breakpoint
CREATE INDEX "import_runs_store_idx" ON "groceries"."import_runs" ("store_id","started_at");--> statement-breakpoint
CREATE UNIQUE INDEX "item_prices_item_day_key" ON "groceries"."item_prices" ("item_id","observed_on");--> statement-breakpoint
CREATE UNIQUE INDEX "items_store_product_key" ON "groceries"."items" ("store_id","store_product_id");--> statement-breakpoint
CREATE INDEX "items_group_idx" ON "groceries"."items" ("group_id");--> statement-breakpoint
CREATE INDEX "items_gtin_idx" ON "groceries"."items" ("gtin");--> statement-breakpoint
CREATE INDEX "items_name_trgm_idx" ON "groceries"."items" USING gin ("name" gin_trgm_ops);--> statement-breakpoint
ALTER TABLE "groceries"."groups" ADD CONSTRAINT "groups_merged_into_id_groups_id_fkey" FOREIGN KEY ("merged_into_id") REFERENCES "groceries"."groups"("id") ON DELETE SET NULL;--> statement-breakpoint
ALTER TABLE "groceries"."import_runs" ADD CONSTRAINT "import_runs_store_id_stores_id_fkey" FOREIGN KEY ("store_id") REFERENCES "groceries"."stores"("id") ON DELETE CASCADE;--> statement-breakpoint
ALTER TABLE "groceries"."item_prices" ADD CONSTRAINT "item_prices_item_id_items_id_fkey" FOREIGN KEY ("item_id") REFERENCES "groceries"."items"("id") ON DELETE CASCADE;--> statement-breakpoint
ALTER TABLE "groceries"."items" ADD CONSTRAINT "items_store_id_stores_id_fkey" FOREIGN KEY ("store_id") REFERENCES "groceries"."stores"("id") ON DELETE CASCADE;--> statement-breakpoint
ALTER TABLE "groceries"."items" ADD CONSTRAINT "items_group_id_groups_id_fkey" FOREIGN KEY ("group_id") REFERENCES "groceries"."groups"("id") ON DELETE SET NULL;--> statement-breakpoint
DO $$
DECLARE t text;
BEGIN
IF to_regprocedure('meta.touch_updated_at()') IS NOT NULL THEN
FOREACH t IN ARRAY ARRAY['groups','items'] LOOP
EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON groceries.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', t);
END LOOP;
END IF;
END $$;
--> statement-breakpoint
-- Private: app role only (default privileges from the first groceries migration also apply).
DO $$
BEGIN
IF current_user = 'personal' AND EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_app') THEN
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA groceries TO personal_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA groceries TO personal_app;
END IF;
END $$;
--> statement-breakpoint
DO $$
BEGIN
IF to_regclass('meta.tables') IS NOT NULL THEN
INSERT INTO meta.tables (schema_name, table_name, slug, display_name, kind, editable, has_done, sort_order)
VALUES ('groceries','groups','groceries-groups','Grocery groups','core',true,false,1100),
('groceries','items','groceries-items','Grocery store items','core',true,false,1100),
('groceries','item_prices','groceries-item-prices','Grocery price history','core',false,false,1100),
('groceries','import_runs','groceries-import-runs','Grocery import runs','core',false,false,1100)
ON CONFLICT DO NOTHING;
IF has_function_privilege('meta.sync(integer)', 'EXECUTE') THEN
PERFORM meta.sync();
END IF;
END IF;
END $$;

File diff suppressed because it is too large Load Diff