diff --git a/backend/internal/data/migrations/postgres/20250531120000_add_entity_tables.go b/backend/internal/data/migrations/postgres/20250531120000_add_entity_tables.go new file mode 100644 index 00000000..cb1a205d --- /dev/null +++ b/backend/internal/data/migrations/postgres/20250531120000_add_entity_tables.go @@ -0,0 +1,202 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "github.com/google/uuid" + "github.com/pressly/goose/v3" + "time" +) + +//nolint:gochecknoinits +func init() { + goose.AddMigrationContext(Up20250531120000, Down20250531120000) +} + +func Up20250531120000(ctx context.Context, tx *sql.Tx) error { + // Create entity_types table + _, err := tx.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS "entity_types" ( + "id" uuid NOT NULL, + "created_at" timestamptz NOT NULL, + "updated_at" timestamptz NOT NULL, + "name" character varying NOT NULL, + "description" character varying NULL, + "icon" character varying NULL, + "color" character varying NULL, + "is_location" boolean NOT NULL DEFAULT false, + "group_entity_types" uuid NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "entity_types_groups_entity_types" FOREIGN KEY ("group_entity_types") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE + ); + `) + if err != nil { + return fmt.Errorf("failed to create entity_types table: %w", err) + } + + // Create entities table + _, err = tx.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS "entities" ( + "id" uuid NOT NULL, + "created_at" timestamptz NOT NULL, + "updated_at" timestamptz NOT NULL, + "name" character varying NOT NULL, + "description" character varying NULL, + "import_ref" character varying NULL, + "notes" character varying NULL, + "quantity" bigint NOT NULL DEFAULT 1, + "insured" boolean NOT NULL DEFAULT false, + "archived" boolean NOT NULL DEFAULT false, + "asset_id" bigint NOT NULL DEFAULT 0, + "serial_number" character varying NULL, + "model_number" character varying NULL, + "manufacturer" character varying NULL, + "lifetime_warranty" boolean NOT NULL DEFAULT false, + "warranty_expires" timestamptz NULL, + "warranty_details" character varying NULL, + "purchase_time" timestamptz NULL, + "purchase_from" character varying NULL, + "purchase_price" double precision NOT NULL DEFAULT 0, + "sold_time" timestamptz NULL, + "sold_to" character varying NULL, + "sold_price" double precision NOT NULL DEFAULT 0, + "sold_notes" character varying NULL, + "group_entities" uuid NOT NULL, + "entity_children" uuid NULL, + "entity_parent" uuid NULL, + "entity_type" uuid NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "entities_groups_entities" FOREIGN KEY ("group_entities") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, + CONSTRAINT "entities_entities_children" FOREIGN KEY ("entity_children") REFERENCES "entities" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, + CONSTRAINT "entities_entities_parent" FOREIGN KEY ("entity_parent") REFERENCES "entities" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, + CONSTRAINT "entities_entity_types" FOREIGN KEY ("entity_type") REFERENCES "entity_types" ("id") ON UPDATE NO ACTION ON DELETE CASCADE + ); + `) + if err != nil { + return fmt.Errorf("failed to create entities table: %w", err) + } + + // Create entity_fields table + _, err = tx.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS "entity_fields" ( + "id" uuid NOT NULL, + "created_at" timestamptz NOT NULL, + "updated_at" timestamptz NOT NULL, + "name" character varying NOT NULL, + "description" character varying NULL, + "type" character varying NOT NULL, + "text_value" character varying NULL, + "number_value" bigint NULL, + "boolean_value" boolean NOT NULL DEFAULT false, + "time_value" timestamptz NULL, + "entity_fields" uuid NULL, + PRIMARY KEY ("id"), + CONSTRAINT "entity_fields_entities_fields" FOREIGN KEY ("entity_fields") REFERENCES "entities" ("id") ON UPDATE NO ACTION ON DELETE CASCADE + ); + `) + if err != nil { + return fmt.Errorf("failed to create entity_fields table: %w", err) + } + + // Fetch all groups to create default entity types for each group + groups, err := tx.QueryContext(ctx, `SELECT id FROM "groups"`) + if err != nil { + return fmt.Errorf("failed to query groups: %w", err) + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + fmt.Printf("failed to close rows: %v\n", err) + } + }(groups) + + // Process each group and create default entity types + for groups.Next() { + var groupID uuid.UUID + if err := groups.Scan(&groupID); err != nil { + return fmt.Errorf("failed to scan group ID: %w", err) + } + + // Create default 'Item' entity type for this group + itemTypeID := uuid.New() + now := time.Now().UTC() + _, err = tx.ExecContext(ctx, ` + INSERT INTO "entity_types" ("id", "created_at", "updated_at", "name", "description", "is_location", "group_entity_types") + VALUES ($1, $2, $3, $4, $5, $6, $7) + `, itemTypeID, now, now, "Item", "Default item type", false, groupID) + if err != nil { + return fmt.Errorf("failed to create Item entity type for group %s: %w", groupID, err) + } + + // Create default 'Location' entity type for this group + locTypeID := uuid.New() + _, err = tx.ExecContext(ctx, ` + INSERT INTO "entity_types" ("id", "created_at", "updated_at", "name", "description", "is_location", "group_entity_types") + VALUES ($1, $2, $3, $4, $5, $6, $7) + `, locTypeID, now, now, "Location", "Default location type", true, groupID) + if err != nil { + return fmt.Errorf("failed to create Location entity type for group %s: %w", groupID, err) + } + + // Migrate existing locations to entities + _, err = tx.ExecContext(ctx, ` + INSERT INTO "entities" ( + "id", "created_at", "updated_at", "name", "description", + "group_entities", "entity_children", "entity_type" + ) + SELECT + l."id", l."created_at", l."updated_at", l."name", l."description", + l."group_locations", l."location_children", $1 + FROM "locations" l + WHERE l."group_locations" = $2 + `, locTypeID, groupID) + if err != nil { + return fmt.Errorf("failed to migrate locations to entities for group %s: %w", groupID, err) + } + + // Migrate existing items to entities + _, err = tx.ExecContext(ctx, ` + INSERT INTO "entities" ( + "id", "created_at", "updated_at", "name", "description", + "import_ref", "notes", "quantity", "insured", "archived", + "asset_id", "serial_number", "model_number", "manufacturer", + "lifetime_warranty", "warranty_expires", "warranty_details", "purchase_time", + "purchase_from", "purchase_price", "sold_time", "sold_to", + "sold_price", "sold_notes", "group_entities", "entity_children", + "entity_parent", "entity_type") + SELECT + i."id", i."created_at", i."updated_at", i."name", i."description", + i."import_ref", i."notes", i."quantity", i."insured", i."archived", + i."asset_id", i."serial_number", i."model_number", i."manufacturer", + i."lifetime_warranty", i."warranty_expires", i."warranty_details", i."purchase_time", + i."purchase_from", i."purchase_price", i."sold_time", i."sold_to", + i."sold_price", i."sold_notes", i."group_items", i."item_children", + i."item_parent", $1 + FROM "items" i WHERE i."group_items" = $2 + `, itemTypeID, groupID) + if err != nil { + return fmt.Errorf("failed to migrate items to entities for group %s: %w", groupID, err) + } + } + + _, err = tx.ExecContext(ctx, ` + INSERT INTO "entity_fields" ( + "id", "created_at", "updated_at", "name", "description", + "type", "text_value", "number_value", "boolean_value", "time_value", "entity_fields" + ) + SELECT + "id", "created_at", "updated_at", "name", "description", + "type", "text_value", "number_value", "boolean_value", "time_value", "item_fields" + FROM "item_fields"; + `) + if err != nil { + return fmt.Errorf("failed to migrate item_fields to entity_fields: %w", err) + } + + return nil +} + +func Down20250531120000(ctx context.Context, tx *sql.Tx) error { + return nil +} diff --git a/backend/internal/data/migrations/postgres/20250531120000_add_entity_tables.sql b/backend/internal/data/migrations/postgres/20250531120000_add_entity_tables.sql new file mode 100644 index 00000000..5d43b257 --- /dev/null +++ b/backend/internal/data/migrations/postgres/20250531120000_add_entity_tables.sql @@ -0,0 +1,188 @@ +-- Create entity_types table +CREATE TABLE IF NOT EXISTS "entity_types" ( + "id" uuid NOT NULL, + "created_at" timestamptz NOT NULL, + "updated_at" timestamptz NOT NULL, + "name" character varying NOT NULL, + "description" character varying NULL, + "icon" character varying NULL, + "color" character varying NULL, + "is_location" boolean NOT NULL DEFAULT false, + "group_entity_types" uuid NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "entity_types_groups_entity_types" FOREIGN KEY ("group_entity_types") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE +); + +-- Create entities table +CREATE TABLE IF NOT EXISTS "entities" ( + "id" uuid NOT NULL, + "created_at" timestamptz NOT NULL, + "updated_at" timestamptz NOT NULL, + "name" character varying NOT NULL, + "description" character varying NULL, + "import_ref" character varying NULL, + "notes" character varying NULL, + "quantity" bigint NOT NULL DEFAULT 1, + "insured" boolean NOT NULL DEFAULT false, + "archived" boolean NOT NULL DEFAULT false, + "asset_id" bigint NOT NULL DEFAULT 0, + "serial_number" character varying NULL, + "model_number" character varying NULL, + "manufacturer" character varying NULL, + "lifetime_warranty" boolean NOT NULL DEFAULT false, + "warranty_expires" timestamptz NULL, + "warranty_details" character varying NULL, + "purchase_time" timestamptz NULL, + "purchase_from" character varying NULL, + "purchase_price" double precision NOT NULL DEFAULT 0, + "sold_time" timestamptz NULL, + "sold_to" character varying NULL, + "sold_price" double precision NOT NULL DEFAULT 0, + "sold_notes" character varying NULL, + "group_entities" uuid NOT NULL, + "entity_children" uuid NULL, + "entity_parent" uuid NULL, + "entity_type" uuid NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "entities_groups_entities" FOREIGN KEY ("group_entities") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, + CONSTRAINT "entities_entities_children" FOREIGN KEY ("entity_children") REFERENCES "entities" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, + CONSTRAINT "entities_entities_parent" FOREIGN KEY ("entity_parent") REFERENCES "entities" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, + CONSTRAINT "entities_entity_types" FOREIGN KEY ("entity_type") REFERENCES "entity_types" ("id") ON UPDATE NO ACTION ON DELETE CASCADE +); + +-- Create entity_fields table +CREATE TABLE IF NOT EXISTS "entity_fields" ( + "id" uuid NOT NULL, + "created_at" timestamptz NOT NULL, + "updated_at" timestamptz NOT NULL, + "name" character varying NOT NULL, + "description" character varying NULL, + "type" character varying NOT NULL, + "text_value" character varying NULL, + "number_value" bigint NULL, + "boolean_value" boolean NOT NULL DEFAULT false, + "time_value" timestamptz NULL, + "entity_fields" uuid NULL, + PRIMARY KEY ("id"), + CONSTRAINT "entity_fields_entities_fields" FOREIGN KEY ("entity_fields") REFERENCES "entities" ("id") ON UPDATE NO ACTION ON DELETE CASCADE +); + +-- Create default entity types +INSERT INTO "entity_types" ("id", "created_at", "updated_at", "name", "description", "is_location", "group_entity_types") +SELECT + gen_random_uuid(), + NOW(), + NOW(), + 'Item', + 'Default item type', + false, + id +FROM "groups"; + +INSERT INTO "entity_types" ("id", "created_at", "updated_at", "name", "description", "is_location", "group_entity_types") +SELECT + gen_random_uuid(), + NOW(), + NOW(), + 'Location', + 'Default location type', + true, + id +FROM "groups"; + +-- Migrate locations to entities +INSERT INTO "entities" ( + "id", "created_at", "updated_at", "name", "description", + "group_entities", "entity_children", "entity_type" +) +SELECT + l."id", l."created_at", l."updated_at", l."name", l."description", + l."group_locations", l."location_children", + (SELECT et."id" FROM "entity_types" et WHERE et."name" = 'Location' AND et."group_entity_types" = l."group_locations" LIMIT 1) +FROM "locations" l; + +-- Migrate items to entities +INSERT INTO "entities" ( + "id", "created_at", "updated_at", "name", "description", + "import_ref", "notes", "quantity", "insured", "archived", + "asset_id", "serial_number", "model_number", "manufacturer", + "lifetime_warranty", "warranty_expires", "warranty_details", + "purchase_time", "purchase_from", "purchase_price", + "sold_time", "sold_to", "sold_price", "sold_notes", + "group_entities", "entity_children", "entity_parent", "entity_type" +) +SELECT + i."id", i."created_at", i."updated_at", i."name", i."description", + i."import_ref", i."notes", i."quantity", i."insured", i."archived", + i."asset_id", i."serial_number", i."model_number", i."manufacturer", + i."lifetime_warranty", i."warranty_expires", i."warranty_details", + i."purchase_time", i."purchase_from", i."purchase_price", + i."sold_time", i."sold_to", i."sold_price", i."sold_notes", + i."group_items", i."item_children", + i."location_items", + (SELECT et."id" FROM "entity_types" et WHERE et."name" = 'Item' AND et."group_entity_types" = i."group_items" LIMIT 1) +FROM "items" i; + +-- Migrate item_fields to entity_fields +INSERT INTO "entity_fields" ( + "id", "created_at", "updated_at", "name", "description", + "type", "text_value", "number_value", "boolean_value", "time_value", "entity_fields" +) +SELECT + "id", "created_at", "updated_at", "name", "description", + "type", "text_value", "number_value", "boolean_value", "time_value", "item_fields" +FROM "item_fields"; + +-- Update maintenance_entries to reference entities instead of items +ALTER TABLE "maintenance_entries" +ADD COLUMN "entity_id" uuid NULL; + +UPDATE "maintenance_entries" +SET "entity_id" = "item_id"; + +ALTER TABLE "maintenance_entries" +DROP CONSTRAINT "maintenance_entries_items_maintenance_entries"; + +ALTER TABLE "maintenance_entries" +ADD CONSTRAINT "maintenance_entries_entities_maintenance_entries" +FOREIGN KEY ("entity_id") REFERENCES "entities" ("id") +ON UPDATE NO ACTION ON DELETE CASCADE; + +ALTER TABLE "maintenance_entries" +DROP COLUMN "item_id"; + +-- Update attachments to reference entities instead of items +ALTER TABLE "attachments" +ADD COLUMN "entity_attachments" uuid NULL; + +UPDATE "attachments" +SET "entity_attachments" = "item_attachments"; + +ALTER TABLE "attachments" +DROP CONSTRAINT "attachments_items_attachments"; + +ALTER TABLE "attachments" +ADD CONSTRAINT "attachments_entities_attachments" +FOREIGN KEY ("entity_attachments") REFERENCES "entities" ("id") +ON UPDATE NO ACTION ON DELETE CASCADE; + +ALTER TABLE "attachments" +DROP COLUMN "item_attachments"; + +-- Update labels to reference entities +CREATE TABLE IF NOT EXISTS "label_entities" ( + "label_id" uuid NOT NULL, + "entity_id" uuid NOT NULL, + PRIMARY KEY ("label_id", "entity_id"), + CONSTRAINT "label_entities_entity_id" FOREIGN KEY ("entity_id") REFERENCES "entities" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, + CONSTRAINT "label_entities_label_id" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON UPDATE NO ACTION ON DELETE CASCADE +); + +INSERT INTO "label_entities" ("label_id", "entity_id") +SELECT "label_id", "item_id" FROM "label_items"; + +-- Drop old tables (do this last) +DROP TABLE IF EXISTS "label_items"; +DROP TABLE IF EXISTS "item_fields"; +DROP TABLE IF EXISTS "items"; +DROP TABLE IF EXISTS "locations"; diff --git a/backend/internal/data/migrations/sqlite3/20250510210023_entities_change.sql b/backend/internal/data/migrations/sqlite3/20250510210023_entities_change.sql deleted file mode 100644 index e56c6eb3..00000000 --- a/backend/internal/data/migrations/sqlite3/20250510210023_entities_change.sql +++ /dev/null @@ -1,189 +0,0 @@ --- +goose Up --- +goose no transaction -create table entities -( - id uuid not null - primary key, - type text not null, - created_at datetime not null, - updated_at datetime not null, - name text not null, - description text, - import_ref text, - notes text, - quantity integer default 1 not null, - insured bool default false not null, - archived bool default false not null, - asset_id integer default 0 not null, - serial_number text, - model_number text, - manufacturer text, - lifetime_warranty bool default false not null, - warranty_expires datetime, - warranty_details text, - purchase_time datetime, - purchase_from text, - purchase_price real default 0 not null, - sold_time datetime, - sold_to text, - sold_price real default 0 not null, - sold_notes text, - group_entities uuid not null - constraint entities_groups_entities - references groups - on delete cascade, - entity_children uuid - constraint entities_entities_children - references entities - on delete set null, - location_entities uuid - constraint entities_locations_entities - references entities - on delete cascade, - sync_child_entities_locations BOOLEAN default FALSE not null -); - -create index entity_archived - on entities (archived); - -create index entity_asset_id - on entities (asset_id); - -create index entity_manufacturer - on entities (manufacturer); - -create index entity_model_number - on entities (model_number); - -create index entity_name - on entities (name); - -create index entity_serial_number - on entities (serial_number); - -PRAGMA FOREIGN_KEYS = OFF; - --- Migrate the item_fields table to the new entity_fields table -create table entity_fields -( - id uuid not null - primary key, - created_at datetime not null, - updated_at datetime not null, - name text not null, - description text, - type text not null, - text_value text, - number_value integer, - boolean_value bool default false not null, - time_value datetime not null, - entity_fields uuid - constraint entity_fields_entities_fields - references entities - on delete cascade -); - -insert into entity_fields(id, created_at, updated_at, name, description, type, text_value, number_value, - boolean_value, time_value, entity_fields) -select id, - created_at, - updated_at, - name, - description, - type, - text_value, - number_value, - boolean_value, - time_value, - item_fields -from item_fields; - -drop table item_fields; - --- Update maintenance_entries to use the new entities table -create table maintenance_entries_dg_tmp -( - id uuid not null - primary key, - created_at datetime not null, - updated_at datetime not null, - date datetime, - scheduled_date datetime, - name text not null, - description text, - cost real default 0 not null, - entity_id uuid not null - constraint maintenance_entries_entities_maintenance_entries - references entities - on delete cascade -); - -insert into maintenance_entries_dg_tmp(id, created_at, updated_at, date, scheduled_date, name, description, cost, - entity_id) -select id, - created_at, - updated_at, - date, - scheduled_date, - name, - description, - cost, - item_id -from maintenance_entries; - -drop table maintenance_entries; - -alter table maintenance_entries_dg_tmp - rename to maintenance_entries; - --- Migrate the locations first -INSERT INTO entities (id, type, created_at, updated_at, name, description, group_entities, entity_children) -SELECT id, - 'location', - created_at, - updated_at, - name, - description, - group_locations, - location_children -FROM locations; - --- Then migrate the items -INSERT INTO entities (id, type, created_at, updated_at, name, description, import_ref, notes, quantity, insured, - archived, asset_id, serial_number, model_number, manufacturer, lifetime_warranty, - warranty_expires, warranty_details, purchase_time, purchase_from, purchase_price, sold_time, - sold_to, sold_price, sold_notes, group_entities, entity_children, location_entities) -SELECT id, - 'item', - created_at, - updated_at, - name, - description, - import_ref, - notes, - quantity, - insured, - archived, - asset_id, - serial_number, - model_number, - manufacturer, - lifetime_warranty, - warranty_expires, - warranty_details, - purchase_time, - purchase_from, - purchase_price, - sold_time, - sold_to, - sold_price, - sold_notes, - group_items, - item_children, - location_items -FROM items; - -PRAGMA FOREIGN_KEYS = ON; - -DROP TABLE locations; -DROP TABLE items; \ No newline at end of file diff --git a/backend/internal/data/migrations/sqlite3/20250531120000_add_entity_tables.go b/backend/internal/data/migrations/sqlite3/20250531120000_add_entity_tables.go new file mode 100644 index 00000000..cac19568 --- /dev/null +++ b/backend/internal/data/migrations/sqlite3/20250531120000_add_entity_tables.go @@ -0,0 +1,175 @@ +package sqlite3 + +import ( + "context" + "database/sql" + "fmt" + "github.com/google/uuid" + "github.com/pressly/goose/v3" + "time" +) + +//nolint:gochecknoinits +func init() { + goose.AddMigrationContext(Up20250531120000, Down20250531120000) +} + +func Up20250531120000(ctx context.Context, tx *sql.Tx) error { + // Create entity_types table + _, err := tx.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS "entity_types" ( + "id" text NOT NULL, + "created_at" datetime NOT NULL, + "updated_at" datetime NOT NULL, + "name" text NOT NULL, + "description" text NULL, + "icon" text NULL, + "color" text NULL, + "is_location" integer NOT NULL DEFAULT 0, + "group_entity_types" text NOT NULL, + PRIMARY KEY ("id"), + FOREIGN KEY ("group_entity_types") REFERENCES "groups" ("id") ON DELETE CASCADE + ); + `) + if err != nil { + return fmt.Errorf("failed to create entity_types table: %w", err) + } + + // Create entities table + _, err = tx.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS "entities" ( + "id" text NOT NULL, + "created_at" datetime NOT NULL, + "updated_at" datetime NOT NULL, + "name" text NOT NULL, + "description" text NULL, + "import_ref" text NULL, + "notes" text NULL, + "quantity" integer NOT NULL DEFAULT 1, + "insured" integer NOT NULL DEFAULT 0, + "archived" integer NOT NULL DEFAULT 0, + "asset_id" integer NOT NULL DEFAULT 0, + "serial_number" text NULL, + "model_number" text NULL, + "manufacturer" text NULL, + "lifetime_warranty" integer NOT NULL DEFAULT 0, + "warranty_expires" datetime NULL, + "warranty_details" text NULL, + "purchase_time" datetime NULL, + "purchase_from" text NULL, + "purchase_price" real NOT NULL DEFAULT 0, + "sold_time" datetime NULL, + "sold_to" text NULL, + "sold_price" real NOT NULL DEFAULT 0, + "sold_notes" text NULL, + "group_entities" text NOT NULL, + "entity_children" text NULL, + "entity_parent" text NULL, + "entity_type" text NOT NULL, + PRIMARY KEY ("id"), + FOREIGN KEY ("group_entities") REFERENCES "groups" ("id") ON DELETE CASCADE, + FOREIGN KEY ("entity_children") REFERENCES "entities" ("id") ON DELETE SET NULL, + FOREIGN KEY ("entity_parent") REFERENCES "entities" ("id") ON DELETE SET NULL, + FOREIGN KEY ("entity_type") REFERENCES "entity_types" ("id") ON DELETE CASCADE + ); + `) + if err != nil { + return fmt.Errorf("failed to create entities table: %w", err) + } + + // Create entity_fields table + _, err = tx.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS "entity_fields" ( + "id" text NOT NULL, + "created_at" datetime NOT NULL, + "updated_at" datetime NOT NULL, + "name" text NOT NULL, + "description" text NULL, + "type" text NOT NULL, + "text_value" text NULL, + "number_value" integer NULL, + "boolean_value" integer NOT NULL DEFAULT 0, + "time_value" datetime NULL, + "entity_fields" text NULL, + PRIMARY KEY ("id"), + FOREIGN KEY ("entity_fields") REFERENCES "entities" ("id") ON DELETE CASCADE + ); + `) + if err != nil { + return fmt.Errorf("failed to create entity_fields table: %w", err) + } + + // Fetch all groups to create default entity types for each group + rows, err := tx.QueryContext(ctx, `SELECT id FROM "groups"`) + if err != nil { + return fmt.Errorf("failed to query groups: %w", err) + } + defer rows.Close() + + // Process each group and create default entity types + for rows.Next() { + var groupID string + if err := rows.Scan(&groupID); err != nil { + return fmt.Errorf("failed to scan group ID: %w", err) + } + + // Create default 'Item' entity type for this group + itemTypeID := uuid.New().String() + now := time.Now().UTC() + _, err = tx.ExecContext(ctx, ` + INSERT INTO "entity_types" ("id", "created_at", "updated_at", "name", "description", "is_location", "group_entity_types") + VALUES (?, ?, ?, ?, ?, ?, ?) + `, itemTypeID, now, now, "Item", "Default item type", 0, groupID) + if err != nil { + return fmt.Errorf("failed to create Item entity type for group %s: %w", groupID, err) + } + + // Create default 'Location' entity type for this group + locTypeID := uuid.New().String() + _, err = tx.ExecContext(ctx, ` + INSERT INTO "entity_types" ("id", "created_at", "updated_at", "name", "description", "is_location", "group_entity_types") + VALUES (?, ?, ?, ?, ?, ?, ?) + `, locTypeID, now, now, "Location", "Default location type", 1, groupID) + if err != nil { + return fmt.Errorf("failed to create Location entity type for group %s: %w", groupID, err) + } + + // Migrate existing locations to entities + _, err = tx.ExecContext(ctx, ` + INSERT INTO "entities" ( + "id", "created_at", "updated_at", "name", "description", + "group_entities", "entity_children", "entity_type" + ) + SELECT + l."id", l."created_at", l."updated_at", l."name", l."description", + l."group_locations", l."location_children", ? + FROM "locations" l + WHERE l."group_locations" = ? + `, locTypeID, groupID) + if err != nil { + return fmt.Errorf("failed to migrate locations to entities for group %s: %w", groupID, err) + } + } + + return nil +} + +func Down20250531120000(ctx context.Context, tx *sql.Tx) error { + // Drop tables in reverse order to avoid foreign key constraints + _, err := tx.ExecContext(ctx, `DROP TABLE IF EXISTS "entity_fields";`) + if err != nil { + return fmt.Errorf("failed to drop entity_fields table: %w", err) + } + + _, err = tx.ExecContext(ctx, `DROP TABLE IF EXISTS "entities";`) + if err != nil { + return fmt.Errorf("failed to drop entities table: %w", err) + } + + _, err = tx.ExecContext(ctx, `DROP TABLE IF EXISTS "entity_types";`) + if err != nil { + return fmt.Errorf("failed to drop entity_types table: %w", err) + } + + return nil +} diff --git a/backend/internal/data/migrations/sqlite3/20250531120000_add_entity_tables.sql b/backend/internal/data/migrations/sqlite3/20250531120000_add_entity_tables.sql new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/backend/internal/data/migrations/sqlite3/20250531120000_add_entity_tables.sql @@ -0,0 +1 @@ +