feat: migration initiale — schémas todos/shopping/notes et toutes les tables
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
[alembic]
|
||||||
|
script_location = alembic
|
||||||
|
file_template = %%(rev)s_%%(slug)s
|
||||||
|
prepend_sys_path = .
|
||||||
|
sqlalchemy.url = postgresql+asyncpg://homehub:homehub@localhost:5432/homehub
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import asyncio
|
||||||
|
from logging.config import fileConfig
|
||||||
|
from sqlalchemy import pool
|
||||||
|
from sqlalchemy.engine import Connection
|
||||||
|
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||||
|
from alembic import context
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
config = context.config
|
||||||
|
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||||
|
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
from app.models import todos, shopping, notes # noqa: F401 — enregistre les modèles
|
||||||
|
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def do_run_migrations(connection: Connection) -> None:
|
||||||
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_async_migrations() -> None:
|
||||||
|
connectable = async_engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section, {}),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
async with connectable.connect() as connection:
|
||||||
|
await connection.run_sync(do_run_migrations)
|
||||||
|
await connectable.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
asyncio.run(run_async_migrations())
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""Schémas initiaux et toutes les tables
|
||||||
|
|
||||||
|
Revision ID: 001
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-05-24
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
revision = "001"
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute("CREATE SCHEMA IF NOT EXISTS todos")
|
||||||
|
op.execute("CREATE SCHEMA IF NOT EXISTS shopping")
|
||||||
|
op.execute("CREATE SCHEMA IF NOT EXISTS notes")
|
||||||
|
|
||||||
|
# ── todos.items ──────────────────────────────────────────────
|
||||||
|
op.create_table(
|
||||||
|
"items",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("title", sa.String(255), nullable=False),
|
||||||
|
sa.Column("body", sa.Text),
|
||||||
|
sa.Column("url", sa.Text),
|
||||||
|
sa.Column("domain", sa.String(50)),
|
||||||
|
sa.Column("category", sa.String(50)),
|
||||||
|
sa.Column("tags", postgresql.ARRAY(sa.String(50)), server_default=sa.text("'{}'::varchar[]")),
|
||||||
|
sa.Column("status", sa.String(20), server_default="pending"),
|
||||||
|
sa.Column("priority", sa.String(10), server_default="medium"),
|
||||||
|
sa.Column("due_date", sa.TIMESTAMP(timezone=True)),
|
||||||
|
sa.Column("postponed_count", sa.Integer, server_default="0"),
|
||||||
|
sa.Column("created_at", sa.TIMESTAMP(timezone=True), server_default=sa.text("now()")),
|
||||||
|
sa.Column("updated_at", sa.TIMESTAMP(timezone=True)),
|
||||||
|
sa.Column("owner_id", postgresql.UUID(as_uuid=True)),
|
||||||
|
schema="todos",
|
||||||
|
)
|
||||||
|
op.create_index("idx_todos_tags", "items", [sa.text("tags")], schema="todos", postgresql_using="gin")
|
||||||
|
|
||||||
|
# ── shopping.products ────────────────────────────────────────
|
||||||
|
op.create_table(
|
||||||
|
"products",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("name", sa.String(150), nullable=False),
|
||||||
|
sa.Column("brand", sa.String(100)),
|
||||||
|
sa.Column("category", sa.String(50)),
|
||||||
|
sa.Column("image_path", sa.String(255)),
|
||||||
|
sa.Column("thumbnail_path", sa.String(255)),
|
||||||
|
sa.Column("default_unit", sa.String(20)),
|
||||||
|
sa.Column("barcode", sa.String(50)),
|
||||||
|
sa.Column("frequency_score", sa.Integer, server_default="0"),
|
||||||
|
sa.Column("owner_id", postgresql.UUID(as_uuid=True)),
|
||||||
|
sa.Column("created_at", sa.TIMESTAMP(timezone=True), server_default=sa.text("now()")),
|
||||||
|
schema="shopping",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── shopping.stores ──────────────────────────────────────────
|
||||||
|
op.create_table(
|
||||||
|
"stores",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("name", sa.String(100), nullable=False),
|
||||||
|
sa.Column("location", sa.Text),
|
||||||
|
sa.Column("owner_id", postgresql.UUID(as_uuid=True)),
|
||||||
|
schema="shopping",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── shopping.price_history ───────────────────────────────────
|
||||||
|
op.create_table(
|
||||||
|
"price_history",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("product_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("shopping.products.id", ondelete="CASCADE"), nullable=False),
|
||||||
|
sa.Column("store_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("shopping.stores.id", ondelete="SET NULL")),
|
||||||
|
sa.Column("price", sa.Numeric(8, 2), nullable=False),
|
||||||
|
sa.Column("unit", sa.String(20)),
|
||||||
|
sa.Column("quantity", sa.Numeric(8, 3)),
|
||||||
|
sa.Column("source", sa.String(20), server_default="manual"),
|
||||||
|
sa.Column("recorded_at", sa.TIMESTAMP(timezone=True), server_default=sa.text("now()")),
|
||||||
|
schema="shopping",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── shopping.lists ───────────────────────────────────────────
|
||||||
|
op.create_table(
|
||||||
|
"lists",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("name", sa.String(100)),
|
||||||
|
sa.Column("store_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("shopping.stores.id", ondelete="SET NULL")),
|
||||||
|
sa.Column("week_date", sa.Date),
|
||||||
|
sa.Column("status", sa.String(20), server_default="draft"),
|
||||||
|
sa.Column("owner_id", postgresql.UUID(as_uuid=True)),
|
||||||
|
sa.Column("created_at", sa.TIMESTAMP(timezone=True), server_default=sa.text("now()")),
|
||||||
|
schema="shopping",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── shopping.list_items ──────────────────────────────────────
|
||||||
|
op.create_table(
|
||||||
|
"list_items",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("list_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("shopping.lists.id", ondelete="CASCADE"), nullable=False),
|
||||||
|
sa.Column("product_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("shopping.products.id", ondelete="SET NULL")),
|
||||||
|
sa.Column("custom_name", sa.String(150)),
|
||||||
|
sa.Column("quantity", sa.Numeric(8, 3)),
|
||||||
|
sa.Column("unit", sa.String(20)),
|
||||||
|
sa.Column("is_checked", sa.Boolean, server_default="false"),
|
||||||
|
sa.Column("price_recorded", sa.Numeric(8, 2)),
|
||||||
|
sa.Column("carried_over", sa.Boolean, server_default="false"),
|
||||||
|
sa.Column("sort_order", sa.Integer),
|
||||||
|
schema="shopping",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── notes.items ──────────────────────────────────────────────
|
||||||
|
op.create_table(
|
||||||
|
"items",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("title", sa.String(255)),
|
||||||
|
sa.Column("content", sa.Text, nullable=False),
|
||||||
|
sa.Column("category", sa.String(50)),
|
||||||
|
sa.Column("tags", postgresql.ARRAY(sa.String(50)), server_default=sa.text("'{}'::varchar[]")),
|
||||||
|
sa.Column("gps_lat", sa.Numeric(10, 7)),
|
||||||
|
sa.Column("gps_lon", sa.Numeric(10, 7)),
|
||||||
|
sa.Column("metadata", postgresql.JSONB),
|
||||||
|
sa.Column("created_at", sa.TIMESTAMP(timezone=True), server_default=sa.text("now()")),
|
||||||
|
sa.Column("owner_id", postgresql.UUID(as_uuid=True)),
|
||||||
|
schema="notes",
|
||||||
|
)
|
||||||
|
op.create_index("idx_notes_tags", "items", [sa.text("tags")], schema="notes", postgresql_using="gin")
|
||||||
|
op.create_index(
|
||||||
|
"idx_notes_fts",
|
||||||
|
"items",
|
||||||
|
[sa.text("to_tsvector('french', coalesce(title,'') || ' ' || content)")],
|
||||||
|
schema="notes",
|
||||||
|
postgresql_using="gin",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── notes.attachments ────────────────────────────────────────
|
||||||
|
op.create_table(
|
||||||
|
"attachments",
|
||||||
|
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("note_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("notes.items.id", ondelete="CASCADE"), nullable=False),
|
||||||
|
sa.Column("file_path", sa.String(255)),
|
||||||
|
sa.Column("thumbnail_path", sa.String(255)),
|
||||||
|
sa.Column("file_type", sa.String(20)),
|
||||||
|
sa.Column("original_name", sa.String(255)),
|
||||||
|
sa.Column("created_at", sa.TIMESTAMP(timezone=True), server_default=sa.text("now()")),
|
||||||
|
schema="notes",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("attachments", schema="notes")
|
||||||
|
op.drop_table("items", schema="notes")
|
||||||
|
op.drop_table("list_items", schema="shopping")
|
||||||
|
op.drop_table("lists", schema="shopping")
|
||||||
|
op.drop_table("price_history", schema="shopping")
|
||||||
|
op.drop_table("stores", schema="shopping")
|
||||||
|
op.drop_table("products", schema="shopping")
|
||||||
|
op.drop_table("items", schema="todos")
|
||||||
|
op.execute("DROP SCHEMA IF EXISTS notes CASCADE")
|
||||||
|
op.execute("DROP SCHEMA IF EXISTS shopping CASCADE")
|
||||||
|
op.execute("DROP SCHEMA IF EXISTS todos CASCADE")
|
||||||
Reference in New Issue
Block a user