v8.2.0: Add database migration framework and initial migrations

Introduce migration management tools using Peewee's migrate module. Add scripts for migration creation, application, rollback, and listing. Include initial batch of database migrations for schema and data changes.
master
Aidaho 2025-05-17 08:51:50 +03:00
parent d268fc4f40
commit b1bec4ec06
22 changed files with 847 additions and 3 deletions

View File

@ -20,11 +20,12 @@ scheduler.start()
jwt = JWTManager(app)
from app.modules.db.db_model import create_tables
from app.create_db import default_values, update_all
from app.create_db import default_values
from app.modules.db.migration_manager import migrate
create_tables()
default_values()
update_all()
migrate()
set_correct_owner('/var/lib/roxy-wi')

63
app/migrate.py Executable file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
import argparse
import sys
import os
# Add the parent directory to the path so we can import the app modules
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from app.modules.db.migration_manager import create_migrations_table, migrate, rollback, create_migration, list_migrations
def main():
parser = argparse.ArgumentParser(description='Database migration tool')
subparsers = parser.add_subparsers(dest='command', help='Command to run')
# Create migration command
create_parser = subparsers.add_parser('create', help='Create a new migration')
create_parser.add_argument('name', help='Name of the migration')
# Migrate command
subparsers.add_parser('migrate', help='Apply pending migrations')
# Rollback command
rollback_parser = subparsers.add_parser('rollback', help='Rollback migrations')
rollback_parser.add_argument('--steps', type=int, default=1, help='Number of migrations to roll back')
# Initialize command
subparsers.add_parser('init', help='Initialize the migrations table')
# list command
subparsers.add_parser('list', help='List all migrations and their status')
args = parser.parse_args()
if args.command == 'create':
filename = create_migration(args.name)
print(f"Created migration file: {filename}")
elif args.command == 'migrate':
success = migrate()
if success:
print("Migrations applied successfully")
else:
print("Error applying migrations")
sys.exit(1)
elif args.command == 'rollback':
success = rollback(args.steps)
if success:
print(f"Rolled back {args.steps} migration(s) successfully")
else:
print("Error rolling back migrations")
sys.exit(1)
elif args.command == 'list':
list_migrations()
elif args.command == 'init':
create_migrations_table()
print("Migrations table initialized")
else:
parser.print_help()
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -1,6 +1,6 @@
from datetime import datetime
from peewee import ForeignKeyField
from peewee import ForeignKeyField,CharField, DateTimeField, AutoField
from playhouse.migrate import *
from playhouse.shortcuts import ReconnectMixin
from playhouse.sqlite_ext import SqliteExtDatabase

61
app/modules/db/migrate.py Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
import argparse
import sys
import os
# Add the parent directory to the path so we can import the app modules
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))
from app.modules.db.migration_manager import create_migrations_table, migrate, rollback, create_migration, list_migrations
def main():
parser = argparse.ArgumentParser(description='Database migration tool')
subparsers = parser.add_subparsers(dest='command', help='Command to run')
# Create migration command
create_parser = subparsers.add_parser('create', help='Create a new migration')
create_parser.add_argument('name', help='Name of the migration')
# Migrate command
migrate_parser = subparsers.add_parser('migrate', help='Apply pending migrations')
# Rollback command
rollback_parser = subparsers.add_parser('rollback', help='Rollback migrations')
rollback_parser.add_argument('--steps', type=int, default=1, help='Number of migrations to roll back')
# Initialize command
init_parser = subparsers.add_parser('init', help='Initialize the migrations table')
# list command
subparsers.add_parser('list', help='List all migrations and their status')
args = parser.parse_args()
if args.command == 'create':
filename = create_migration(args.name)
print(f"Created migration file: {filename}")
elif args.command == 'migrate':
success = migrate()
if success:
print("Migrations applied successfully")
else:
print("Error applying migrations")
sys.exit(1)
elif args.command == 'rollback':
success = rollback(args.steps)
if success:
print(f"Rolled back {args.steps} migration(s) successfully")
else:
print("Error rolling back migrations")
sys.exit(1)
elif args.command == 'list':
list_migrations()
elif args.command == 'init':
create_migrations_table()
print("Migrations table initialized")
else:
parser.print_help()
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,176 @@
import os
import importlib
from datetime import datetime
from peewee import CharField, DateTimeField, AutoField
from playhouse.migrate import *
from app.modules.db.db_model import BaseModel, connect
# Define the Migration model to track applied migrations
class Migration(BaseModel):
id = AutoField()
name = CharField(unique=True)
applied_at = DateTimeField(default=datetime.now)
class Meta:
table_name = 'migrations'
def create_migrations_table():
"""Create the migrations table if it doesn't exist."""
conn = connect()
conn.create_tables([Migration], safe=True)
def get_migration_files():
"""Get all migration files from the migrations directory."""
migrations_dir = os.path.join(os.path.dirname(__file__), 'migrations')
migration_files = []
for filename in os.listdir(migrations_dir):
if filename.endswith('.py') and not filename.startswith('__'):
migration_name = filename[:-3] # Remove .py extension
migration_files.append(migration_name)
# Sort migrations by name (which should include a timestamp)
migration_files.sort()
return migration_files
def get_applied_migrations():
"""Get all migrations that have been applied."""
return [m.name for m in Migration.select(Migration.name)]
def apply_migration(migration_name):
"""Apply a single migration."""
try:
# Import the migration module
module = importlib.import_module(f'app.modules.db.migrations.{migration_name}')
# Apply the migration
print(f"Applying migration: {migration_name}")
module.up()
# Record the migration as applied
Migration.create(name=migration_name)
print(f"Migration applied: {migration_name}")
return True
except Exception as e:
print("error: applying migration {migration_name}: {str(e)}")
return False
def rollback_migration(migration_name):
"""Rollback a single migration."""
try:
# Import the migration module
module = importlib.import_module(f'app.modules.db.migrations.{migration_name}')
# Rollback the migration
print(f"Rolling back migration: {migration_name}")
module.down()
# Remove the migration record
Migration.delete().where(Migration.name == migration_name).execute()
print(f"Migration rolled back: {migration_name}")
return True
except Exception as e:
print("error: rolling back migration {migration_name}: {str(e)}")
return False
def migrate():
"""Apply all pending migrations."""
create_migrations_table()
# Get all migration files and applied migrations
migration_files = get_migration_files()
applied_migrations = get_applied_migrations()
# Determine which migrations need to be applied
pending_migrations = [m for m in migration_files if m not in applied_migrations]
if not pending_migrations:
print("No pending migrations to apply.")
return True
# Apply pending migrations
success = True
for migration_name in pending_migrations:
if not apply_migration(migration_name):
success = False
break
return success
def rollback(steps=1):
"""Rollback the specified number of migrations."""
create_migrations_table()
# Get applied migrations in reverse order (most recent first)
applied_migrations = Migration.select().order_by(Migration.applied_at.desc())
if not applied_migrations:
print("No migrations to roll back.")
return True
# Rollback the specified number of migrations
success = True
for i, migration in enumerate(applied_migrations):
if i >= steps:
break
if not rollback_migration(migration.name):
success = False
break
return success
def create_migration(name):
"""Create a new migration file."""
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
filename = f"{timestamp}_{name}.py"
filepath = os.path.join(os.path.dirname(__file__), 'migrations', filename)
template = """from playhouse.migrate import *
from app.modules.db.db_model import connect, mysql_enable
migrator = connect(get_migrator=1)
def up():
\"\"\"Apply the migration.\"\"\"
# Example:
# migrate(
# migrator.add_column('table_name', 'column_name', CharField(default='')),
# )
pass
def down():
\"\"\"Roll back the migration.\"\"\"
# Example:
# migrate(
# migrator.drop_column('table_name', 'column_name'),
# )
pass
"""
with open(filepath, 'w') as f:
f.write(template)
print(f"Created migration file: {filename}")
return filename
def list_migrations() -> None:
"""
List all migrations and their status.
"""
# Get all migration files
migration_files = get_migration_files()
# Get applied migrations
applied_migrations = get_applied_migrations()
# Print migrations
print("Migrations:")
for filename in migration_files:
migration_name = filename.replace('.py', '')
status = "Applied" if migration_name in applied_migrations else "Pending"
print(f" {migration_name}: {status}")

View File

@ -0,0 +1,19 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, Version
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# Insert version record with version '1.0'
try:
Version.insert(version='1.0').execute()
print("Inserted version record with version '1.0'")
except Exception as e:
print(f"Error inserting version record: {e}")
def down():
"""Roll back the migration."""
# This is the initial migration, so rolling back would mean dropping all tables
# This is dangerous and not recommended, so we'll just pass
pass

View File

@ -0,0 +1,24 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, User, UserGroups
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration updates user groups
# It inserts user_id and group_id from User table into UserGroups table
try:
UserGroups.insert_from(
User.select(User.user_id, User.group_id), fields=[UserGroups.user_id, UserGroups.user_group_id]
).on_conflict_ignore().execute()
except Exception as e:
if e.args[0] == 'duplicate column name: haproxy' or str(e) == '(1060, "Duplicate column name \'haproxy\'")':
print('Migration already applied')
else:
raise e
def down():
"""Roll back the migration."""
# This migration adds data, not schema changes, so rolling back would mean deleting data
# This is potentially dangerous, so we'll just pass
pass

View File

@ -0,0 +1,34 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, mysql_enable
from peewee import IntegerField, SQL
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration adds a use_src column to the ha_cluster_vips table
try:
if mysql_enable:
migrate(
migrator.add_column('ha_cluster_vips', 'use_src', IntegerField(default=0)),
)
else:
migrate(
migrator.add_column('ha_cluster_vips', 'use_src', IntegerField(constraints=[SQL('DEFAULT 0')])),
)
except Exception as e:
if e.args[0] == 'duplicate column name: use_src' or str(e) == '(1060, "Duplicate column name \'use_src\'")':
print('Column already exists')
else:
raise e
def down():
"""Roll back the migration."""
# This migration removes the use_src column from the ha_cluster_vips table
try:
migrate(
migrator.drop_column('ha_cluster_vips', 'use_src'),
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,32 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration renames columns in the backups table
try:
migrate(
migrator.rename_column('backups', 'cred', 'cred_id'),
migrator.rename_column('backups', 'backup_type', 'type'),
)
except Exception as e:
if e.args[0] == 'no such column: "cred"' or str(e) == '(1060, no such column: "cred")':
print("Columns already renamed")
elif e.args[0] == "'bool' object has no attribute 'sql'":
print("Columns already renamed")
else:
raise e
def down():
"""Roll back the migration."""
# This migration renames columns back to their original names
try:
migrate(
migrator.rename_column('backups', 'cred_id', 'cred'),
migrator.rename_column('backups', 'type', 'backup_type'),
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,66 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration renames multiple columns across different tables
try:
migrate(
migrator.rename_column('telegram', 'groups', 'group_id'),
migrator.rename_column('slack', 'groups', 'group_id'),
migrator.rename_column('mattermost', 'groups', 'group_id'),
migrator.rename_column('pd', 'groups', 'group_id'),
migrator.rename_column('servers', 'groups', 'group_id'),
migrator.rename_column('udp_balancers', 'desc', 'description'),
migrator.rename_column('ha_clusters', 'desc', 'description'),
migrator.rename_column('cred', 'enable', 'key_enabled'),
migrator.rename_column('cred', 'groups', 'group_id'),
migrator.rename_column('servers', 'desc', 'description'),
migrator.rename_column('servers', 'active', 'haproxy_active'),
migrator.rename_column('servers', 'metrics', 'haproxy_metrics'),
migrator.rename_column('servers', 'alert', 'haproxy_alert'),
migrator.rename_column('servers', 'cred', 'cred_id'),
migrator.rename_column('servers', 'enable', 'enabled'),
migrator.rename_column('servers', 'groups', 'group_id'),
migrator.rename_column('user', 'activeuser', 'enabled'),
migrator.rename_column('user', 'groups', 'group_id'),
migrator.rename_column('user', 'role', 'role_id'),
)
except Exception as e:
if e.args[0] == 'no such column: "groups"' or str(e) == '(1060, no such column: "groups")':
print("Columns already renamed")
elif e.args[0] == "'bool' object has no attribute 'sql'":
print("Columns already renamed")
else:
raise e
def down():
"""Roll back the migration."""
# This migration renames columns back to their original names
try:
migrate(
migrator.rename_column('telegram', 'group_id', 'groups'),
migrator.rename_column('slack', 'group_id', 'groups'),
migrator.rename_column('mattermost', 'group_id', 'groups'),
migrator.rename_column('pd', 'group_id', 'groups'),
migrator.rename_column('servers', 'group_id', 'groups'),
migrator.rename_column('udp_balancers', 'description', 'desc'),
migrator.rename_column('ha_clusters', 'description', 'desc'),
migrator.rename_column('cred', 'key_enabled', 'enable'),
migrator.rename_column('cred', 'group_id', 'groups'),
migrator.rename_column('servers', 'description', 'desc'),
migrator.rename_column('servers', 'haproxy_active', 'active'),
migrator.rename_column('servers', 'haproxy_metrics', 'metrics'),
migrator.rename_column('servers', 'haproxy_alert', 'alert'),
migrator.rename_column('servers', 'cred_id', 'cred'),
migrator.rename_column('servers', 'enabled', 'enable'),
migrator.rename_column('servers', 'group_id', 'groups'),
migrator.rename_column('user', 'enabled', 'activeuser'),
migrator.rename_column('user', 'group_id', 'groups'),
migrator.rename_column('user', 'role_id', 'role'),
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,34 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, mysql_enable
from peewee import IntegerField, SQL
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration adds a shared column to the cred table
try:
if mysql_enable:
migrate(
migrator.add_column('cred', 'shared', IntegerField(default=0)),
)
else:
migrate(
migrator.add_column('cred', 'shared', IntegerField(constraints=[SQL('DEFAULT 0')])),
)
except Exception as e:
if e.args[0] == 'duplicate column name: shared' or str(e) == '(1060, "Duplicate column name \'shared\'")':
print('Column already exists')
else:
raise e
def down():
"""Roll back the migration."""
# This migration removes the shared column from the cred table
try:
migrate(
migrator.drop_column('cred', 'shared'),
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,37 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, RoxyTool
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration deletes rows from the RoxyTool table
try:
RoxyTool.delete().where(RoxyTool.name == 'prometheus').execute()
RoxyTool.delete().where(RoxyTool.name == 'grafana-server').execute()
except Exception as e:
print(f"Error applying migration: {str(e)}")
raise e
def down():
"""Roll back the migration."""
# This migration adds back the deleted rows to the RoxyTool table
try:
RoxyTool.insert(
name='prometheus',
current_version='1.0',
new_version='1.0',
is_roxy=0,
desc='Prometheus monitoring system'
).on_conflict_ignore().execute()
RoxyTool.insert(
name='grafana-server',
current_version='1.0',
new_version='1.0',
is_roxy=0,
desc='Grafana visualization tool'
).on_conflict_ignore().execute()
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,30 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration renames the server column to server_id in the backups table
try:
migrate(
migrator.rename_column('backups', 'server', 'server_id')
)
except Exception as e:
if e.args[0] == 'no such column: "server"' or str(e) == '(1060, no such column: "server")':
print("Column already renamed")
elif e.args[0] == "'bool' object has no attribute 'sql'":
print("Column already renamed")
else:
raise e
def down():
"""Roll back the migration."""
# This migration renames the server_id column back to server in the backups table
try:
migrate(
migrator.rename_column('backups', 'server_id', 'server')
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,30 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration renames the server column to server_id in the s3_backups table
try:
migrate(
migrator.rename_column('s3_backups', 'server', 'server_id')
)
except Exception as e:
if e.args[0] == 'no such column: "server"' or str(e) == '(1060, no such column: "server")':
print("Column already renamed")
elif e.args[0] == "'bool' object has no attribute 'sql'":
print("Column already renamed")
else:
raise e
def down():
"""Roll back the migration."""
# This migration renames the server_id column back to server in the s3_backups table
try:
migrate(
migrator.rename_column('s3_backups', 'server_id', 'server')
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,30 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration renames the rhost column to rserver in the backups table
try:
migrate(
migrator.rename_column('backups', 'rhost', 'rserver')
)
except Exception as e:
if e.args[0] == 'no such column: "rhost"' or str(e) == '(1060, no such column: "rhost")':
print("Column already renamed")
elif e.args[0] == "'bool' object has no attribute 'sql'":
print("Column already renamed")
else:
raise e
def down():
"""Roll back the migration."""
# This migration renames the rserver column back to rhost in the backups table
try:
migrate(
migrator.rename_column('backups', 'rserver', 'rhost')
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,30 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration renames the period column to time in the git_setting table
try:
migrate(
migrator.rename_column('git_setting', 'period', 'time')
)
except Exception as e:
if e.args[0] == 'no such column: "period"' or str(e) == '(1060, no such column: "period")':
print("Column already renamed")
elif e.args[0] == "'bool' object has no attribute 'sql'":
print("Column already renamed")
else:
raise e
def down():
"""Roll back the migration."""
# This migration renames the time column back to period in the git_setting table
try:
migrate(
migrator.rename_column('git_setting', 'time', 'period')
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,30 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration renames the group column to group_id in the settings table
try:
migrate(
migrator.rename_column('settings', 'group', 'group_id'),
)
except Exception as e:
if e.args[0] == 'no such column: "group"' or 'column "group" does not exist' in str(e) or str(e) == '(1060, no such column: "group")':
print("Column already renamed")
elif e.args[0] == "'bool' object has no attribute 'sql'":
print("Column already renamed")
else:
raise e
def down():
"""Roll back the migration."""
# This migration renames the group_id column back to group in the settings table
try:
migrate(
migrator.rename_column('settings', 'group_id', 'group'),
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,29 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, TextField
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration adds a private_key column to the cred table
try:
migrate(
migrator.add_column('cred', 'private_key', TextField(null=True)),
)
except Exception as e:
if (e.args[0] == 'duplicate column name: private_key' or 'column "private_key" of relation "cred" already exists'
or str(e) == '(1060, "Duplicate column name \'private_key\'")'):
print('Column already exists')
else:
raise e
def down():
"""Roll back the migration."""
# This migration removes the private_key column from the cred table
try:
migrate(
migrator.drop_column('cred', 'private_key'),
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,35 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, mysql_enable
from peewee import IntegerField, SQL
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration adds an is_checker column to the udp_balancers table
try:
if mysql_enable:
migrate(
migrator.add_column('udp_balancers', 'is_checker', IntegerField(default=0)),
)
else:
migrate(
migrator.add_column('udp_balancers', 'is_checker', IntegerField(constraints=[SQL('DEFAULT 0')])),
)
except Exception as e:
if (e.args[0] == 'duplicate column name: is_checker' or 'column "is_checker" of relation "udp_balancers" already exists'
or str(e) == '(1060, "Duplicate column name \'is_checker\'")'):
print('Column already exists')
else:
raise e
def down():
"""Roll back the migration."""
# This migration removes the is_checker column from the udp_balancers table
try:
migrate(
migrator.drop_column('udp_balancers', 'is_checker'),
)
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,22 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, Version
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
# This migration updates the version in the database to 8.2.0
try:
Version.update(version='8.2.0').execute()
except Exception as e:
print(f"Error updating version: {str(e)}")
raise e
def down():
"""Roll back the migration."""
# This migration sets the version back to 8.1.6
try:
Version.update(version='8.1.6').execute()
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -0,0 +1,60 @@
# Database Migrations
This directory contains database migration files for Roxy-WI. Each migration file represents a specific change to the database schema.
## Migration System
The migration system is designed to track which migrations have been applied, apply migrations in the correct order, and support rollbacks. It uses Peewee's migration functionality to make changes to the database schema.
## Migration Files
Each migration file is a Python module with two functions:
- `up()`: Applies the migration
- `down()`: Rolls back the migration
Migration files are named with a timestamp prefix to ensure they are applied in the correct order.
## Using the Migration System
The migration system provides a command-line interface for managing migrations. The following commands are available:
### Initialize the Migrations Table
```bash
python app/migrate.py init
```
This command creates the migrations table in the database if it doesn't exist.
### Create a New Migration
```bash
python app/migrate.py create <migration_name>
```
This command creates a new migration file with the given name. The file will be created in the migrations directory with a timestamp prefix.
### Apply Pending Migrations
```bash
python app/migrate.py migrate
```
This command applies all pending migrations in the correct order.
### Roll Back Migrations
```bash
python app/migrate.py rollback [--steps <number>]
```
This command rolls back the specified number of migrations (default: 1) in reverse order.
## Automatic Migrations
The migration system is automatically run when the application starts up. This ensures that the database schema is always up to date.
## Converting from the Old Update System
The old update system used multiple update functions in create_db.py to handle database schema changes. These functions have been converted to migration files in this directory. The application now uses the migration system instead of the old update functions.

View File

@ -0,0 +1 @@
# This file makes the migrations directory a Python package