From e2c358c90a8f05e346e2e04eefb3064f81b0925a Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sat, 25 Apr 2026 08:46:44 -0700 Subject: [PATCH] fix: extend source CHECK constraints to include visual_capture (kiwi#79) Migrations 037 and 038 rebuild products and inventory_items tables to add 'visual_capture' as a valid source value, which the label-confirm endpoint sets when saving user-verified nutrition label data. Adds 2 schema tests covering the new allowed value. --- .../037_products_source_visual_capture.sql | 34 +++++++++++++++ ..._inventory_items_source_visual_capture.sql | 43 +++++++++++++++++++ tests/db/test_captured_products.py | 36 ++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 app/db/migrations/037_products_source_visual_capture.sql create mode 100644 app/db/migrations/038_inventory_items_source_visual_capture.sql diff --git a/app/db/migrations/037_products_source_visual_capture.sql b/app/db/migrations/037_products_source_visual_capture.sql new file mode 100644 index 0000000..3d6df0f --- /dev/null +++ b/app/db/migrations/037_products_source_visual_capture.sql @@ -0,0 +1,34 @@ +-- Migration 037: add 'visual_capture' to products.source CHECK constraint +-- SQLite cannot ALTER a CHECK constraint, so we rebuild the table. + +PRAGMA foreign_keys = OFF; + +BEGIN; + +CREATE TABLE products_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + barcode TEXT UNIQUE, + name TEXT NOT NULL, + brand TEXT, + category TEXT, + description TEXT, + image_url TEXT, + nutrition_data TEXT NOT NULL DEFAULT '{}', + source TEXT NOT NULL DEFAULT 'openfoodfacts' + CHECK (source IN ('openfoodfacts', 'manual', 'receipt_ocr', 'visual_capture')), + source_data TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +INSERT INTO products_new + SELECT id, barcode, name, brand, category, description, image_url, + nutrition_data, source, source_data, created_at, updated_at + FROM products; + +DROP TABLE products; +ALTER TABLE products_new RENAME TO products; + +COMMIT; + +PRAGMA foreign_keys = ON; diff --git a/app/db/migrations/038_inventory_items_source_visual_capture.sql b/app/db/migrations/038_inventory_items_source_visual_capture.sql new file mode 100644 index 0000000..870fcbd --- /dev/null +++ b/app/db/migrations/038_inventory_items_source_visual_capture.sql @@ -0,0 +1,43 @@ +-- Migration 038: add 'visual_capture' to inventory_items.source CHECK constraint +-- SQLite cannot ALTER a CHECK constraint, so we rebuild the table. + +PRAGMA foreign_keys = OFF; + +BEGIN; + +CREATE TABLE inventory_items_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER NOT NULL + REFERENCES products (id) ON DELETE RESTRICT, + receipt_id INTEGER + REFERENCES receipts (id) ON DELETE SET NULL, + quantity REAL NOT NULL DEFAULT 1 CHECK (quantity > 0), + unit TEXT NOT NULL DEFAULT 'count', + location TEXT NOT NULL, + sublocation TEXT, + purchase_date TEXT, + expiration_date TEXT, + status TEXT NOT NULL DEFAULT 'available' + CHECK (status IN ('available', 'consumed', 'expired', 'discarded')), + consumed_at TEXT, + notes TEXT, + source TEXT NOT NULL DEFAULT 'manual' + CHECK (source IN ('barcode_scan', 'manual', 'receipt', 'visual_capture')), + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + opened_date TEXT, + disposal_reason TEXT +); + +INSERT INTO inventory_items_new + SELECT id, product_id, receipt_id, quantity, unit, location, sublocation, + purchase_date, expiration_date, status, consumed_at, notes, source, + created_at, updated_at, opened_date, disposal_reason + FROM inventory_items; + +DROP TABLE inventory_items; +ALTER TABLE inventory_items_new RENAME TO inventory_items; + +COMMIT; + +PRAGMA foreign_keys = ON; diff --git a/tests/db/test_captured_products.py b/tests/db/test_captured_products.py index 28942fb..4529e2f 100644 --- a/tests/db/test_captured_products.py +++ b/tests/db/test_captured_products.py @@ -11,6 +11,42 @@ def store(tmp_path: Path) -> Store: s.close() +class TestProductsSchema: + """Verify migrations 037/038 allow 'visual_capture' as a valid source.""" + + def test_products_accepts_visual_capture_source(self, store): + """Migration 037: products.source CHECK includes 'visual_capture'.""" + store.conn.execute( + "INSERT INTO products (name, source) VALUES (?, ?)", + ("Test Product", "visual_capture"), + ) + store.conn.commit() + row = store.conn.execute( + "SELECT source FROM products WHERE name='Test Product'" + ).fetchone() + assert row[0] == "visual_capture" + + def test_inventory_items_accepts_visual_capture_source(self, store): + """Migration 038: inventory_items.source CHECK includes 'visual_capture'.""" + store.conn.execute( + "INSERT INTO products (name, source) VALUES (?, ?)", + ("Temp Product", "manual"), + ) + store.conn.commit() + product_id = store.conn.execute( + "SELECT id FROM products WHERE name='Temp Product'" + ).fetchone()[0] + store.conn.execute( + "INSERT INTO inventory_items (product_id, location, source) VALUES (?, ?, ?)", + (product_id, "pantry", "visual_capture"), + ) + store.conn.commit() + row = store.conn.execute( + "SELECT source FROM inventory_items WHERE product_id=?", (product_id,) + ).fetchone() + assert row[0] == "visual_capture" + + class TestMigration: def test_captured_products_table_exists(self, store): cur = store.conn.execute(