Jun 26 MT-0 phase
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""control0001 — initial control-plane schema
|
||||
|
||||
Creates the control-plane registry tables (MULTI_TENANT_PLAN.md §3):
|
||||
plans, plan_features, superadmins, tenants, tenant_domains,
|
||||
provisioning_jobs, tenant_audit.
|
||||
|
||||
Raw MySQL DDL with INFORMATION_SCHEMA existence checks so the migration is
|
||||
safe to re-run (CLAUDE.md Rule 14). InnoDB + utf8mb4 throughout. Targets the
|
||||
CONTROL database only — never a tenant DB.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = 'control0001_init'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _table_exists(bind, table: str) -> bool:
|
||||
result = bind.execute(sa.text(
|
||||
"SELECT COUNT(*) FROM information_schema.tables "
|
||||
"WHERE table_schema = DATABASE() AND table_name = :t"
|
||||
), {'t': table})
|
||||
return result.scalar() > 0
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
|
||||
if not _table_exists(bind, 'plans'):
|
||||
op.execute(sa.text("""
|
||||
CREATE TABLE plans (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(32) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
max_users INT NULL,
|
||||
max_facilities INT NULL,
|
||||
max_inspections_month INT NULL,
|
||||
max_issues_month INT NULL,
|
||||
allow_custom_domain TINYINT(1) NOT NULL DEFAULT 0,
|
||||
allow_mobile_api TINYINT(1) NOT NULL DEFAULT 0,
|
||||
allow_scheduled_reports TINYINT(1) NOT NULL DEFAULT 0,
|
||||
allow_branding TINYINT(1) NOT NULL DEFAULT 0,
|
||||
price_cents INT NULL,
|
||||
billing_period VARCHAR(16) NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
UNIQUE KEY uq_plans_code (code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
|
||||
if not _table_exists(bind, 'plan_features'):
|
||||
op.execute(sa.text("""
|
||||
CREATE TABLE plan_features (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
plan_id INT NOT NULL,
|
||||
feature_key VARCHAR(64) NOT NULL,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
CONSTRAINT fk_plan_features_plan FOREIGN KEY (plan_id)
|
||||
REFERENCES plans(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uq_plan_feature (plan_id, feature_key),
|
||||
INDEX ix_plan_features_plan (plan_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
|
||||
if not _table_exists(bind, 'superadmins'):
|
||||
op.execute(sa.text("""
|
||||
CREATE TABLE superadmins (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
UNIQUE KEY uq_superadmins_username (username),
|
||||
UNIQUE KEY uq_superadmins_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
|
||||
if not _table_exists(bind, 'tenants'):
|
||||
op.execute(sa.text("""
|
||||
CREATE TABLE tenants (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(63) NOT NULL,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
plan_id INT NOT NULL,
|
||||
status ENUM('provisioning','active','suspended','deleted')
|
||||
NOT NULL DEFAULT 'provisioning',
|
||||
db_host VARCHAR(255) NOT NULL,
|
||||
db_port INT NOT NULL DEFAULT 3306,
|
||||
db_name VARCHAR(64) NOT NULL,
|
||||
db_user VARCHAR(64) NOT NULL,
|
||||
db_password_enc TEXT NULL,
|
||||
alembic_head VARCHAR(64) NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
suspended_at DATETIME NULL,
|
||||
notes TEXT NULL,
|
||||
CONSTRAINT fk_tenants_plan FOREIGN KEY (plan_id)
|
||||
REFERENCES plans(id),
|
||||
UNIQUE KEY uq_tenants_slug (slug),
|
||||
UNIQUE KEY uq_tenants_db_name (db_name),
|
||||
INDEX ix_tenants_plan (plan_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
|
||||
if not _table_exists(bind, 'tenant_domains'):
|
||||
op.execute(sa.text("""
|
||||
CREATE TABLE tenant_domains (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_id INT NOT NULL,
|
||||
domain VARCHAR(255) NOT NULL,
|
||||
kind ENUM('subdomain','custom') NOT NULL,
|
||||
is_primary TINYINT(1) NOT NULL DEFAULT 0,
|
||||
verified TINYINT(1) NOT NULL DEFAULT 0,
|
||||
verification_token VARCHAR(64) NULL,
|
||||
tls_status ENUM('pending','active','failed')
|
||||
NOT NULL DEFAULT 'pending',
|
||||
created_at DATETIME NOT NULL,
|
||||
CONSTRAINT fk_tenant_domains_tenant FOREIGN KEY (tenant_id)
|
||||
REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uq_tenant_domains_domain (domain),
|
||||
INDEX ix_tenant_domains_tenant (tenant_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
|
||||
if not _table_exists(bind, 'provisioning_jobs'):
|
||||
op.execute(sa.text("""
|
||||
CREATE TABLE provisioning_jobs (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_id INT NULL,
|
||||
action ENUM('create_db','migrate','seed','suspend','resume','delete')
|
||||
NOT NULL,
|
||||
status ENUM('queued','running','ok','failed')
|
||||
NOT NULL DEFAULT 'queued',
|
||||
log TEXT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
finished_at DATETIME NULL,
|
||||
CONSTRAINT fk_provisioning_jobs_tenant FOREIGN KEY (tenant_id)
|
||||
REFERENCES tenants(id) ON DELETE SET NULL,
|
||||
INDEX ix_provisioning_jobs_tenant (tenant_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
|
||||
if not _table_exists(bind, 'tenant_audit'):
|
||||
op.execute(sa.text("""
|
||||
CREATE TABLE tenant_audit (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
superadmin_id INT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
tenant_id INT NULL,
|
||||
details TEXT NULL,
|
||||
ip_address VARCHAR(45) NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
CONSTRAINT fk_tenant_audit_superadmin FOREIGN KEY (superadmin_id)
|
||||
REFERENCES superadmins(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_tenant_audit_tenant FOREIGN KEY (tenant_id)
|
||||
REFERENCES tenants(id) ON DELETE SET NULL,
|
||||
INDEX ix_tenant_audit_tenant (tenant_id),
|
||||
INDEX ix_tenant_audit_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""))
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
for table in ('tenant_audit', 'provisioning_jobs', 'tenant_domains',
|
||||
'tenants', 'superadmins', 'plan_features', 'plans'):
|
||||
if _table_exists(bind, table):
|
||||
op.execute(sa.text(f'DROP TABLE {table}'))
|
||||
Reference in New Issue
Block a user