autoworkflow 3.1.5 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# Alembic Skill
|
|
2
|
+
|
|
3
|
+
## Initial Setup
|
|
4
|
+
\`\`\`bash
|
|
5
|
+
# Initialize Alembic in project
|
|
6
|
+
alembic init alembic
|
|
7
|
+
|
|
8
|
+
# Or for async SQLAlchemy
|
|
9
|
+
alembic init -t async alembic
|
|
10
|
+
\`\`\`
|
|
11
|
+
|
|
12
|
+
## Project Structure
|
|
13
|
+
\`\`\`
|
|
14
|
+
project/
|
|
15
|
+
├── alembic/
|
|
16
|
+
│ ├── versions/ # Migration files
|
|
17
|
+
│ ├── env.py # Migration environment
|
|
18
|
+
│ ├── script.py.mako # Migration template
|
|
19
|
+
│ └── README
|
|
20
|
+
├── alembic.ini # Alembic config
|
|
21
|
+
└── app/
|
|
22
|
+
└── models.py # SQLAlchemy models
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
## alembic.ini Configuration
|
|
26
|
+
\`\`\`ini
|
|
27
|
+
[alembic]
|
|
28
|
+
script_location = alembic
|
|
29
|
+
prepend_sys_path = .
|
|
30
|
+
version_path_separator = os
|
|
31
|
+
|
|
32
|
+
# Use environment variable for database URL (don't commit credentials!)
|
|
33
|
+
sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
34
|
+
|
|
35
|
+
[post_write_hooks]
|
|
36
|
+
hooks = black
|
|
37
|
+
black.type = console_scripts
|
|
38
|
+
black.entrypoint = black
|
|
39
|
+
black.options = -q
|
|
40
|
+
|
|
41
|
+
[loggers]
|
|
42
|
+
keys = root,sqlalchemy,alembic
|
|
43
|
+
|
|
44
|
+
[logger_alembic]
|
|
45
|
+
level = INFO
|
|
46
|
+
handlers =
|
|
47
|
+
qualname = alembic
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
## env.py - Standard Configuration
|
|
51
|
+
\`\`\`python
|
|
52
|
+
# alembic/env.py
|
|
53
|
+
import os
|
|
54
|
+
from logging.config import fileConfig
|
|
55
|
+
from sqlalchemy import engine_from_config, pool
|
|
56
|
+
from alembic import context
|
|
57
|
+
|
|
58
|
+
# Import your models' Base
|
|
59
|
+
from app.models import Base
|
|
60
|
+
|
|
61
|
+
config = context.config
|
|
62
|
+
|
|
63
|
+
# Override sqlalchemy.url from environment variable
|
|
64
|
+
config.set_main_option(
|
|
65
|
+
"sqlalchemy.url",
|
|
66
|
+
os.environ.get("DATABASE_URL", "postgresql://localhost/mydb")
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if config.config_file_name is not None:
|
|
70
|
+
fileConfig(config.config_file_name)
|
|
71
|
+
|
|
72
|
+
target_metadata = Base.metadata
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_migrations_offline() -> None:
|
|
76
|
+
"""Run migrations in 'offline' mode (SQL script generation)."""
|
|
77
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
78
|
+
context.configure(
|
|
79
|
+
url=url,
|
|
80
|
+
target_metadata=target_metadata,
|
|
81
|
+
literal_binds=True,
|
|
82
|
+
dialect_opts={"paramstyle": "named"},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
with context.begin_transaction():
|
|
86
|
+
context.run_migrations()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_migrations_online() -> None:
|
|
90
|
+
"""Run migrations in 'online' mode (direct database connection)."""
|
|
91
|
+
connectable = engine_from_config(
|
|
92
|
+
config.get_section(config.config_ini_section, {}),
|
|
93
|
+
prefix="sqlalchemy.",
|
|
94
|
+
poolclass=pool.NullPool,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with connectable.connect() as connection:
|
|
98
|
+
context.configure(
|
|
99
|
+
connection=connection,
|
|
100
|
+
target_metadata=target_metadata,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
with context.begin_transaction():
|
|
104
|
+
context.run_migrations()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if context.is_offline_mode():
|
|
108
|
+
run_migrations_offline()
|
|
109
|
+
else:
|
|
110
|
+
run_migrations_online()
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
## env.py - Async Configuration
|
|
114
|
+
\`\`\`python
|
|
115
|
+
# alembic/env.py (async version)
|
|
116
|
+
import asyncio
|
|
117
|
+
import os
|
|
118
|
+
from logging.config import fileConfig
|
|
119
|
+
from sqlalchemy import pool
|
|
120
|
+
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
121
|
+
from alembic import context
|
|
122
|
+
|
|
123
|
+
from app.models import Base
|
|
124
|
+
|
|
125
|
+
config = context.config
|
|
126
|
+
config.set_main_option(
|
|
127
|
+
"sqlalchemy.url",
|
|
128
|
+
os.environ.get("DATABASE_URL", "postgresql+asyncpg://localhost/mydb")
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if config.config_file_name is not None:
|
|
132
|
+
fileConfig(config.config_file_name)
|
|
133
|
+
|
|
134
|
+
target_metadata = Base.metadata
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def run_migrations_offline() -> None:
|
|
138
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
139
|
+
context.configure(
|
|
140
|
+
url=url,
|
|
141
|
+
target_metadata=target_metadata,
|
|
142
|
+
literal_binds=True,
|
|
143
|
+
dialect_opts={"paramstyle": "named"},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
with context.begin_transaction():
|
|
147
|
+
context.run_migrations()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def do_run_migrations(connection):
|
|
151
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
152
|
+
with context.begin_transaction():
|
|
153
|
+
context.run_migrations()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def run_async_migrations() -> None:
|
|
157
|
+
connectable = async_engine_from_config(
|
|
158
|
+
config.get_section(config.config_ini_section, {}),
|
|
159
|
+
prefix="sqlalchemy.",
|
|
160
|
+
poolclass=pool.NullPool,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
async with connectable.connect() as connection:
|
|
164
|
+
await connection.run_sync(do_run_migrations)
|
|
165
|
+
|
|
166
|
+
await connectable.dispose()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def run_migrations_online() -> None:
|
|
170
|
+
asyncio.run(run_async_migrations())
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if context.is_offline_mode():
|
|
174
|
+
run_migrations_offline()
|
|
175
|
+
else:
|
|
176
|
+
run_migrations_online()
|
|
177
|
+
\`\`\`
|
|
178
|
+
|
|
179
|
+
## Common Migration Commands
|
|
180
|
+
\`\`\`bash
|
|
181
|
+
# Create auto-generated migration
|
|
182
|
+
alembic revision --autogenerate -m "Add users table"
|
|
183
|
+
|
|
184
|
+
# Create empty migration (for data migrations)
|
|
185
|
+
alembic revision -m "Populate default categories"
|
|
186
|
+
|
|
187
|
+
# Apply all pending migrations
|
|
188
|
+
alembic upgrade head
|
|
189
|
+
|
|
190
|
+
# Apply specific number of migrations
|
|
191
|
+
alembic upgrade +2
|
|
192
|
+
|
|
193
|
+
# Rollback one migration
|
|
194
|
+
alembic downgrade -1
|
|
195
|
+
|
|
196
|
+
# Rollback to specific revision
|
|
197
|
+
alembic downgrade abc123
|
|
198
|
+
|
|
199
|
+
# Rollback all migrations
|
|
200
|
+
alembic downgrade base
|
|
201
|
+
|
|
202
|
+
# Show current revision
|
|
203
|
+
alembic current
|
|
204
|
+
|
|
205
|
+
# Show migration history
|
|
206
|
+
alembic history --verbose
|
|
207
|
+
|
|
208
|
+
# Show pending migrations
|
|
209
|
+
alembic history -r current:head
|
|
210
|
+
|
|
211
|
+
# Generate SQL instead of applying
|
|
212
|
+
alembic upgrade head --sql > migration.sql
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
## Schema Migration Example
|
|
216
|
+
\`\`\`python
|
|
217
|
+
"""Add users table
|
|
218
|
+
|
|
219
|
+
Revision ID: abc123
|
|
220
|
+
Revises:
|
|
221
|
+
Create Date: 2024-01-15 10:00:00.000000
|
|
222
|
+
"""
|
|
223
|
+
from typing import Sequence, Union
|
|
224
|
+
from alembic import op
|
|
225
|
+
import sqlalchemy as sa
|
|
226
|
+
|
|
227
|
+
revision: str = 'abc123'
|
|
228
|
+
down_revision: Union[str, None] = None
|
|
229
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
230
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def upgrade() -> None:
|
|
234
|
+
op.create_table(
|
|
235
|
+
'users',
|
|
236
|
+
sa.Column('id', sa.Integer(), primary_key=True),
|
|
237
|
+
sa.Column('email', sa.String(255), nullable=False, unique=True),
|
|
238
|
+
sa.Column('password_hash', sa.String(255), nullable=False),
|
|
239
|
+
sa.Column('is_active', sa.Boolean(), server_default='true'),
|
|
240
|
+
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
|
|
241
|
+
)
|
|
242
|
+
op.create_index('ix_users_email', 'users', ['email'])
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def downgrade() -> None:
|
|
246
|
+
op.drop_index('ix_users_email', table_name='users')
|
|
247
|
+
op.drop_table('users')
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
## Data Migration Example
|
|
251
|
+
\`\`\`python
|
|
252
|
+
"""Populate default categories
|
|
253
|
+
|
|
254
|
+
Revision ID: def456
|
|
255
|
+
Revises: abc123
|
|
256
|
+
Create Date: 2024-01-16 10:00:00.000000
|
|
257
|
+
"""
|
|
258
|
+
from alembic import op
|
|
259
|
+
import sqlalchemy as sa
|
|
260
|
+
|
|
261
|
+
revision: str = 'def456'
|
|
262
|
+
down_revision: str = 'abc123'
|
|
263
|
+
|
|
264
|
+
# Define table for data operations
|
|
265
|
+
categories = sa.table(
|
|
266
|
+
'categories',
|
|
267
|
+
sa.column('id', sa.Integer),
|
|
268
|
+
sa.column('name', sa.String),
|
|
269
|
+
sa.column('slug', sa.String),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def upgrade() -> None:
|
|
274
|
+
# Bulk insert default data
|
|
275
|
+
op.bulk_insert(
|
|
276
|
+
categories,
|
|
277
|
+
[
|
|
278
|
+
{'id': 1, 'name': 'Technology', 'slug': 'technology'},
|
|
279
|
+
{'id': 2, 'name': 'Business', 'slug': 'business'},
|
|
280
|
+
{'id': 3, 'name': 'Science', 'slug': 'science'},
|
|
281
|
+
]
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def downgrade() -> None:
|
|
286
|
+
op.execute(categories.delete().where(categories.c.id.in_([1, 2, 3])))
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
## Complex Data Migration with Queries
|
|
290
|
+
\`\`\`python
|
|
291
|
+
"""Migrate user roles to new format
|
|
292
|
+
|
|
293
|
+
Revision ID: ghi789
|
|
294
|
+
Revises: def456
|
|
295
|
+
"""
|
|
296
|
+
from alembic import op
|
|
297
|
+
import sqlalchemy as sa
|
|
298
|
+
from sqlalchemy.orm import Session
|
|
299
|
+
|
|
300
|
+
revision: str = 'ghi789'
|
|
301
|
+
down_revision: str = 'def456'
|
|
302
|
+
|
|
303
|
+
# Old and new tables
|
|
304
|
+
old_user_roles = sa.table(
|
|
305
|
+
'user_roles',
|
|
306
|
+
sa.column('user_id', sa.Integer),
|
|
307
|
+
sa.column('role', sa.String),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
new_roles = sa.table(
|
|
311
|
+
'roles',
|
|
312
|
+
sa.column('id', sa.Integer),
|
|
313
|
+
sa.column('name', sa.String),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
user_role_assignments = sa.table(
|
|
317
|
+
'user_role_assignments',
|
|
318
|
+
sa.column('user_id', sa.Integer),
|
|
319
|
+
sa.column('role_id', sa.Integer),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def upgrade() -> None:
|
|
324
|
+
bind = op.get_bind()
|
|
325
|
+
session = Session(bind=bind)
|
|
326
|
+
|
|
327
|
+
# Create new roles table
|
|
328
|
+
op.create_table(
|
|
329
|
+
'roles',
|
|
330
|
+
sa.Column('id', sa.Integer(), primary_key=True),
|
|
331
|
+
sa.Column('name', sa.String(50), unique=True),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Create junction table
|
|
335
|
+
op.create_table(
|
|
336
|
+
'user_role_assignments',
|
|
337
|
+
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id')),
|
|
338
|
+
sa.Column('role_id', sa.Integer(), sa.ForeignKey('roles.id')),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Migrate data
|
|
342
|
+
unique_roles = session.execute(
|
|
343
|
+
sa.select(old_user_roles.c.role).distinct()
|
|
344
|
+
).fetchall()
|
|
345
|
+
|
|
346
|
+
role_mapping = {}
|
|
347
|
+
for i, (role_name,) in enumerate(unique_roles, 1):
|
|
348
|
+
session.execute(new_roles.insert().values(id=i, name=role_name))
|
|
349
|
+
role_mapping[role_name] = i
|
|
350
|
+
|
|
351
|
+
# Migrate assignments
|
|
352
|
+
old_assignments = session.execute(sa.select(old_user_roles)).fetchall()
|
|
353
|
+
for user_id, role_name in old_assignments:
|
|
354
|
+
session.execute(
|
|
355
|
+
user_role_assignments.insert().values(
|
|
356
|
+
user_id=user_id,
|
|
357
|
+
role_id=role_mapping[role_name]
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Drop old column
|
|
362
|
+
op.drop_table('user_roles')
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def downgrade() -> None:
|
|
366
|
+
# Reverse migration logic
|
|
367
|
+
pass
|
|
368
|
+
\`\`\`
|
|
369
|
+
|
|
370
|
+
## Batch Operations (SQLite Compatible)
|
|
371
|
+
\`\`\`python
|
|
372
|
+
"""Add new columns with batch mode for SQLite
|
|
373
|
+
|
|
374
|
+
Revision ID: jkl012
|
|
375
|
+
Revises: ghi789
|
|
376
|
+
"""
|
|
377
|
+
from alembic import op
|
|
378
|
+
import sqlalchemy as sa
|
|
379
|
+
|
|
380
|
+
revision: str = 'jkl012'
|
|
381
|
+
down_revision: str = 'ghi789'
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def upgrade() -> None:
|
|
385
|
+
# SQLite doesn't support ALTER TABLE well, use batch mode
|
|
386
|
+
with op.batch_alter_table('users') as batch_op:
|
|
387
|
+
batch_op.add_column(sa.Column('phone', sa.String(20)))
|
|
388
|
+
batch_op.add_column(sa.Column('verified', sa.Boolean(), server_default='false'))
|
|
389
|
+
batch_op.alter_column('email', nullable=False)
|
|
390
|
+
batch_op.create_index('ix_users_phone', ['phone'])
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def downgrade() -> None:
|
|
394
|
+
with op.batch_alter_table('users') as batch_op:
|
|
395
|
+
batch_op.drop_index('ix_users_phone')
|
|
396
|
+
batch_op.drop_column('verified')
|
|
397
|
+
batch_op.drop_column('phone')
|
|
398
|
+
\`\`\`
|
|
399
|
+
|
|
400
|
+
## Testing Migrations
|
|
401
|
+
\`\`\`python
|
|
402
|
+
# tests/test_migrations.py
|
|
403
|
+
import pytest
|
|
404
|
+
from alembic import command
|
|
405
|
+
from alembic.config import Config
|
|
406
|
+
from sqlalchemy import create_engine, inspect
|
|
407
|
+
|
|
408
|
+
@pytest.fixture
|
|
409
|
+
def alembic_config():
|
|
410
|
+
config = Config("alembic.ini")
|
|
411
|
+
config.set_main_option("sqlalchemy.url", "sqlite:///:memory:")
|
|
412
|
+
return config
|
|
413
|
+
|
|
414
|
+
@pytest.fixture
|
|
415
|
+
def engine():
|
|
416
|
+
return create_engine("sqlite:///:memory:")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_migrations_upgrade_downgrade(alembic_config, engine):
|
|
420
|
+
"""Test that all migrations can be applied and rolled back."""
|
|
421
|
+
# Run all upgrades
|
|
422
|
+
with engine.begin() as connection:
|
|
423
|
+
alembic_config.attributes['connection'] = connection
|
|
424
|
+
command.upgrade(alembic_config, "head")
|
|
425
|
+
|
|
426
|
+
# Verify final state
|
|
427
|
+
inspector = inspect(engine)
|
|
428
|
+
tables = inspector.get_table_names()
|
|
429
|
+
assert 'users' in tables
|
|
430
|
+
assert 'alembic_version' in tables
|
|
431
|
+
|
|
432
|
+
# Run all downgrades
|
|
433
|
+
command.downgrade(alembic_config, "base")
|
|
434
|
+
|
|
435
|
+
# Verify clean state
|
|
436
|
+
inspector = inspect(engine)
|
|
437
|
+
tables = inspector.get_table_names()
|
|
438
|
+
assert 'users' not in tables
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_migration_data_integrity(alembic_config, engine):
|
|
442
|
+
"""Test data migrations preserve integrity."""
|
|
443
|
+
with engine.begin() as connection:
|
|
444
|
+
alembic_config.attributes['connection'] = connection
|
|
445
|
+
|
|
446
|
+
# Apply migrations up to data migration
|
|
447
|
+
command.upgrade(alembic_config, "def456")
|
|
448
|
+
|
|
449
|
+
# Verify data was inserted
|
|
450
|
+
result = connection.execute(sa.text("SELECT COUNT(*) FROM categories"))
|
|
451
|
+
assert result.scalar() == 3
|
|
452
|
+
\`\`\`
|
|
453
|
+
|
|
454
|
+
## Multi-Database Support
|
|
455
|
+
\`\`\`python
|
|
456
|
+
# alembic/env.py with multiple databases
|
|
457
|
+
from alembic import context
|
|
458
|
+
import os
|
|
459
|
+
|
|
460
|
+
def get_url_for_db(db_name: str) -> str:
|
|
461
|
+
urls = {
|
|
462
|
+
'primary': os.environ.get('PRIMARY_DB_URL'),
|
|
463
|
+
'analytics': os.environ.get('ANALYTICS_DB_URL'),
|
|
464
|
+
}
|
|
465
|
+
return urls.get(db_name)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def run_migrations_online() -> None:
|
|
469
|
+
db_name = context.get_x_argument(as_dictionary=True).get('db', 'primary')
|
|
470
|
+
url = get_url_for_db(db_name)
|
|
471
|
+
|
|
472
|
+
# Use appropriate metadata based on database
|
|
473
|
+
if db_name == 'analytics':
|
|
474
|
+
from app.models.analytics import Base as AnalyticsBase
|
|
475
|
+
target_metadata = AnalyticsBase.metadata
|
|
476
|
+
else:
|
|
477
|
+
from app.models import Base
|
|
478
|
+
target_metadata = Base.metadata
|
|
479
|
+
|
|
480
|
+
# ... rest of configuration
|
|
481
|
+
\`\`\`
|
|
482
|
+
|
|
483
|
+
\`\`\`bash
|
|
484
|
+
# Run migrations for specific database
|
|
485
|
+
alembic -x db=primary upgrade head
|
|
486
|
+
alembic -x db=analytics upgrade head
|
|
487
|
+
\`\`\`
|
|
488
|
+
|
|
489
|
+
## ✅ DO
|
|
490
|
+
- Always review auto-generated migrations before applying
|
|
491
|
+
- Use environment variables for database URLs (never commit credentials)
|
|
492
|
+
- Write both \`upgrade()\` and \`downgrade()\` functions
|
|
493
|
+
- Use \`batch_alter_table\` for SQLite compatibility
|
|
494
|
+
- Test migrations in CI before deploying
|
|
495
|
+
- Use descriptive migration messages
|
|
496
|
+
- Create separate migrations for schema and data changes
|
|
497
|
+
- Back up database before running migrations in production
|
|
498
|
+
|
|
499
|
+
## ❌ DON'T
|
|
500
|
+
- Don't edit migrations that have already been applied in production
|
|
501
|
+
- Don't use \`autogenerate\` for data migrations
|
|
502
|
+
- Don't commit database credentials in alembic.ini
|
|
503
|
+
- Don't skip downgrade implementations (needed for rollbacks)
|
|
504
|
+
- Don't run untested migrations directly in production
|