mirror of
https://github.com/grocy/grocy.git
synced 2026-03-28 07:39:25 +01:00
Changes uniqueness constraint from global unique name to composite unique (name, parent_location_id). This allows e.g. "Shelf 1" in Fridge and in Cupboard. Uses partial unique indexes to handle NULL parent correctly.
147 lines
4.5 KiB
SQL
147 lines
4.5 KiB
SQL
-- Add parent_location_id column and change uniqueness constraint for hierarchical locations
|
|
-- Allows same name under different parents (e.g., "Shelf 1" in Fridge and "Shelf 1" in Cupboard)
|
|
|
|
PRAGMA legacy_alter_table = ON;
|
|
|
|
-- Rename old table
|
|
ALTER TABLE locations RENAME TO locations_old;
|
|
|
|
-- Create new table with parent_location_id and without UNIQUE constraint on name
|
|
CREATE TABLE locations (
|
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
|
|
is_freezer TINYINT NOT NULL DEFAULT 0,
|
|
active TINYINT NOT NULL DEFAULT 1 CHECK(active IN (0, 1)),
|
|
parent_location_id INTEGER
|
|
);
|
|
|
|
-- Copy data
|
|
INSERT INTO locations (id, name, description, row_created_timestamp, is_freezer, active)
|
|
SELECT id, name, description, row_created_timestamp, is_freezer, active
|
|
FROM locations_old;
|
|
|
|
-- Drop old table
|
|
DROP TABLE locations_old;
|
|
|
|
-- Create partial unique indexes for composite uniqueness
|
|
-- Ensures name is unique within each parent (including NULL as a distinct parent)
|
|
CREATE UNIQUE INDEX ix_locations_name_parent ON locations(name, parent_location_id)
|
|
WHERE parent_location_id IS NOT NULL;
|
|
|
|
CREATE UNIQUE INDEX ix_locations_name_root ON locations(name)
|
|
WHERE parent_location_id IS NULL;
|
|
|
|
-- Create recursive view for resolving location hierarchy (ancestor-descendant pairs)
|
|
-- Used for finding all descendants of a location
|
|
CREATE VIEW locations_resolved
|
|
AS
|
|
WITH RECURSIVE location_hierarchy(location_id, ancestor_location_id, level)
|
|
AS (
|
|
-- Base case: all locations map to themselves at level 0
|
|
SELECT id, id, 0
|
|
FROM locations
|
|
|
|
UNION ALL
|
|
|
|
-- Recursive case: find ancestors by following parent_location_id chain
|
|
SELECT lh.location_id, l.parent_location_id, lh.level + 1
|
|
FROM location_hierarchy lh
|
|
JOIN locations l ON lh.ancestor_location_id = l.id
|
|
WHERE l.parent_location_id IS NOT NULL
|
|
LIMIT 100 -- Safety limit to prevent infinite loops
|
|
)
|
|
SELECT
|
|
location_id AS id,
|
|
location_id,
|
|
ancestor_location_id,
|
|
level
|
|
FROM location_hierarchy;
|
|
|
|
-- Create view for location hierarchy display with computed path and depth
|
|
CREATE VIEW locations_hierarchy
|
|
AS
|
|
WITH RECURSIVE location_tree(id, name, description, parent_location_id, is_freezer, active, row_created_timestamp, path, depth)
|
|
AS (
|
|
-- Base case: root locations (no parent)
|
|
SELECT id, name, description, parent_location_id, is_freezer, active, row_created_timestamp, name, 0
|
|
FROM locations
|
|
WHERE parent_location_id IS NULL
|
|
|
|
UNION ALL
|
|
|
|
-- Recursive case: child locations
|
|
SELECT l.id, l.name, l.description, l.parent_location_id, l.is_freezer, l.active, l.row_created_timestamp,
|
|
lt.path || ' > ' || l.name,
|
|
lt.depth + 1
|
|
FROM locations l
|
|
JOIN location_tree lt ON l.parent_location_id = lt.id
|
|
LIMIT 100 -- Safety limit
|
|
)
|
|
SELECT
|
|
id,
|
|
name,
|
|
description,
|
|
parent_location_id,
|
|
is_freezer,
|
|
active,
|
|
row_created_timestamp,
|
|
path AS location_path,
|
|
depth AS location_depth
|
|
FROM location_tree;
|
|
|
|
-- Trigger to enforce NULL handling for empty parent_location_id
|
|
CREATE TRIGGER enforce_parent_location_id_null_when_empty_INS AFTER INSERT ON locations
|
|
BEGIN
|
|
UPDATE locations
|
|
SET parent_location_id = NULL
|
|
WHERE id = NEW.id
|
|
AND IFNULL(parent_location_id, '') = '';
|
|
END;
|
|
|
|
CREATE TRIGGER enforce_parent_location_id_null_when_empty_UPD AFTER UPDATE ON locations
|
|
BEGIN
|
|
UPDATE locations
|
|
SET parent_location_id = NULL
|
|
WHERE id = NEW.id
|
|
AND IFNULL(parent_location_id, '') = '';
|
|
END;
|
|
|
|
-- Trigger to prevent setting self as parent
|
|
CREATE TRIGGER prevent_self_parent_location_INS BEFORE INSERT ON locations
|
|
BEGIN
|
|
SELECT CASE WHEN((
|
|
SELECT 1
|
|
WHERE NEW.parent_location_id IS NOT NULL
|
|
AND NEW.parent_location_id = NEW.id
|
|
) NOTNULL) THEN RAISE(ABORT, 'A location cannot be its own parent') END;
|
|
END;
|
|
|
|
CREATE TRIGGER prevent_self_parent_location_UPD BEFORE UPDATE ON locations
|
|
BEGIN
|
|
SELECT CASE WHEN((
|
|
SELECT 1
|
|
WHERE NEW.parent_location_id IS NOT NULL
|
|
AND NEW.parent_location_id = NEW.id
|
|
) NOTNULL) THEN RAISE(ABORT, 'A location cannot be its own parent') END;
|
|
END;
|
|
|
|
-- Trigger to prevent circular references in location hierarchy
|
|
CREATE TRIGGER prevent_circular_location_hierarchy_UPD BEFORE UPDATE ON locations
|
|
WHEN NEW.parent_location_id IS NOT NULL
|
|
BEGIN
|
|
SELECT CASE WHEN((
|
|
WITH RECURSIVE descendants(id) AS (
|
|
SELECT NEW.id
|
|
UNION ALL
|
|
SELECT l.id
|
|
FROM locations l
|
|
JOIN descendants d ON l.parent_location_id = d.id
|
|
WHERE l.id != NEW.id
|
|
LIMIT 100
|
|
)
|
|
SELECT 1 FROM descendants WHERE id = NEW.parent_location_id
|
|
) NOTNULL) THEN RAISE(ABORT, 'Circular location hierarchy detected') END;
|
|
END;
|