plain.models 0.47.0__py3-none-any.whl → 0.48.0__py3-none-any.whl

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.
plain/models/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.48.0](https://github.com/dropseed/plain/releases/plain-models@0.48.0) (2025-09-26)
4
+
5
+ ### What's changed
6
+
7
+ - Migrations now run in a single transaction by default for databases that support transactional DDL, providing all-or-nothing migration batches for better safety and consistency ([6d0c105](https://github.com/dropseed/plain/commit/6d0c105fa9))
8
+ - Added `--atomic-batch/--no-atomic-batch` options to `plain migrate` to explicitly control whether migrations are run in a single transaction ([6d0c105](https://github.com/dropseed/plain/commit/6d0c105fa9))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required
13
+
3
14
  ## [0.47.0](https://github.com/dropseed/plain/releases/plain-models@0.47.0) (2025-09-25)
4
15
 
5
16
  ### What's changed
@@ -59,6 +59,7 @@ class BaseDatabaseCreation:
59
59
  prune=False,
60
60
  no_input=True,
61
61
  verbosity=max(verbosity - 1, 0),
62
+ atomic_batch=False, # No need for atomic batch when creating test database
62
63
  )
63
64
 
64
65
  # Ensure a connection for the side effect of initializing the test database.
plain/models/cli.py CHANGED
@@ -362,6 +362,11 @@ def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbos
362
362
  default=1,
363
363
  help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
364
364
  )
365
+ @click.option(
366
+ "--atomic-batch/--no-atomic-batch",
367
+ default=None,
368
+ help="Run migrations in a single transaction (auto-detected by default)",
369
+ )
365
370
  def migrate(
366
371
  package_label,
367
372
  migration_name,
@@ -372,6 +377,7 @@ def migrate(
372
377
  prune,
373
378
  no_input,
374
379
  verbosity,
380
+ atomic_batch,
375
381
  ):
376
382
  """Updates database schema. Manages both packages with migrations and those without."""
377
383
 
@@ -575,6 +581,65 @@ def migrate(
575
581
  # pprint(sql)
576
582
 
577
583
  if migration_plan:
584
+ # Determine whether to use atomic batch
585
+ use_atomic_batch = False
586
+ if len(migration_plan) > 1:
587
+ # Check database capabilities
588
+ can_rollback_ddl = db_connection.features.can_rollback_ddl
589
+
590
+ # Check if all migrations support atomic
591
+ non_atomic_migrations = [m for m in migration_plan if not m.atomic]
592
+
593
+ if atomic_batch is True:
594
+ # User explicitly requested atomic batch
595
+ if not can_rollback_ddl:
596
+ raise click.UsageError(
597
+ f"--atomic-batch not supported on {db_connection.vendor}. "
598
+ "Remove the flag or use a database that supports transactional DDL."
599
+ )
600
+ if non_atomic_migrations:
601
+ names = ", ".join(
602
+ f"{m.package_label}.{m.name}" for m in non_atomic_migrations[:3]
603
+ )
604
+ if len(non_atomic_migrations) > 3:
605
+ names += f", and {len(non_atomic_migrations) - 3} more"
606
+ raise click.UsageError(
607
+ f"--atomic-batch requested but these migrations have atomic=False: {names}"
608
+ )
609
+ use_atomic_batch = True
610
+ if verbosity >= 1:
611
+ click.echo(
612
+ f" Running {len(migration_plan)} migrations in atomic batch (all-or-nothing)"
613
+ )
614
+ elif atomic_batch is False:
615
+ # User explicitly disabled atomic batch
616
+ use_atomic_batch = False
617
+ if verbosity >= 1:
618
+ click.echo(f" Running {len(migration_plan)} migrations separately")
619
+ else:
620
+ # Auto-detect (atomic_batch is None)
621
+ if can_rollback_ddl and not non_atomic_migrations:
622
+ use_atomic_batch = True
623
+ if verbosity >= 1:
624
+ click.echo(
625
+ f" Running {len(migration_plan)} migrations in atomic batch (all-or-nothing)"
626
+ )
627
+ else:
628
+ use_atomic_batch = False
629
+ if verbosity >= 1:
630
+ if not can_rollback_ddl:
631
+ click.echo(
632
+ f" Running {len(migration_plan)} migrations separately ({db_connection.vendor} doesn't support batch transactions)"
633
+ )
634
+ elif non_atomic_migrations:
635
+ click.echo(
636
+ f" Running {len(migration_plan)} migrations separately (some migrations have atomic=False)"
637
+ )
638
+ else:
639
+ click.echo(
640
+ f" Running {len(migration_plan)} migrations separately"
641
+ )
642
+
578
643
  if backup or (backup is None and settings.DEBUG):
579
644
  backup_name = f"migrate_{time.strftime('%Y%m%d_%H%M%S')}"
580
645
  click.secho(
@@ -599,6 +664,7 @@ def migrate(
599
664
  plan=migration_plan,
600
665
  state=pre_migrate_state.clone(),
601
666
  fake=fake,
667
+ atomic_batch=use_atomic_batch,
602
668
  )
603
669
  # post_migrate signals have access to all models. Ensure that all models
604
670
  # are reloaded in case any are delayed.
@@ -1,3 +1,6 @@
1
+ from contextlib import nullcontext
2
+
3
+ from ..transaction import atomic
1
4
  from .loader import MigrationLoader
2
5
  from .recorder import MigrationRecorder
3
6
  from .state import ProjectState
@@ -52,12 +55,14 @@ class MigrationExecutor:
52
55
  migration.mutate_state(state, preserve=False)
53
56
  return state
54
57
 
55
- def migrate(self, targets, plan=None, state=None, fake=False):
58
+ def migrate(self, targets, plan=None, state=None, fake=False, atomic_batch=False):
56
59
  """
57
60
  Migrate the database up to the given targets.
58
61
 
59
62
  Plain first needs to create all project states before a migration is
60
63
  (un)applied and in a second step run all the database operations.
64
+
65
+ atomic_batch: Whether to run all migrations in a single transaction.
61
66
  """
62
67
  # The plain_migrations table must be present to record applied
63
68
  # migrations, but don't create it if there are no migrations to apply.
@@ -82,34 +87,31 @@ class MigrationExecutor:
82
87
  if state is None:
83
88
  # The resulting state should still include applied migrations.
84
89
  state = self._create_project_state(with_applied_migrations=True)
85
- state = self._migrate_all_forwards(state, plan, full_plan, fake=fake)
86
-
87
- self.check_replacements()
88
90
 
89
- return state
91
+ migrations_to_run = set(plan)
92
+
93
+ # Choose context manager based on atomic_batch
94
+ batch_context = atomic if (atomic_batch and len(plan) > 1) else nullcontext
95
+
96
+ with batch_context():
97
+ for migration in full_plan:
98
+ if not migrations_to_run:
99
+ # We remove every migration that we applied from these sets so
100
+ # that we can bail out once the last migration has been applied
101
+ # and don't always run until the very end of the migration
102
+ # process.
103
+ break
104
+ if migration in migrations_to_run:
105
+ if "models_registry" not in state.__dict__:
106
+ if self.progress_callback:
107
+ self.progress_callback("render_start")
108
+ state.models_registry # Render all -- performance critical
109
+ if self.progress_callback:
110
+ self.progress_callback("render_success")
111
+ state = self.apply_migration(state, migration, fake=fake)
112
+ migrations_to_run.remove(migration)
90
113
 
91
- def _migrate_all_forwards(self, state, plan, full_plan, fake):
92
- """
93
- Take a list of 2-tuples of the form (migration instance, False) and
94
- apply them in the order they occur in the full_plan.
95
- """
96
- migrations_to_run = set(plan)
97
- for migration in full_plan:
98
- if not migrations_to_run:
99
- # We remove every migration that we applied from these sets so
100
- # that we can bail out once the last migration has been applied
101
- # and don't always run until the very end of the migration
102
- # process.
103
- break
104
- if migration in migrations_to_run:
105
- if "models_registry" not in state.__dict__:
106
- if self.progress_callback:
107
- self.progress_callback("render_start")
108
- state.models_registry # Render all -- performance critical
109
- if self.progress_callback:
110
- self.progress_callback("render_success")
111
- state = self.apply_migration(state, migration, fake=fake)
112
- migrations_to_run.remove(migration)
114
+ self.check_replacements()
113
115
 
114
116
  return state
115
117
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.47.0
3
+ Version: 0.48.0
4
4
  Summary: Model your data and store it in a database.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -1,10 +1,10 @@
1
1
  plain/models/AGENTS.md,sha256=xQQW-z-DehnCUyjiGSBfLqUjoSUdo_W1b0JmwYmWieA,209
2
- plain/models/CHANGELOG.md,sha256=JMkQ7LwLIp63_HtRIwOMdIpveUrhHvIBJbvBDaNG9k4,17633
2
+ plain/models/CHANGELOG.md,sha256=_EJXRX89opgWTq_FnrLrs2Q7BOKxbrINWsYU4qwZIqQ,18245
3
3
  plain/models/README.md,sha256=lqzWJrEIxBCHC1P8X1YoRjbsMFlu0-kG4ujP76B_ZO4,8572
4
4
  plain/models/__init__.py,sha256=aB9HhIKBh0iK3LZztInAE-rDF-yKsdfcjfMtwtN5vnI,2920
5
5
  plain/models/aggregates.py,sha256=P0mhsMl1VZt2CVHMuCHnNI8SxZ9citjDLEgioN6NOpo,7240
6
6
  plain/models/base.py,sha256=4NkK-gkcxbIWZm7c9Q51eDwLClIieWZvlRg6l7vJOxE,65692
7
- plain/models/cli.py,sha256=lhvlw7DVOU3CVoMUpHaj7qIbQ2d6qtSm_l9PdCeCnc0,36404
7
+ plain/models/cli.py,sha256=rqrgG__OyqtaDaKLB6XZnS6Zv7esU9K9EAyKCO35McA,39548
8
8
  plain/models/config.py,sha256=-m15VY1ZJKWdPGt-5i9fnMvz9LBzPfSRgWmWeEV8Dao,382
9
9
  plain/models/connections.py,sha256=RBNa2FZ0x3C9un6PaYL-IYzH_OesRSpdHNGKvYHGiOM,2276
10
10
  plain/models/constants.py,sha256=ndnj9TOTKW0p4YcIPLOLEbsH6mOgFi6B1-rIzr_iwwU,210
@@ -34,7 +34,7 @@ plain/models/backends/utils.py,sha256=VN9b_hnGeLqndVAcCx00X7KhFC6jY2Tn6J3E62HqDE
34
34
  plain/models/backends/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  plain/models/backends/base/base.py,sha256=I5040pUAe7-yNTb9wfMNOYjeiFQLZ5dcRI4rtmVKJ04,26457
36
36
  plain/models/backends/base/client.py,sha256=90Ffs6zZYCli3tJjwsPH8TItZ8tz1Pp-zhQa-EpsNqc,937
37
- plain/models/backends/base/creation.py,sha256=H-xx667pLediQ19HDmE_mP-b03tDKQriBmWPKigJ5t0,9332
37
+ plain/models/backends/base/creation.py,sha256=JlLnboF5reo-s6RM35EpdWBqsRv63wcJSxX5igIPPUM,9420
38
38
  plain/models/backends/base/features.py,sha256=1AehdhpeC7VobhggwpeXIt7HJNY2EWY700j4gYX8Xjs,7856
39
39
  plain/models/backends/base/introspection.py,sha256=8icKf9h8y4kobmyrbo8JWTDcpQIAt4oS_4FtCnY7FqQ,6815
40
40
  plain/models/backends/base/operations.py,sha256=tzuw7AgVb3aZn2RHamReGNco7wIt4UBkAHx8HebDZ_s,25652
@@ -89,7 +89,7 @@ plain/models/functions/window.py,sha256=3S0QIZc_pIVcWpE5Qq-OxixmtATLb8rZrWkvCfVt
89
89
  plain/models/migrations/__init__.py,sha256=ZAQUGrfr_OxYMIO7vUBIHLs_M3oZ4iQSjDzCHRFUdtI,96
90
90
  plain/models/migrations/autodetector.py,sha256=MuJEVaU9IZ8c6HfcDsEANNujUrSGMN-iDd5WDPM7ht8,60957
91
91
  plain/models/migrations/exceptions.py,sha256=_bGjIMaBP2Py9ePUxUhiH0p1zXrQM4JhJO4lWfyF8-g,1044
92
- plain/models/migrations/executor.py,sha256=2_1bWM7Dp3s8z6PADAEN-Y0KnIiRzAqsUkn_nRQl5TA,6757
92
+ plain/models/migrations/executor.py,sha256=4Hsqs6UV5I_Z4PesNi86YPvJyzPcJUTyrxHxGJu2zf0,6907
93
93
  plain/models/migrations/graph.py,sha256=nrztu_8dU0wAUSxKUqqFWpvZcSQxGEqE6dXWkPytmCU,12570
94
94
  plain/models/migrations/loader.py,sha256=qUTmaEYI1_mV6goQPQYZKjSz8rMbE6G1wqvrAsmuGwA,16464
95
95
  plain/models/migrations/migration.py,sha256=22YwRHnaRnCkBpW5p7K89tAU6h4QSsG5yiq-o7W-cSI,6505
@@ -115,8 +115,8 @@ plain/models/sql/where.py,sha256=ezE9Clt2BmKo-I7ARsgqZ_aVA-1UdayCwr6ULSWZL6c,126
115
115
  plain/models/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
116
  plain/models/test/pytest.py,sha256=KD5-mxonBxOYIhUh9Ql5uJOIiC9R4t-LYfb6sjA0UdE,3486
117
117
  plain/models/test/utils.py,sha256=S3d6zf3OFWDxB_kBJr0tDvwn51bjwDVWKPumv37N-p8,467
118
- plain_models-0.47.0.dist-info/METADATA,sha256=oBEWDFezcMc1KxtVoQIMSEr15Oy7NB43aLiAGurmYfg,8884
119
- plain_models-0.47.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
120
- plain_models-0.47.0.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
121
- plain_models-0.47.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
122
- plain_models-0.47.0.dist-info/RECORD,,
118
+ plain_models-0.48.0.dist-info/METADATA,sha256=rE7jXG5-QLDFTyIJQnTb9QGt6Z23o_LZ16soPfLKvlI,8884
119
+ plain_models-0.48.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
120
+ plain_models-0.48.0.dist-info/entry_points.txt,sha256=IYJAW9MpL3PXyXFWmKmALagAGXC_5rzBn2eEGJlcV04,112
121
+ plain_models-0.48.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
122
+ plain_models-0.48.0.dist-info/RECORD,,