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