@@ -63,13 +63,22 @@ class TableSpec:
6363 foreign_keys : list [ForeignKeySpec ] = field (default_factory = list )
6464
6565
66+ @dataclass
67+ class AlterTableSpec :
68+ """Specification for altering an existing table (adding columns)."""
69+
70+ name : str
71+ add_columns : list [ColumnSpec ]
72+
73+
6674@dataclass
6775class ServiceMigrationSpec :
6876 """Migration specification for a service."""
6977
7078 service_name : str
7179 tables : list [TableSpec ]
7280 description : str
81+ alter_tables : list [AlterTableSpec ] = field (default_factory = list )
7382
7483
7584# ============================================================================
@@ -90,7 +99,6 @@ class ServiceMigrationSpec:
9099 ColumnSpec (
91100 "is_verified" , "sa.Boolean()" , nullable = False , default = "False"
92101 ),
93- ColumnSpec ("role" , "sa.String()" , nullable = False , default = "'user'" ),
94102 ColumnSpec ("hashed_password" , "sa.String()" , nullable = False ),
95103 ColumnSpec ("last_login" , "sa.DateTime()" , nullable = True ),
96104 ColumnSpec ("created_at" , "sa.DateTime()" , nullable = False ),
@@ -101,6 +109,20 @@ class ServiceMigrationSpec:
101109 ],
102110)
103111
112+ AUTH_RBAC_MIGRATION = ServiceMigrationSpec (
113+ service_name = "auth_rbac" ,
114+ description = "RBAC role column for user table" ,
115+ tables = [],
116+ alter_tables = [
117+ AlterTableSpec (
118+ name = "user" ,
119+ add_columns = [
120+ ColumnSpec ("role" , "sa.String()" , nullable = False , default = "'user'" ),
121+ ],
122+ ),
123+ ],
124+ )
125+
104126ORG_MIGRATION = ServiceMigrationSpec (
105127 service_name = "auth_org" ,
106128 description = "Organization and membership tables" ,
@@ -418,6 +440,7 @@ class ServiceMigrationSpec:
418440# Registry of all service migrations
419441MIGRATION_SPECS : dict [str , ServiceMigrationSpec ] = {
420442 "auth" : AUTH_MIGRATION ,
443+ "auth_rbac" : AUTH_RBAC_MIGRATION ,
421444 "auth_org" : ORG_MIGRATION ,
422445 "ai" : AI_MIGRATION ,
423446 "ai_voice" : VOICE_MIGRATION ,
@@ -449,7 +472,7 @@ class ServiceMigrationSpec:
449472
450473
451474def upgrade() -> None:
452- """Create {{ service_name }} service tables. """
475+ """{{ upgrade_description }}"""
453476{% for table in tables %}
454477 # Create {{ table.name }} table
455478 op.create_table(
@@ -472,10 +495,22 @@ def upgrade() -> None:
472495 op.create_index(op.f('{{ index.name }}'), '{{ table.name }}', {{ index.columns }}{% if index.unique %}, unique=True{% endif %})
473496{% endfor %}
474497
498+ {% endfor %}
499+ {% for alter in alter_tables %}
500+ # Alter {{ alter.name }} table
501+ {% for column in alter.add_columns %}
502+ op.add_column('{{ alter.name }}', sa.Column('{{ column.name }}', {{ column.type }}, nullable={{ column.nullable }}{% if column.server_default %}, server_default={{ column.server_default }}{% endif %}))
503+ {% endfor %}
504+
475505{% endfor %}
476506
477507def downgrade() -> None:
478- """Drop {{ service_name }} service tables."""
508+ """Reverse {{ service_name }} migration."""
509+ {% for alter in alter_tables|reverse %}
510+ {% for column in alter.add_columns|reverse %}
511+ op.drop_column('{{ alter.name }}', '{{ column.name }}')
512+ {% endfor %}
513+ {% endfor %}
479514{% for table in tables|reverse %}
480515{% for index in table.indexes %}
481516 op.drop_index(op.f('{{ index.name }}'), table_name='{{ table.name }}')
@@ -604,14 +639,48 @@ def _render_migration(
604639 }
605640 )
606641
642+ # Prepare alter table data for template
643+ alter_tables_data = []
644+ for alter in spec .alter_tables :
645+ cols = []
646+ for col in alter .add_columns :
647+ # For add_column, server_default needs sa.text() wrapper
648+ server_default = None
649+ if col .default is not None :
650+ server_default = f'sa.text("{ col .default } ")'
651+ cols .append (
652+ {
653+ "name" : col .name ,
654+ "type" : col .type ,
655+ "nullable" : col .nullable ,
656+ "server_default" : server_default ,
657+ }
658+ )
659+ alter_tables_data .append (
660+ {
661+ "name" : alter .name ,
662+ "add_columns" : cols ,
663+ }
664+ )
665+
666+ # Build upgrade description
667+ if spec .tables and spec .alter_tables :
668+ upgrade_description = f"Create and alter { spec .service_name } service tables."
669+ elif spec .alter_tables :
670+ upgrade_description = f"Add { spec .service_name } columns."
671+ else :
672+ upgrade_description = f"Create { spec .service_name } service tables."
673+
607674 return template .render (
608675 description = spec .description ,
609676 service_name = spec .service_name ,
677+ upgrade_description = upgrade_description ,
610678 revision = revision ,
611679 down_revision = down_revision ,
612680 down_revision_repr = f"'{ down_revision } '" if down_revision else "None" ,
613681 create_date = datetime .now (UTC ).strftime ("%Y-%m-%d %H:%M:%S.%f" ),
614682 tables = tables_data ,
683+ alter_tables = alter_tables_data ,
615684 )
616685
617686
@@ -693,14 +762,24 @@ def get_services_needing_migrations(context: dict[str, Any]) -> list[str]:
693762 """
694763 services = []
695764
696- # Auth service
765+ # Auth service (base user table)
697766 include_auth = context .get ("include_auth" )
698767 if include_auth == "yes" or include_auth is True :
699768 services .append ("auth" )
700769
770+ # Auth RBAC columns (rbac or org level)
771+ include_auth_rbac = context .get ("include_auth_rbac" )
772+ auth_level = context .get ("auth_level" )
773+ rbac_enabled = (
774+ include_auth_rbac == "yes"
775+ or include_auth_rbac is True
776+ or (isinstance (auth_level , str ) and auth_level .lower () in ("rbac" , "org" ))
777+ )
778+ if (include_auth == "yes" or include_auth is True ) and rbac_enabled :
779+ services .append ("auth_rbac" )
780+
701781 # Auth org tables (only with org-level auth)
702782 include_auth_org = context .get ("include_auth_org" )
703- auth_level = context .get ("auth_level" )
704783 org_enabled = (
705784 include_auth_org == "yes"
706785 or include_auth_org is True
0 commit comments