diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..51765ea --- /dev/null +++ b/alembic.ini @@ -0,0 +1,40 @@ +[alembic] +# A comma separated list of paths to migration scripts to be run. By default, Alembic will look for versions +# in the "versions" directory within the "migrations" folder. You can adjust this path based on your setup. +script_location = migrations + +# The database URL to use for your database connections. You can also use environment variables here. +# You should replace this with your actual database URL, for example: +# sql_connection_url = postgresql://username:password@localhost/dbname +sqlalchemy.url = sqlite:///./test.db + +# If you're using a database like PostgreSQL, it might look like: +# sqlalchemy.url = postgresql://username:password@localhost/dbname + +# Enables logging of database migrations to stdout (useful for debugging) +# logging config for Alembic +log_file_config = true + +# Uncomment this if you want to use a logging config (e.g., to save logs to a file). +# loggers = ['sqlalchemy.engine'] + +# Uncomment this if you'd like to run migrations in "offline" mode, where no live connection is made to the DB +# and instead, the migrations are written to a file. +# offline = true + +# You can also set this if you want the migrations to apply automatically in your production environment. +# Uncomment below to auto-generate migrations. +# autogenerate = true + +# Add any additional configurations for Alembic. Below is an example of logging setup: +loggers = ['root', 'sqlalchemy', 'alembic'] +databases = storage_db, auth_db + +[DEFAULT] +script_location = migrations + +[storage_db] +version_locations = ./migrations/versions/storage + +[auth_db] +version_locations = ./migrations/versions/auth diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..a937331 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,97 @@ +import os +import sys + +from alembic import context +from application.process import process +from sqlalchemy import engine_from_config, pool +from sqlmodel import SQLModel + +# Add the `xcap` directory to the Python path so we can import our modules +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) + +from xcap.configuration import DatabaseConfig as XCAPDatabaseConfig +from xcap.db.manager import Base +from xcap.db.models import XCAP, Subscriber, Watcher + +process.configuration.local_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) + + +class DatabaseConfig(XCAPDatabaseConfig): + authentication_db_uri = '' + storage_db_uri = '' + + +config = context.config + +target_metadata = Base.metadata + +db_name = config.config_ini_section + + +def include_object(object, name, type_, reflected, compare_to): + model_class = next( + (cls for cls in SQLModel.__subclasses__() if getattr(cls, "__tablename__", None) == name), + None + ) + model_database = getattr(model_class, "__database__", None) + + if type_ == 'foreign_key_constraint' and compare_to and ( + compare_to.elements[0].target_fullname == db_name + '.' + + object.elements[0].target_fullname or + db_name + '.' + compare_to.elements[0].target_fullname == object.elements[ + 0].target_fullname): + return False + if type_ == 'table': + if model_database == db_name or model_database is None: + return True + elif model_database == db_name or model_database is None: + return True + else: + return False + + +def run_migrations_online(): + """Run migrations in 'online' mode.""" + if db_name == 'auth_db': + config.set_main_option("sqlalchemy.url", DatabaseConfig.authentication_db_uri) + else: + config.set_main_option("sqlalchemy.url", DatabaseConfig.storage_db_uri) + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool + ) + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + include_object=include_object + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_offline(): + """Run migrations in 'offline' mode.""" + if db_name == 'auth_db': + url = DatabaseConfig.authentication_db_uri + else: + url = DatabaseConfig.storage_db_uri + + context.configure( + url=url, + target_metadata=target_metadata, + include_object=include_object, + literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..55df286 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${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 identifiers, used by Alembic. +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"} diff --git a/migrations/versions/auth/ad8f7251778a_initial_migration.py b/migrations/versions/auth/ad8f7251778a_initial_migration.py new file mode 100644 index 0000000..4151d80 --- /dev/null +++ b/migrations/versions/auth/ad8f7251778a_initial_migration.py @@ -0,0 +1,35 @@ +"""initial_migration + +Revision ID: ad8f7251778a +Revises: +Create Date: 2025-03-11 16:45:21.258669 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers, used by Alembic. +revision = 'ad8f7251778a' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('subscriber', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=64), nullable=True), + sa.Column('domain', sa.String(length=64), nullable=True), + sa.Column('password', sa.String(length=255), nullable=True), + sa.Column('ha1', sa.String(length=64), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('subscriber') + # ### end Alembic commands ### diff --git a/migrations/versions/storage/9662e80c6ac2_initial_migration.py b/migrations/versions/storage/9662e80c6ac2_initial_migration.py new file mode 100644 index 0000000..04a89a6 --- /dev/null +++ b/migrations/versions/storage/9662e80c6ac2_initial_migration.py @@ -0,0 +1,58 @@ +"""initial_migration + +Revision ID: 9662e80c6ac2 +Revises: +Create Date: 2025-03-11 16:45:10.460029 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers, used by Alembic. +revision = '9662e80c6ac2' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('watchers', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('presentity_uri', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False), + sa.Column('watcher_username', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False), + sa.Column('watcher_domain', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False), + sa.Column('event', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False), + sa.Column('status', sa.Integer(), nullable=False), + sa.Column('reason', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True), + sa.Column('inserted_time', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('presentity_uri', 'watcher_username', 'watcher_domain', 'event', name='watcher_idx') + ) + op.create_table('xcap', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('subscriber_id', sa.Integer(), nullable=True), + sa.Column('username', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False), + sa.Column('domain', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False), + sa.Column('doc', sa.LargeBinary(), nullable=False), + sa.Column('doc_type', sa.Integer(), nullable=False), + sa.Column('etag', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=False), + sa.Column('source', sa.Integer(), nullable=False), + sa.Column('doc_uri', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False), + sa.Column('port', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username', 'domain', 'doc_type', 'doc_uri', name='account_doc_type_idx') + ) + op.create_index('source_idx', 'xcap', ['source'], unique=False) + op.create_index('xcap_subscriber_id_exists', 'xcap', ['subscriber_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('xcap_subscriber_id_exists', table_name='xcap') + op.drop_index('source_idx', table_name='xcap') + op.drop_table('xcap') + op.drop_table('watchers') + # ### end Alembic commands ###