510 lines
23 KiB
SQL
510 lines
23 KiB
SQL
-- ==========================================================================
|
||
-- 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;
|