-- 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 $$;