-- ========================================================================== -- Einkaufsliste – vollständiges Datenbankschema -- Entspricht dem Stand nach Alembic-Revision 0012. -- ========================================================================== -- -- Normalerweise wird diese Datei NICHT gebraucht: Der api-Container -- führt beim Start `alembic upgrade head` aus und legt alles selbst an. -- -- Sie ist da für: -- * Nachvollziehbarkeit im Versionsverwaltungssystem – man sieht auf -- einen Blick, wie das Schema aussieht, ohne elf Migrationen zu lesen -- * Aufsetzen ohne Alembic, etwa auf einer verwalteten Datenbank -- * Vergleich, wenn eine Migration irgendwo halb durchgelaufen ist -- -- Einspielen: -- docker compose up -d db -- docker compose exec -T db mariadb -u root -p einkaufsapp < db/schema.sql -- -- Der Eintrag in `alembic_version` am Ende ist wichtig: Ohne ihn würde -- Alembic beim nächsten Start alle Migrationen erneut anwenden und an -- den bereits vorhandenen Tabellen scheitern. -- -- Alle Bezeichner in Backticks, weil `key` und `count` in MariaDB -- Schlüsselwörter sind. -- ========================================================================== SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- -------------------------------------------------------------------------- -- Konten und Anmeldung -- -------------------------------------------------------------------------- CREATE TABLE `user` ( `id` VARCHAR(36) NOT NULL, -- Normalisiert (kleingeschrieben, getrimmt). Die Eindeutigkeit steht -- auf diesem Wert, deshalb muss die Anwendung konsequent normalisieren. `email` VARCHAR(255) NOT NULL, `display_name` VARCHAR(80) NULL, -- Argon2id `password_hash` VARCHAR(255) NOT NULL, `verified_at` DATETIME NULL, `is_admin` TINYINT(1) NOT NULL DEFAULT 0, `is_active` TINYINT(1) NOT NULL DEFAULT 1, -- Sperrt alles außer "Profil lesen" und "Passwort ändern" `must_change_password` TINYINT(1) NOT NULL DEFAULT 0, -- Grundlage der automatischen Deaktivierung. Wird bei der Anmeldung -- gesetzt und danach höchstens stündlich nachgeführt. `last_seen_at` DATETIME NULL, -- Seit wann deaktiviert. Grundlage der automatischen Löschung; -- is_active allein sagt nichts über die Dauer. `deactivated_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uq_user_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `user_session` ( `id` VARCHAR(36) NOT NULL, -- Nur der SHA-256-Hash des Sitzungstokens. Der Klartext steht -- ausschließlich im HttpOnly-Cookie des Browsers. `token_hash` VARCHAR(64) NOT NULL, `user_id` VARCHAR(36) NOT NULL, -- Double-Submit-Verfahren: steht zusätzlich in einem lesbaren Cookie -- und muss bei schreibenden Anfragen im Header wiederkommen. `csrf_token` VARCHAR(64) NOT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `expires_at` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_user_session_token` (`token_hash`), KEY `ix_user_session_expires` (`expires_at`), KEY `fk_user_session_user` (`user_id`), CONSTRAINT `fk_user_session_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `email_token` ( `id` VARCHAR(36) NOT NULL, `token_hash` VARCHAR(64) NOT NULL, `user_id` VARCHAR(36) NOT NULL, -- "verify" | "reset" `purpose` VARCHAR(32) NOT NULL, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uq_email_token_token` (`token_hash`), KEY `ix_email_token_user_purpose` (`user_id`, `purpose`), CONSTRAINT `fk_email_token_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Laufende Änderung einer E-Mail-Adresse. -- -- Bei gewöhnlichen Konten muss nur die NEUE Adresse bestätigen; die alte -- bekommt eine Benachrichtigung. Bei Administratorkonten müssen BEIDE -- bestätigen: Wer Zugriff auf ein Administratorkonto erlangt, könnte -- sonst die Adresse auf eine eigene umstellen und sich dauerhaft -- einnisten - der rechtmäßige Inhaber verlöre den Weg zurück über -- "Passwort vergessen". CREATE TABLE `email_change` ( `id` VARCHAR(36) NOT NULL, `user_id` VARCHAR(36) NOT NULL, `old_email` VARCHAR(255) NOT NULL, `new_email` VARCHAR(255) NOT NULL, `token_new_hash` VARCHAR(64) NOT NULL, `token_old_hash` VARCHAR(64) NULL, -- True bei Administratorkonten `requires_old` TINYINT(1) NOT NULL DEFAULT 0, `confirmed_new_at` DATETIME NULL, `confirmed_old_at` DATETIME NULL, `applied_at` DATETIME NULL, `cancelled_at` DATETIME NULL, `requested_by` VARCHAR(36) NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `expires_at` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_email_change_token_new` (`token_new_hash`), UNIQUE KEY `uq_email_change_token_old` (`token_old_hash`), KEY `ix_email_change_user` (`user_id`), KEY `fk_email_change_requested_by` (`requested_by`), CONSTRAINT `fk_email_change_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_email_change_requested_by` FOREIGN KEY (`requested_by`) REFERENCES `user` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Betrieb -- -------------------------------------------------------------------------- CREATE TABLE `setting` ( `key` VARCHAR(64) NOT NULL, `value` VARCHAR(255) NOT NULL, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Zählerbasiertes Rate Limiting ohne Redis: ein Datensatz je -- (Schlüssel, Zeitfenster). CREATE TABLE `rate_limit` ( `bucket` VARCHAR(160) NOT NULL, `window_start` DATETIME NOT NULL, `count` INT NOT NULL DEFAULT 0, PRIMARY KEY (`bucket`, `window_start`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Listen und Mitgliedschaften -- -------------------------------------------------------------------------- CREATE TABLE `shopping_list` ( `id` VARCHAR(36) NOT NULL, `name` VARCHAR(120) NOT NULL, `owner_id` VARCHAR(36) NOT NULL, -- Monoton steigender Zähler je Liste. Jede Änderung erhöht ihn und -- schreibt den neuen Wert in row_rev der geänderten Zeile. `rev` BIGINT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Soft Delete: Offline gebliebene Geräte sollen vom Löschen erfahren. `deleted_at` DATETIME NULL, PRIMARY KEY (`id`), KEY `fk_shopping_list_owner` (`owner_id`), -- RESTRICT: Ein Konto lässt sich nicht löschen, solange ihm Listen -- gehören. Sonst verwaisen sie. CONSTRAINT `fk_shopping_list_owner` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `list_member` ( `list_id` VARCHAR(36) NOT NULL, `user_id` VARCHAR(36) NOT NULL, -- "owner" | "editor" | "viewer" `role` VARCHAR(16) NOT NULL DEFAULT 'editor', -- Zusatzrecht, unabhängig von der Rolle `may_share_public` TINYINT(1) NOT NULL DEFAULT 0, `joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`list_id`, `user_id`), KEY `ix_list_member_user` (`user_id`), CONSTRAINT `fk_list_member_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_list_member_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Katalog: Märkte, Warengruppen, Artikel -- -------------------------------------------------------------------------- CREATE TABLE `market` ( `list_id` VARCHAR(36) NOT NULL, `id` VARCHAR(36) NOT NULL, `name` VARCHAR(120) NOT NULL, -- Bestimmt die Reihenfolge in Liste und Ausdruck `sort_order` INT NOT NULL DEFAULT 0, `row_rev` BIGINT NOT NULL DEFAULT 0, `deleted_at` DATETIME NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_market_list_name` (`list_id`, `name`), KEY `ix_market_list_rev` (`list_id`, `row_rev`), CONSTRAINT `fk_market_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `category` ( `list_id` VARCHAR(36) NOT NULL, `id` VARCHAR(36) NOT NULL, `name` VARCHAR(120) NOT NULL, `sort_order` INT NOT NULL DEFAULT 0, `row_rev` BIGINT NOT NULL DEFAULT 0, `deleted_at` DATETIME NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_category_list_name` (`list_id`, `name`), KEY `ix_category_list_rev` (`list_id`, `row_rev`), CONSTRAINT `fk_category_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `article` ( `id` VARCHAR(36) NOT NULL, `list_id` VARCHAR(36) NOT NULL, `name` VARCHAR(200) NOT NULL, `barcode` VARCHAR(64) NULL, `note` VARCHAR(500) NULL, -- Vorgaben für neue Listeneinträge `default_market_id` VARCHAR(36) NULL, `default_category_id` VARCHAR(36) NULL, `row_rev` BIGINT NOT NULL DEFAULT 0, `deleted_at` DATETIME NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_article_list_name` (`list_id`, `name`), KEY `ix_article_list_rev` (`list_id`, `row_rev`), KEY `ix_article_barcode` (`list_id`, `barcode`), KEY `fk_article_market` (`default_market_id`), KEY `fk_article_category` (`default_category_id`), CONSTRAINT `fk_article_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_article_market` FOREIGN KEY (`default_market_id`) REFERENCES `market` (`id`) ON DELETE SET NULL, CONSTRAINT `fk_article_category` FOREIGN KEY (`default_category_id`) REFERENCES `category` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Frei definierbare Eigenschaften: Verpackungseinheit, Farbe, Größe. -- Spalte heißt attr_name, weil `key` reserviert ist. CREATE TABLE `article_attribute` ( `id` VARCHAR(36) NOT NULL, `article_id` VARCHAR(36) NOT NULL, `attr_name` VARCHAR(80) NOT NULL, `attr_value` VARCHAR(300) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_article_attr` (`article_id`, `attr_name`), CONSTRAINT `fk_article_attribute_article` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- In welchen Märkten ist der Artikel erhältlich CREATE TABLE `article_market` ( `article_id` VARCHAR(36) NOT NULL, `market_id` VARCHAR(36) NOT NULL, PRIMARY KEY (`article_id`, `market_id`), KEY `fk_article_market_market` (`market_id`), CONSTRAINT `fk_article_market_article` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_article_market_market` FOREIGN KEY (`market_id`) REFERENCES `market` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Listeneinträge -- -------------------------------------------------------------------------- CREATE TABLE `list_item` ( `id` VARCHAR(36) NOT NULL, `list_id` VARCHAR(36) NOT NULL, `article_id` VARCHAR(36) NOT NULL, -- Überschreiben die Vorgaben aus dem Artikel, falls gesetzt `market_id` VARCHAR(36) NULL, `category_id` VARCHAR(36) NULL, -- Stückzahl: wie viele Gebinde. price_cents gilt für EINES davon, -- die Summe ist price_cents * count. `count` INT NOT NULL DEFAULT 1, -- Gebinde: wie groß eine Packung ist (500 ml, 250 g) `pack_size` DECIMAL(10,3) NULL, `pack_unit` VARCHAR(32) NULL, -- Nähere Bestimmung, z. B. "bunt, ganz" `variant` VARCHAR(200) NULL, -- Bemerkung für den Einkaufenden `note` VARCHAR(500) NULL, -- "open" | "bought" | "deferred" `status` VARCHAR(16) NOT NULL DEFAULT 'open', -- Preis in Cent für EIN Gebinde. Niemals als Fließkommazahl rechnen. `price_cents` INT NULL, `created_by` VARCHAR(36) NULL, `row_rev` BIGINT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted_at` DATETIME NULL, PRIMARY KEY (`id`), KEY `ix_list_item_list_rev` (`list_id`, `row_rev`), KEY `ix_list_item_list_status` (`list_id`, `status`), KEY `fk_list_item_article` (`article_id`), KEY `fk_list_item_market` (`market_id`), KEY `fk_list_item_category` (`category_id`), KEY `fk_list_item_created_by` (`created_by`), CONSTRAINT `fk_list_item_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_list_item_article` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_list_item_market` FOREIGN KEY (`market_id`) REFERENCES `market` (`id`) ON DELETE SET NULL, CONSTRAINT `fk_list_item_category` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE SET NULL, -- SET NULL: Der Eintrag bleibt, der Personenbezug verschwindet CONSTRAINT `fk_list_item_created_by` FOREIGN KEY (`created_by`) REFERENCES `user` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Teilen -- -------------------------------------------------------------------------- CREATE TABLE `list_invite` ( `id` VARCHAR(36) NOT NULL, `list_id` VARCHAR(36) NOT NULL, `email` VARCHAR(255) NOT NULL, `role` VARCHAR(16) NOT NULL DEFAULT 'editor', -- Nur der Hash. Erneutes Senden erzeugt deshalb ein neues Token. `token_hash` VARCHAR(64) NOT NULL, `invited_by` VARCHAR(36) NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_sent_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `send_count` INT NOT NULL DEFAULT 1, `expires_at` DATETIME NOT NULL, `accepted_at` DATETIME NULL, `accepted_by` VARCHAR(36) NULL, `revoked_at` DATETIME NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_list_invite_token` (`token_hash`), KEY `ix_list_invite_list` (`list_id`), KEY `ix_list_invite_email` (`email`), KEY `fk_list_invite_invited_by` (`invited_by`), KEY `fk_list_invite_accepted_by` (`accepted_by`), CONSTRAINT `fk_list_invite_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_list_invite_invited_by` FOREIGN KEY (`invited_by`) REFERENCES `user` (`id`) ON DELETE SET NULL, CONSTRAINT `fk_list_invite_accepted_by` FOREIGN KEY (`accepted_by`) REFERENCES `user` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE `public_share` ( `id` VARCHAR(36) NOT NULL, `list_id` VARCHAR(36) NOT NULL, -- Nur der Hash: Ein Datenbankleck gibt keinen Zugriff auf die Listen. `token_hash` VARCHAR(64) NOT NULL, `label` VARCHAR(120) NULL, -- Darf über diesen Link abgehakt werden? `allow_check` TINYINT(1) NOT NULL DEFAULT 1, `created_by` VARCHAR(36) NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Pflicht: ein unbefristeter Link wäre ein dauerhaft offenes Fenster `expires_at` DATETIME NOT NULL, `revoked_at` DATETIME NULL, -- Grobe Nutzungsanzeige ohne IP-Adressen und ohne Zeitreihe `last_access_at` DATETIME NULL, `access_count` INT NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uq_public_share_token` (`token_hash`), KEY `ix_public_share_list` (`list_id`), KEY `fk_public_share_created_by` (`created_by`), CONSTRAINT `fk_public_share_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_public_share_created_by` FOREIGN KEY (`created_by`) REFERENCES `user` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Synchronisation -- -------------------------------------------------------------------------- -- Quittung für eine bereits verarbeitete Operation aus einer -- Outbox-Warteschlange. Verhindert, dass aus einem verlorenen -- "Butter hinzufügen" beim nächsten Versuch zweimal Butter wird. CREATE TABLE `applied_op` ( `op_id` VARCHAR(64) NOT NULL, `list_id` VARCHAR(36) NOT NULL, `user_id` VARCHAR(36) NULL, `kind` VARCHAR(32) NOT NULL, -- Bei item.create: die vergebene ID, damit der Client seinen -- vorläufigen Eintrag zuordnen kann `result_id` VARCHAR(36) NULL, `rev` BIGINT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`op_id`), KEY `ix_applied_op_list` (`list_id`, `created_at`), KEY `fk_applied_op_user` (`user_id`), CONSTRAINT `fk_applied_op_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_applied_op_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Produktdaten und Preise -- -------------------------------------------------------------------------- -- Zwischenspeicher für Abfragen bei Open Food Facts. Sorgt dafür, dass -- jeder Strichcode höchstens einmal je Gültigkeitszeitraum nach außen -- geht – und zwar vom Server, nicht vom Gerät des Nutzers. CREATE TABLE `product_cache` ( `barcode` VARCHAR(64) NOT NULL, `found` TINYINT(1) NOT NULL DEFAULT 0, `name` VARCHAR(300) NULL, `brand` VARCHAR(200) NULL, -- Rohtext wie "500 g" oder "6 x 33 cl" `package` VARCHAR(120) NULL, -- Daraus zerlegt `count` INT NULL, `pack_size` DECIMAL(10,3) NULL, `pack_unit` VARCHAR(32) NULL, `source` VARCHAR(32) NOT NULL DEFAULT 'openfoodfacts', `fetched_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`barcode`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Beobachteter Preis. Bewusst OHNE Nutzerbezug: Aus "wer hat wann wo was -- zu welchem Preis gekauft" ließe sich ein Bewegungs- und Konsumprofil -- bilden. Für den Preisvergleich genügt Markt, Artikel und Zeitpunkt. CREATE TABLE `price_point` ( `id` VARCHAR(36) NOT NULL, `list_id` VARCHAR(36) NOT NULL, `article_id` VARCHAR(36) NOT NULL, `market_id` VARCHAR(36) NOT NULL, -- Preis für EIN Gebinde `price_cents` INT NOT NULL, `pack_size` DECIMAL(10,3) NULL, `pack_unit` VARCHAR(32) NULL, `recorded_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `ix_price_article_market` (`article_id`, `market_id`, `recorded_at`), KEY `ix_price_list` (`list_id`, `recorded_at`), KEY `fk_price_point_market` (`market_id`), CONSTRAINT `fk_price_point_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_price_point_article` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_price_point_market` FOREIGN KEY (`market_id`) REFERENCES `market` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Push-Benachrichtigungen -- -------------------------------------------------------------------------- CREATE TABLE `push_subscription` ( `id` VARCHAR(36) NOT NULL, `user_id` VARCHAR(36) NOT NULL, -- Vom Push-Dienst des Browserherstellers vergebene URL `endpoint` VARCHAR(500) NOT NULL, -- Schlüsselmaterial für die Ende-zu-Ende-Verschlüsselung. Auch der -- Push-Dienst kann den Inhalt der Nachricht nicht lesen. `p256dh` VARCHAR(200) NOT NULL, `auth` VARCHAR(100) NOT NULL, `label` VARCHAR(80) NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_success_at` DATETIME NULL, `failure_count` INT NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uq_push_subscription_endpoint` (`endpoint`), KEY `ix_push_subscription_user` (`user_id`), CONSTRAINT `fk_push_subscription_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Grundlage der Zwei-Stunden-Drosselung CREATE TABLE `notify_state` ( `list_id` VARCHAR(36) NOT NULL, `user_id` VARCHAR(36) NOT NULL, `last_notified_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`list_id`, `user_id`), KEY `fk_notify_state_user` (`user_id`), CONSTRAINT `fk_notify_state_list` FOREIGN KEY (`list_id`) REFERENCES `shopping_list` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_notify_state_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------------------------- -- Migrationsstand -- -------------------------------------------------------------------------- -- -- Ohne diesen Eintrag würde Alembic beim nächsten Start alle -- Migrationen erneut anwenden und an den vorhandenen Tabellen -- scheitern. CREATE TABLE `alembic_version` ( `version_num` VARCHAR(32) NOT NULL, PRIMARY KEY (`version_num`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; INSERT INTO `alembic_version` (`version_num`) VALUES ('0012'); SET FOREIGN_KEY_CHECKS = 1;