Initial Switch to V2. Completely Overhauled Backend, Frontend and General Structure.
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
"""phase_0_schema_foundation
|
||||
|
||||
Adds all Phase 0 tables:
|
||||
- _migration_runs (migration tracking)
|
||||
- audit_log (staff action audit trail)
|
||||
- crm_products
|
||||
- crm_customers
|
||||
- crm_orders
|
||||
- crm_comms_log
|
||||
- crm_media
|
||||
- crm_sync_state
|
||||
- crm_quotations
|
||||
- crm_quotation_items
|
||||
- staff
|
||||
- console_settings
|
||||
- public_features
|
||||
- melody_drafts
|
||||
- built_melodies
|
||||
- mfg_audit_log
|
||||
- device_alerts
|
||||
- commands (raw SQL — no ORM model)
|
||||
- heartbeats (raw SQL — no ORM model)
|
||||
- device_logs (partitioned by month — raw SQL)
|
||||
- device_logs_2025_01 … device_logs_2026_06 (initial partitions)
|
||||
|
||||
Revision ID: b1c2d3e4f5a6
|
||||
Revises: a1b2c3d4e5f6
|
||||
Create Date: 2026-04-17 00:00:00.000000
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||
|
||||
# revision identifiers
|
||||
revision: str = "b1c2d3e4f5a6"
|
||||
down_revision: Union[str, None] = "a1b2c3d4e5f6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ------------------------------------------------------------------ #
|
||||
# _migration_runs
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"_migration_runs",
|
||||
sa.Column("id", sa.BigInteger(), primary_key=True, autoincrement=True),
|
||||
sa.Column("script_name", sa.String(256), nullable=False),
|
||||
sa.Column("ran_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
sa.Column("source_rows", sa.BigInteger(), nullable=False, server_default="0"),
|
||||
sa.Column("dest_rows", sa.BigInteger(), nullable=False, server_default="0"),
|
||||
sa.Column("success", sa.String(8), nullable=False, server_default="ok"),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# audit_log
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"audit_log",
|
||||
sa.Column("id", sa.BigInteger(), primary_key=True, autoincrement=True),
|
||||
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
sa.Column("actor_id", sa.String(128), nullable=False),
|
||||
sa.Column("actor_name", sa.String(255), nullable=False),
|
||||
sa.Column("action", sa.String(64), nullable=False),
|
||||
sa.Column("entity_type", sa.String(64), nullable=False),
|
||||
sa.Column("entity_id", sa.String(128), nullable=False),
|
||||
sa.Column("entity_label", sa.String(500), nullable=True),
|
||||
sa.Column("changes", JSONB, nullable=True),
|
||||
sa.Column("meta", JSONB, nullable=True),
|
||||
)
|
||||
op.create_index("idx_audit_actor", "audit_log", ["actor_id", "occurred_at"])
|
||||
op.create_index("idx_audit_entity", "audit_log", ["entity_type","entity_id", "occurred_at"])
|
||||
op.create_index("idx_audit_action", "audit_log", ["action", "occurred_at"])
|
||||
op.create_index("idx_audit_occurred", "audit_log", ["occurred_at"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# staff
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"staff",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("firestore_id", sa.String(128), nullable=True, unique=True),
|
||||
sa.Column("email", sa.String(256), nullable=False, unique=True),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("role", sa.String(64), nullable=False, server_default="staff"),
|
||||
sa.Column("permissions", JSONB, nullable=False, server_default="{}"),
|
||||
sa.Column("hashed_password", sa.String(256), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# console_settings & public_features
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"console_settings",
|
||||
sa.Column("key", sa.String(128), primary_key=True),
|
||||
sa.Column("value", JSONB, nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
op.create_table(
|
||||
"public_features",
|
||||
sa.Column("key", sa.String(128), primary_key=True),
|
||||
sa.Column("value", JSONB, nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# crm_products
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"crm_products",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("firestore_id", sa.String(128), nullable=True, unique=True),
|
||||
sa.Column("name", sa.String(500), nullable=False),
|
||||
sa.Column("sku", sa.String(128), nullable=True),
|
||||
sa.Column("category", sa.String(128), nullable=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("unit_cost", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("currency", sa.String(10), nullable=False, server_default="EUR"),
|
||||
sa.Column("unit_type", sa.String(32), nullable=False, server_default="pcs"),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# crm_customers
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"crm_customers",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("firestore_id", sa.String(128), nullable=True, unique=True),
|
||||
sa.Column("title", sa.String(32), nullable=True),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("surname", sa.String(255), nullable=True),
|
||||
sa.Column("organization", sa.String(500), nullable=True),
|
||||
sa.Column("religion", sa.String(64), nullable=True),
|
||||
sa.Column("language", sa.String(10), nullable=False, server_default="el"),
|
||||
sa.Column("folder_id", sa.String(128), nullable=False, unique=True),
|
||||
sa.Column("relationship_status", sa.String(64), nullable=False, server_default="lead"),
|
||||
sa.Column("nextcloud_folder", sa.String(500), nullable=True),
|
||||
sa.Column("contacts", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("notes", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("location", JSONB, nullable=True),
|
||||
sa.Column("tags", ARRAY(sa.String()), nullable=False, server_default="{}"),
|
||||
sa.Column("owned_items", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("linked_user_ids", ARRAY(sa.String()), nullable=False, server_default="{}"),
|
||||
sa.Column("technical_issues", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("install_support", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("transaction_history", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("crm_summary", JSONB, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index("idx_crm_customers_rel_status", "crm_customers", ["relationship_status"])
|
||||
op.create_index("idx_crm_customers_name", "crm_customers", ["name", "surname"])
|
||||
op.create_index("idx_crm_customers_tags", "crm_customers", ["tags"],
|
||||
postgresql_using="gin")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# crm_orders
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"crm_orders",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("customer_id", sa.String(128),
|
||||
sa.ForeignKey("crm_customers.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("order_number", sa.String(64), nullable=False, unique=True),
|
||||
sa.Column("title", sa.String(500), nullable=True),
|
||||
sa.Column("created_by", sa.String(128), nullable=True),
|
||||
sa.Column("status", sa.String(64), nullable=False,
|
||||
server_default="negotiating"),
|
||||
sa.Column("status_updated_date", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("status_updated_by", sa.String(128), nullable=True),
|
||||
sa.Column("items", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("subtotal", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("discount", JSONB, nullable=True),
|
||||
sa.Column("total_price", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("currency", sa.String(10), nullable=False, server_default="EUR"),
|
||||
sa.Column("shipping", JSONB, nullable=True),
|
||||
sa.Column("payment_status", JSONB, nullable=False, server_default="{}"),
|
||||
sa.Column("invoice_path", sa.String(500), nullable=True),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("timeline", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index("idx_crm_orders_customer", "crm_orders", ["customer_id"])
|
||||
op.create_index("idx_crm_orders_status", "crm_orders", ["status"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# crm_comms_log
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"crm_comms_log",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("customer_id", sa.String(128),
|
||||
sa.ForeignKey("crm_customers.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("type", sa.String(32), nullable=False),
|
||||
sa.Column("mail_account", sa.String(256), nullable=True),
|
||||
sa.Column("direction", sa.String(16), nullable=False),
|
||||
sa.Column("subject", sa.String(500), nullable=True),
|
||||
sa.Column("body", sa.Text(), nullable=True),
|
||||
sa.Column("body_html", sa.Text(), nullable=True),
|
||||
sa.Column("attachments", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("ext_message_id", sa.String(500), nullable=True),
|
||||
sa.Column("from_addr", sa.String(500), nullable=True),
|
||||
sa.Column("to_addrs", sa.Text(), nullable=True),
|
||||
sa.Column("logged_by", sa.String(128), nullable=True),
|
||||
sa.Column("is_important", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("is_read", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("idx_crm_comms_customer", "crm_comms_log", ["customer_id", "occurred_at"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# crm_media
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"crm_media",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("customer_id", sa.String(128),
|
||||
sa.ForeignKey("crm_customers.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("order_id", sa.String(128), nullable=True),
|
||||
sa.Column("filename", sa.String(500), nullable=False),
|
||||
sa.Column("nextcloud_path", sa.String(1000), nullable=False),
|
||||
sa.Column("thumbnail_path", sa.String(1000), nullable=True),
|
||||
sa.Column("mime_type", sa.String(128), nullable=True),
|
||||
sa.Column("direction", sa.String(16), nullable=True),
|
||||
sa.Column("tags", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("uploaded_by", sa.String(128), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("idx_crm_media_customer", "crm_media", ["customer_id"])
|
||||
op.create_index("idx_crm_media_order", "crm_media", ["order_id"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# crm_sync_state
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"crm_sync_state",
|
||||
sa.Column("key", sa.String(128), primary_key=True),
|
||||
sa.Column("value", sa.Text(), nullable=True),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# crm_quotations
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"crm_quotations",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("quotation_number", sa.String(64), nullable=False, unique=True),
|
||||
sa.Column("title", sa.String(500), nullable=True),
|
||||
sa.Column("subtitle", sa.String(500), nullable=True),
|
||||
sa.Column("customer_id", sa.String(128),
|
||||
sa.ForeignKey("crm_customers.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("language", sa.String(10), nullable=False, server_default="en"),
|
||||
sa.Column("status", sa.String(32), nullable=False, server_default="draft"),
|
||||
sa.Column("order_type", sa.String(64), nullable=True),
|
||||
sa.Column("shipping_method", sa.String(64), nullable=True),
|
||||
sa.Column("estimated_shipping_date", sa.String(32), nullable=True),
|
||||
sa.Column("global_discount_label", sa.String(128), nullable=True),
|
||||
sa.Column("global_discount_percent", sa.Numeric(8, 4), nullable=False, server_default="0"),
|
||||
sa.Column("vat_percent", sa.Numeric(8, 4), nullable=False, server_default="24"),
|
||||
sa.Column("global_vat_percent", sa.Numeric(8, 4), nullable=False, server_default="24"),
|
||||
sa.Column("shipping_cost", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("shipping_cost_discount", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("install_cost", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("install_cost_discount", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("extras_label", sa.String(256), nullable=True),
|
||||
sa.Column("extras_cost", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("comments", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("quick_notes", JSONB, nullable=False, server_default="{}"),
|
||||
sa.Column("subtotal_before_discount", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("global_discount_amount", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("new_subtotal", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("vat_amount", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("final_total", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("nextcloud_pdf_path", sa.String(1000), nullable=True),
|
||||
sa.Column("nextcloud_pdf_url", sa.String(1000), nullable=True),
|
||||
sa.Column("client_org", sa.String(500), nullable=True),
|
||||
sa.Column("client_name", sa.String(500), nullable=True),
|
||||
sa.Column("client_location", sa.String(500), nullable=True),
|
||||
sa.Column("client_phone", sa.String(64), nullable=True),
|
||||
sa.Column("client_email", sa.String(256), nullable=True),
|
||||
sa.Column("is_legacy", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("legacy_date", sa.String(32), nullable=True),
|
||||
sa.Column("legacy_pdf_path", sa.String(1000), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
)
|
||||
op.create_index("idx_crm_quotations_customer", "crm_quotations", ["customer_id"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# crm_quotation_items
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"crm_quotation_items",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("quotation_id", sa.String(128),
|
||||
sa.ForeignKey("crm_quotations.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("product_id", sa.String(128), nullable=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("description_en", sa.Text(), nullable=True),
|
||||
sa.Column("description_gr", sa.Text(), nullable=True),
|
||||
sa.Column("unit_type", sa.String(32), nullable=False, server_default="pcs"),
|
||||
sa.Column("unit_cost", sa.Numeric(12, 4), nullable=False, server_default="0"),
|
||||
sa.Column("discount_percent", sa.Numeric(8, 4), nullable=False, server_default="0"),
|
||||
sa.Column("vat_percent", sa.Numeric(8, 4), nullable=False, server_default="24"),
|
||||
sa.Column("quantity", sa.Numeric(12, 4), nullable=False, server_default="1"),
|
||||
sa.Column("line_total", sa.Numeric(12, 2), nullable=False, server_default="0"),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
|
||||
)
|
||||
op.create_index("idx_crm_quotation_items_quotation", "crm_quotation_items",
|
||||
["quotation_id", "sort_order"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# melody_drafts
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"melody_drafts",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("status", sa.String(32), nullable=False, server_default="draft"),
|
||||
sa.Column("data", JSONB, nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("idx_melody_drafts_status", "melody_drafts", ["status"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# built_melodies
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"built_melodies",
|
||||
sa.Column("id", sa.String(128), primary_key=True),
|
||||
sa.Column("name", sa.String(500), nullable=False),
|
||||
sa.Column("pid", sa.String(128), nullable=False),
|
||||
sa.Column("steps", JSONB, nullable=False),
|
||||
sa.Column("binary_path", sa.String(1000), nullable=True),
|
||||
sa.Column("progmem_code", sa.Text(), nullable=True),
|
||||
sa.Column("assigned_melody_ids", JSONB, nullable=False, server_default="[]"),
|
||||
sa.Column("is_builtin", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# mfg_audit_log
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"mfg_audit_log",
|
||||
sa.Column("id", sa.BigInteger(), primary_key=True, autoincrement=True),
|
||||
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
sa.Column("admin_user", sa.String(256), nullable=False),
|
||||
sa.Column("action", sa.String(128), nullable=False),
|
||||
sa.Column("serial_number", sa.String(128), nullable=True),
|
||||
sa.Column("detail", sa.Text(), nullable=True),
|
||||
)
|
||||
op.create_index("idx_mfg_audit_time", "mfg_audit_log", ["timestamp"])
|
||||
op.create_index("idx_mfg_audit_action", "mfg_audit_log", ["action"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# device_alerts
|
||||
# ------------------------------------------------------------------ #
|
||||
op.create_table(
|
||||
"device_alerts",
|
||||
sa.Column("id", sa.BigInteger(), primary_key=True, autoincrement=True),
|
||||
sa.Column("device_serial", sa.String(128), nullable=False),
|
||||
sa.Column("subsystem", sa.String(128), nullable=False),
|
||||
sa.Column("state", sa.String(64), nullable=False),
|
||||
sa.Column("message", sa.Text(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now()),
|
||||
sa.UniqueConstraint("device_serial", "subsystem", name="uq_device_alerts_serial_subsystem"),
|
||||
)
|
||||
op.create_index("idx_device_alerts_serial", "device_alerts", ["device_serial"])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# commands (raw SQL — mirrors SQLite schema, no ORM model)
|
||||
# ------------------------------------------------------------------ #
|
||||
op.execute("""
|
||||
CREATE TABLE commands (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
device_serial TEXT NOT NULL,
|
||||
command_name TEXT NOT NULL,
|
||||
command_payload TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
response_payload TEXT,
|
||||
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
responded_at TIMESTAMPTZ
|
||||
)
|
||||
""")
|
||||
op.execute("CREATE INDEX idx_commands_serial_time ON commands(device_serial, sent_at DESC)")
|
||||
op.execute("CREATE INDEX idx_commands_status ON commands(status)")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# heartbeats (raw SQL — mirrors SQLite schema, no ORM model)
|
||||
# ------------------------------------------------------------------ #
|
||||
op.execute("""
|
||||
CREATE TABLE heartbeats (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
device_serial TEXT NOT NULL,
|
||||
device_id TEXT,
|
||||
firmware_version TEXT,
|
||||
ip_address TEXT,
|
||||
gateway TEXT,
|
||||
uptime_ms BIGINT,
|
||||
uptime_display TEXT,
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
""")
|
||||
op.execute("CREATE INDEX idx_heartbeats_serial_time ON heartbeats(device_serial, received_at DESC)")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# device_logs — partitioned by month on received_at
|
||||
# ------------------------------------------------------------------ #
|
||||
op.execute("""
|
||||
CREATE TABLE device_logs (
|
||||
id BIGSERIAL,
|
||||
device_serial TEXT NOT NULL,
|
||||
level TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
device_timestamp BIGINT,
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (id, received_at)
|
||||
) PARTITION BY RANGE (received_at)
|
||||
""")
|
||||
op.execute("""
|
||||
CREATE INDEX idx_device_logs_serial_time
|
||||
ON device_logs(device_serial, received_at DESC)
|
||||
""")
|
||||
op.execute("""
|
||||
CREATE INDEX idx_device_logs_level
|
||||
ON device_logs(level, received_at DESC)
|
||||
""")
|
||||
|
||||
# Create partitions: 2025-01 through 2026-06 (covers all existing data + near future)
|
||||
partitions = [
|
||||
("2025_01", "2025-01-01", "2025-02-01"),
|
||||
("2025_02", "2025-02-01", "2025-03-01"),
|
||||
("2025_03", "2025-03-01", "2025-04-01"),
|
||||
("2025_04", "2025-04-01", "2025-05-01"),
|
||||
("2025_05", "2025-05-01", "2025-06-01"),
|
||||
("2025_06", "2025-06-01", "2025-07-01"),
|
||||
("2025_07", "2025-07-01", "2025-08-01"),
|
||||
("2025_08", "2025-08-01", "2025-09-01"),
|
||||
("2025_09", "2025-09-01", "2025-10-01"),
|
||||
("2025_10", "2025-10-01", "2025-11-01"),
|
||||
("2025_11", "2025-11-01", "2025-12-01"),
|
||||
("2025_12", "2025-12-01", "2026-01-01"),
|
||||
("2026_01", "2026-01-01", "2026-02-01"),
|
||||
("2026_02", "2026-02-01", "2026-03-01"),
|
||||
("2026_03", "2026-03-01", "2026-04-01"),
|
||||
("2026_04", "2026-04-01", "2026-05-01"),
|
||||
("2026_05", "2026-05-01", "2026-06-01"),
|
||||
("2026_06", "2026-06-01", "2026-07-01"),
|
||||
]
|
||||
for suffix, start, end in partitions:
|
||||
op.execute(f"""
|
||||
CREATE TABLE device_logs_{suffix} PARTITION OF device_logs
|
||||
FOR VALUES FROM ('{start}') TO ('{end}')
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop in reverse dependency order
|
||||
op.execute("DROP TABLE IF EXISTS device_logs CASCADE") # drops all partitions too
|
||||
op.execute("DROP TABLE IF EXISTS heartbeats CASCADE")
|
||||
op.execute("DROP TABLE IF EXISTS commands CASCADE")
|
||||
op.drop_table("device_alerts")
|
||||
op.drop_table("mfg_audit_log")
|
||||
op.drop_table("built_melodies")
|
||||
op.drop_table("melody_drafts")
|
||||
op.drop_table("crm_quotation_items")
|
||||
op.drop_table("crm_quotations")
|
||||
op.drop_table("crm_sync_state")
|
||||
op.drop_table("crm_media")
|
||||
op.drop_table("crm_comms_log")
|
||||
op.drop_table("crm_orders")
|
||||
op.drop_table("crm_customers")
|
||||
op.drop_table("crm_products")
|
||||
op.drop_table("public_features")
|
||||
op.drop_table("console_settings")
|
||||
op.drop_table("staff")
|
||||
op.drop_table("audit_log")
|
||||
op.drop_table("_migration_runs")
|
||||
Reference in New Issue
Block a user