Files
einkaufsapp/db/schema.sql

520 lines
23 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ==========================================================================
-- Einkaufsliste vollständiges Datenbankschema
-- Entspricht dem Stand nach Alembic-Revision 0013.
-- ==========================================================================
--
-- 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,
-- Vorwarnungen vor der automatischen Löschung, getrennt vermerkt,
-- damit jede Stufe genau einmal verschickt wird
`warn_7d_at` DATETIME NULL,
`warn_1d_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,
-- Archivierung ist eine Eigenschaft der MITGLIEDSCHAFT, nicht der
-- Liste: Dieselbe Liste kann bei einem Mitglied im Archiv liegen und
-- beim anderen aktiv sein.
`archived_at` DATETIME NULL,
-- Grundlage der automatischen Archivierung, je Person getrennt
`last_opened_at` DATETIME NULL,
`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 ('0013');
SET FOREIGN_KEY_CHECKS = 1;