Erste Produktivversion

This commit is contained in:
2026-08-08 20:31:58 +02:00
parent d8ded2816d
commit 7b21e2d1b4
110 changed files with 18170 additions and 644 deletions

509
db/schema.sql Normal file
View File

@@ -0,0 +1,509 @@
-- ==========================================================================
-- 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;