plain.postgres 0.95.0__tar.gz → 0.96.0__tar.gz
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_postgres-0.95.0 → plain_postgres-0.96.0}/PKG-INFO +99 -38
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/CHANGELOG.md +50 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/README.md +98 -37
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/__init__.py +3 -2
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/base.py +66 -33
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/cli/migrations.py +39 -14
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/cli/schema.py +1 -1
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/cli/sync.py +18 -10
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/convergence/__init__.py +8 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/convergence/analysis.py +153 -21
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/convergence/fixes.py +135 -23
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/convergence/planning.py +13 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/ddl.py +24 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/default_settings.py +18 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/dialect.py +45 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/enums.py +9 -15
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/expressions.py +4 -2
- plain_postgres-0.96.0/plain/postgres/fields/__init__.py +51 -0
- plain_postgres-0.96.0/plain/postgres/fields/base.py +867 -0
- plain_postgres-0.96.0/plain/postgres/fields/binary.py +65 -0
- plain_postgres-0.96.0/plain/postgres/fields/boolean.py +38 -0
- plain_postgres-0.96.0/plain/postgres/fields/duration.py +51 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/encrypted.py +34 -43
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/json.py +16 -48
- plain_postgres-0.96.0/plain/postgres/fields/network.py +101 -0
- plain_postgres-0.96.0/plain/postgres/fields/numeric.py +278 -0
- plain_postgres-0.96.0/plain/postgres/fields/primary_key.py +86 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/related.py +76 -68
- plain_postgres-0.96.0/plain/postgres/fields/related_lookups.py +103 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/reverse_related.py +3 -7
- plain_postgres-0.96.0/plain/postgres/fields/temporal.py +381 -0
- plain_postgres-0.96.0/plain/postgres/fields/text.py +131 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/timezones.py +27 -15
- plain_postgres-0.96.0/plain/postgres/fields/uuid.py +78 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/forms.py +52 -31
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/functions/__init__.py +6 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/functions/datetime.py +13 -6
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/functions/mixins.py +8 -2
- plain_postgres-0.96.0/plain/postgres/functions/random.py +51 -0
- plain_postgres-0.96.0/plain/postgres/functions/uuid.py +9 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/introspection/__init__.py +2 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/introspection/schema.py +249 -14
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/meta.py +1 -1
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/autodetector.py +109 -45
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/exceptions.py +9 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/operations/fields.py +2 -23
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/operations/special.py +12 -2
- plain_postgres-0.96.0/plain/postgres/migrations/questioner.py +109 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/recorder.py +1 -2
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/serializer.py +2 -2
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/state.py +0 -14
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/query.py +12 -5
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/query_utils.py +7 -13
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/schema.py +122 -245
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/sql/compiler.py +31 -93
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/sql/query.py +15 -75
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/sql/where.py +0 -24
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/types.py +2 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/types.pyi +24 -163
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/pyproject.toml +1 -1
- plain_postgres-0.96.0/tests/app/examples/forms.py +48 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +4 -4
- plain_postgres-0.96.0/tests/app/examples/migrations/0011_defaultsexample.py +28 -0
- plain_postgres-0.96.0/tests/app/examples/migrations/0012_iterationexample.py +21 -0
- plain_postgres-0.96.0/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +36 -0
- plain_postgres-0.96.0/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +64 -0
- plain_postgres-0.96.0/tests/app/examples/migrations/0015_dbdefaultsexample.py +22 -0
- plain_postgres-0.96.0/tests/app/examples/migrations/0016_formsexample.py +41 -0
- plain_postgres-0.96.0/tests/app/examples/migrations/0017_random_string_token.py +18 -0
- plain_postgres-0.96.0/tests/app/examples/models/__init__.py +18 -0
- plain_postgres-0.96.0/tests/app/examples/models/constraints.py +28 -0
- plain_postgres-0.96.0/tests/app/examples/models/defaults.py +50 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/models/delete.py +1 -7
- plain_postgres-0.96.0/tests/app/examples/models/encrypted.py +16 -0
- plain_postgres-0.96.0/tests/app/examples/models/forms.py +35 -0
- plain_postgres-0.96.0/tests/app/examples/models/indexes.py +19 -0
- plain_postgres-0.96.0/tests/app/examples/models/iteration.py +20 -0
- plain_postgres-0.96.0/tests/app/examples/models/mixins.py +26 -0
- plain_postgres-0.96.0/tests/app/examples/models/nullability.py +17 -0
- plain_postgres-0.96.0/tests/app/examples/models/querysets.py +41 -0
- plain_postgres-0.96.0/tests/app/examples/models/relationships.py +44 -0
- plain_postgres-0.96.0/tests/app/examples/models/trees.py +17 -0
- plain_postgres-0.96.0/tests/app/examples/models/unregistered.py +7 -0
- plain_postgres-0.96.0/tests/app/examples/urls.py +36 -0
- plain_postgres-0.96.0/tests/app/examples/views.py +66 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/urls.py +3 -4
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/conftest_convergence.py +15 -2
- plain_postgres-0.96.0/tests/test_autodetector_not_null_errors.py +211 -0
- plain_postgres-0.96.0/tests/test_autodetector_type_change.py +105 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_convergence.py +203 -165
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_convergence_constraints.py +365 -241
- plain_postgres-0.96.0/tests/test_convergence_defaults.py +471 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_convergence_fk.py +90 -95
- plain_postgres-0.96.0/tests/test_convergence_indexes.py +600 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_convergence_nullability.py +85 -46
- plain_postgres-0.96.0/tests/test_convergence_timeouts.py +350 -0
- plain_postgres-0.96.0/tests/test_db_expression_defaults.py +495 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_delete_behaviors.py +39 -41
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_encrypted_fields.py +1 -1
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_exceptions.py +26 -17
- plain_postgres-0.96.0/tests/test_field_defaults.py +146 -0
- plain_postgres-0.96.0/tests/test_functions_uuid.py +31 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_introspection.py +177 -45
- plain_postgres-0.96.0/tests/test_iterator.py +101 -0
- plain_postgres-0.96.0/tests/test_literal_default_persistence.py +212 -0
- plain_postgres-0.96.0/tests/test_m2m.py +114 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_manager_assignment.py +1 -1
- plain_postgres-0.96.0/tests/test_mixins.py +19 -0
- plain_postgres-0.96.0/tests/test_modelform_roundtrip.py +295 -0
- plain_postgres-0.96.0/tests/test_no_callable_defaults.py +55 -0
- plain_postgres-0.96.0/tests/test_random_string_field.py +124 -0
- plain_postgres-0.96.0/tests/test_raw_query.py +44 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_read_only_transactions.py +16 -16
- plain_postgres-0.95.0/tests/test_related_descriptors.py → plain_postgres-0.96.0/tests/test_related.py +176 -5
- plain_postgres-0.96.0/tests/test_schema_timeouts.py +129 -0
- plain_postgres-0.95.0/plain/postgres/fields/__init__.py +0 -1937
- plain_postgres-0.95.0/plain/postgres/fields/related_lookups.py +0 -223
- plain_postgres-0.95.0/plain/postgres/migrations/questioner.py +0 -314
- plain_postgres-0.95.0/tests/app/examples/models/__init__.py +0 -139
- plain_postgres-0.95.0/tests/test_convergence_indexes.py +0 -483
- plain_postgres-0.95.0/tests/test_iterator.py +0 -99
- plain_postgres-0.95.0/tests/test_models.py +0 -197
- plain_postgres-0.95.0/tests/test_related_manager_api.py +0 -154
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/.gitignore +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/CLAUDE.md +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/LICENSE +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/README.md +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/connections.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/introspection/health.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/test/utils.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_connection_isolation.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_database_url.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_migration_executor.py +0 -0
- {plain_postgres-0.95.0 → plain_postgres-0.96.0}/tests/test_schema_normalize_type.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.96.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-Expression: BSD-3-Clause
|
|
@@ -48,7 +48,7 @@ class User(postgres.Model):
|
|
|
48
48
|
email: str = types.EmailField()
|
|
49
49
|
password = PasswordField()
|
|
50
50
|
is_admin: bool = types.BooleanField(default=False)
|
|
51
|
-
created_at: datetime = types.DateTimeField(
|
|
51
|
+
created_at: datetime = types.DateTimeField(create_now=True)
|
|
52
52
|
|
|
53
53
|
def __str__(self) -> str:
|
|
54
54
|
return self.email
|
|
@@ -452,35 +452,42 @@ Schema changes fall into three categories, each with a different author and appl
|
|
|
452
452
|
- **Structural migrations** — tables, columns, renames, column type changes. Framework-generated from the model diff, but you review them and decide when to deploy (a column type change can rewrite the table; a column drop is destructive).
|
|
453
453
|
- **Data migrations** — backfills, transformations, one-time cleanup. Authored by you via `RunPython` or `RunSQL`. The framework only sequences them.
|
|
454
454
|
|
|
455
|
-
| Change | Category | Safe apply pattern
|
|
456
|
-
| ------------------------------------- | -------------------- |
|
|
457
|
-
| Add / drop index | Convergence | `CREATE INDEX CONCURRENTLY`
|
|
458
|
-
| Add / drop unique or check constraint | Convergence | `ADD CONSTRAINT NOT VALID` + `VALIDATE`
|
|
459
|
-
| Add / remove NOT NULL | Convergence | `CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL`
|
|
460
|
-
| Change FK `on_delete` action | Convergence | drop + re-add with `NOT VALID` + `VALIDATE`
|
|
461
|
-
|
|
|
462
|
-
|
|
|
463
|
-
|
|
|
464
|
-
|
|
|
465
|
-
|
|
|
455
|
+
| Change | Category | Safe apply pattern |
|
|
456
|
+
| ------------------------------------- | -------------------- | --------------------------------------------------- |
|
|
457
|
+
| Add / drop index | Convergence | `CREATE INDEX CONCURRENTLY` |
|
|
458
|
+
| Add / drop unique or check constraint | Convergence | `ADD CONSTRAINT NOT VALID` + `VALIDATE` |
|
|
459
|
+
| Add / remove NOT NULL | Convergence | `CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL` |
|
|
460
|
+
| Change FK `on_delete` action | Convergence | drop + re-add with `NOT VALID` + `VALIDATE` |
|
|
461
|
+
| Set / change / drop column `DEFAULT` | Convergence | catalog-only `ALTER COLUMN SET/DROP DEFAULT` |
|
|
462
|
+
| Create / drop table | Structural migration | framework-generated, you review |
|
|
463
|
+
| Add / drop / rename column | Structural migration | framework-generated, you review |
|
|
464
|
+
| Column type change (safe widening) | Structural migration | framework-generated `ALTER TYPE` with implicit cast |
|
|
465
|
+
| Column type change (other) | Data migration | you author (explicit `RunSQL` with `USING`) |
|
|
466
|
+
| Data backfill or transformation | Data migration | you author (`RunPython` / `RunSQL`) |
|
|
467
|
+
| One-time cleanup, seeding | Data migration | you author |
|
|
466
468
|
|
|
467
469
|
**The principle: who authors the change, and can the framework guarantee safety?** If the framework can derive both the change and a universally-safe apply pattern from model definitions, it belongs to convergence. If the framework can generate the DDL but safety depends on context (table size, deploy timing, destructiveness), it's a structural migration — you review it before deploying. If only you know what to do, it's a data migration.
|
|
468
470
|
|
|
469
471
|
Many convergence-managed changes produce DB-enforced behavior — cascading deletes (`ON DELETE`), validation (`CHECK`, `NOT NULL`), default generation. Whether a change is "behavioral" doesn't determine the category; whether the framework can guarantee a safe apply does.
|
|
470
472
|
|
|
471
|
-
| Property
|
|
472
|
-
|
|
|
473
|
-
| Authored by
|
|
474
|
-
| When it runs
|
|
475
|
-
| Drift correction
|
|
476
|
-
| Reversible
|
|
477
|
-
|
|
|
478
|
-
|
|
|
473
|
+
| Property | Convergence | Migrations |
|
|
474
|
+
| ------------------------ | --------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
475
|
+
| Authored by | Framework (derived from models) | Framework (structural) or you (data) |
|
|
476
|
+
| When it runs | Every `sync`, by diffing models vs database | Once, in recorded timestamp order |
|
|
477
|
+
| Drift correction | Yes — reverts undeclared DB changes on next sync | No — manual DB changes persist |
|
|
478
|
+
| Reversible (intentional) | Implicit — roll back code, re-sync re-derives | No — forward-only, fix-forward |
|
|
479
|
+
| Failure behavior | Per-operation commits — partial progress on failure (re-run to retry) | Batch transaction — failure rolls back the entire migration |
|
|
480
|
+
| Files on disk | None — derived from models live | `.py` files in `migrations/` |
|
|
481
|
+
| Safe DDL | Framework-applied patterns (CONCURRENTLY, NOT VALID + VALIDATE) | Generated DDL; you review before deploy |
|
|
479
482
|
|
|
480
483
|
**Drift correction is a convergence-only behavior.** Convergence re-runs on every `sync` and compares models against the database. An index created manually outside a model declaration will be dropped on the next run because models are the source of truth. Migrations don't behave this way — once applied, they're recorded and never re-applied.
|
|
481
484
|
|
|
482
485
|
**Caveats.** The safety promise isn't absolute. Structural migrations aren't lint-checked yet: adding a column with a volatile default (`gen_random_uuid()`, `now()`) on a large table will rewrite it without warning. Review structural migrations before deploying to production.
|
|
483
486
|
|
|
487
|
+
**Column type changes.** The autodetector only auto-generates `AlterField` for a small allowlist of lossless widenings (`smallint → integer`, `smallint → bigint`, `integer → bigint`) and for parameter-only changes like `max_length`. Every other base-type change rejects with guidance — arbitrary `USING col::newtype` casts either fail at apply time (e.g. timestamp → uuid) or silently corrupt data (e.g. bigint FK → text stringifies PKs), and migrations are forward-only. For anything outside the allowlist, scaffold `plain migrations create --empty --name alter_<model>_<field>_type` and author an explicit `RunSQL` with a `USING` expression you've reviewed.
|
|
488
|
+
|
|
489
|
+
"Safe" here means data-integrity safe, not operationally cheap. An `ALTER COLUMN ... TYPE` that changes on-disk width (any of the allowlisted widenings) takes `ACCESS EXCLUSIVE` and rewrites the table — on a large table this can block writes for minutes. Deploy these during a maintenance window, not in the middle of traffic.
|
|
490
|
+
|
|
484
491
|
**Out of scope for convergence.** Triggers, views, stored procedures, and other non-standard DDL stay outside convergence — it won't create them from models, and it won't drop them if they exist. Manage them with `RunSQL` data migrations.
|
|
485
492
|
|
|
486
493
|
### Syncing
|
|
@@ -689,6 +696,43 @@ Some changes can't be applied automatically. For example, if you add `NOT NULL`
|
|
|
689
696
|
|
|
690
697
|
When you remove an index or constraint from a model, convergence automatically drops the undeclared database object on the next `postgres sync`. Models are the source of truth — if it's not declared, it gets removed.
|
|
691
698
|
|
|
699
|
+
### DDL timeouts
|
|
700
|
+
|
|
701
|
+
Every framework-issued DDL statement — both in migrations and in convergence — is wrapped with `lock_timeout` and `statement_timeout` so a deploy can't hang indefinitely waiting for a lock, and so a backfill against an unexpectedly large table fails fast instead of holding `ACCESS EXCLUSIVE` for minutes.
|
|
702
|
+
|
|
703
|
+
```python
|
|
704
|
+
# app/settings.py — defaults shown
|
|
705
|
+
POSTGRES_MIGRATION_LOCK_TIMEOUT = "3s"
|
|
706
|
+
POSTGRES_MIGRATION_STATEMENT_TIMEOUT = "3s"
|
|
707
|
+
POSTGRES_CONVERGENCE_LOCK_TIMEOUT = "3s"
|
|
708
|
+
POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT = "3s"
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
`lock_timeout` applies to every DDL. `statement_timeout` applies only to statements that take `ACCESS EXCLUSIVE` — non-blocking operations (`CREATE INDEX CONCURRENTLY`, `VALIDATE CONSTRAINT`) run unbounded because they can't cascade the lock queue.
|
|
712
|
+
|
|
713
|
+
If a migration issues a row-touching UPDATE (e.g. a hand-written `RunPython` or `RunSQL` backfill), the 3s `statement_timeout` will kill it on any non-tiny table. That's intentional — the right fix is a batched data migration, not a single long-running UPDATE. The common first-time failure mode is applying migrations against a pre-seeded dev or staging database: raise the ceiling for that one run, then lower it back for production deploys.
|
|
714
|
+
|
|
715
|
+
Use `RunSQL(no_timeout=True)` to opt out for a specific operation:
|
|
716
|
+
|
|
717
|
+
```python
|
|
718
|
+
from plain.postgres.migrations.operations import RunSQL
|
|
719
|
+
|
|
720
|
+
operations = [
|
|
721
|
+
RunSQL(
|
|
722
|
+
"UPDATE orders SET status = 'pending' WHERE status IS NULL",
|
|
723
|
+
no_timeout=True,
|
|
724
|
+
),
|
|
725
|
+
]
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Non-atomic migrations (`Migration.atomic = False`, used for `CREATE INDEX CONCURRENTLY` in a migration) skip the timeout prelude automatically — `SET LOCAL` is a no-op outside a transaction block. Manage timeouts inside your own `RunSQL` if you need them.
|
|
729
|
+
|
|
730
|
+
Environment overrides: every setting accepts `PLAIN_POSTGRES_*` env vars, so you can raise the ceiling for a specific deploy without a code change:
|
|
731
|
+
|
|
732
|
+
```bash
|
|
733
|
+
PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT=30s plain migrations apply
|
|
734
|
+
```
|
|
735
|
+
|
|
692
736
|
## Fields
|
|
693
737
|
|
|
694
738
|
You can use many field types for different data:
|
|
@@ -713,8 +757,8 @@ class Product(postgres.Model):
|
|
|
713
757
|
is_active: bool = types.BooleanField(default=True)
|
|
714
758
|
|
|
715
759
|
# Date and time fields
|
|
716
|
-
created_at: datetime = types.DateTimeField(
|
|
717
|
-
updated_at: datetime = types.DateTimeField(
|
|
760
|
+
created_at: datetime = types.DateTimeField(create_now=True)
|
|
761
|
+
updated_at: datetime = types.DateTimeField(update_now=True)
|
|
718
762
|
```
|
|
719
763
|
|
|
720
764
|
**Text fields:**
|
|
@@ -742,10 +786,11 @@ class Product(postgres.Model):
|
|
|
742
786
|
**Other fields:**
|
|
743
787
|
|
|
744
788
|
- [`BooleanField`](./fields/__init__.py#BooleanField) - True/False
|
|
745
|
-
- [`UUIDField`](./fields/__init__.py#UUIDField) - UUID
|
|
789
|
+
- [`UUIDField`](./fields/__init__.py#UUIDField) - UUID (pass `generate=True` for a per-row `gen_random_uuid()` default)
|
|
746
790
|
- [`BinaryField`](./fields/__init__.py#BinaryField) - Raw binary data
|
|
747
791
|
- [`JSONField`](./fields/json.py#JSONField) - JSON data
|
|
748
792
|
- [`GenericIPAddressField`](./fields/__init__.py#GenericIPAddressField) - IPv4 or IPv6 address
|
|
793
|
+
- [`RandomStringField`](./fields/text.py#RandomStringField) - Per-row random hex string generated by Postgres (`length=`) — use for tokens, slugs, short IDs instead of a Python callable default. Slices `gen_random_uuid()` directly; values are uniform hex characters
|
|
749
794
|
|
|
750
795
|
**Encrypted fields:**
|
|
751
796
|
|
|
@@ -775,8 +820,8 @@ from plain.postgres import types
|
|
|
775
820
|
|
|
776
821
|
# Regular Python class for shared fields
|
|
777
822
|
class TimestampedMixin:
|
|
778
|
-
created_at: datetime = types.DateTimeField(
|
|
779
|
-
updated_at: datetime = types.DateTimeField(
|
|
823
|
+
created_at: datetime = types.DateTimeField(create_now=True)
|
|
824
|
+
updated_at: datetime = types.DateTimeField(update_now=True)
|
|
780
825
|
|
|
781
826
|
|
|
782
827
|
# Models inherit from the mixin AND postgres.Model
|
|
@@ -1167,17 +1212,21 @@ When `DATABASE_URL` is set, it is parsed into the individual connection settings
|
|
|
1167
1212
|
|
|
1168
1213
|
Set `DATABASE_URL=none` to explicitly disable the database (e.g. during Docker image builds).
|
|
1169
1214
|
|
|
1170
|
-
| Setting
|
|
1171
|
-
|
|
|
1172
|
-
| `POSTGRES_HOST`
|
|
1173
|
-
| `POSTGRES_PORT`
|
|
1174
|
-
| `POSTGRES_DATABASE`
|
|
1175
|
-
| `POSTGRES_USER`
|
|
1176
|
-
| `POSTGRES_PASSWORD`
|
|
1177
|
-
| `POSTGRES_CONN_MAX_AGE`
|
|
1178
|
-
| `POSTGRES_CONN_HEALTH_CHECKS`
|
|
1179
|
-
| `POSTGRES_OPTIONS`
|
|
1180
|
-
| `POSTGRES_TIME_ZONE`
|
|
1215
|
+
| Setting | Type | Default | Env var |
|
|
1216
|
+
| ---------------------------------------- | ------------- | ------- | ---------------------------------------------- |
|
|
1217
|
+
| `POSTGRES_HOST` | `str` | — | `PLAIN_POSTGRES_HOST` |
|
|
1218
|
+
| `POSTGRES_PORT` | `int \| None` | `None` | `PLAIN_POSTGRES_PORT` |
|
|
1219
|
+
| `POSTGRES_DATABASE` | `str` | — | `PLAIN_POSTGRES_DATABASE` |
|
|
1220
|
+
| `POSTGRES_USER` | `str` | — | `PLAIN_POSTGRES_USER` |
|
|
1221
|
+
| `POSTGRES_PASSWORD` | `Secret[str]` | — | `PLAIN_POSTGRES_PASSWORD` |
|
|
1222
|
+
| `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
|
|
1223
|
+
| `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
|
|
1224
|
+
| `POSTGRES_OPTIONS` | `dict` | `{}` | — |
|
|
1225
|
+
| `POSTGRES_TIME_ZONE` | `str \| None` | `None` | `PLAIN_POSTGRES_TIME_ZONE` |
|
|
1226
|
+
| `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
|
|
1227
|
+
| `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
|
|
1228
|
+
| `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
|
|
1229
|
+
| `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
|
|
1181
1230
|
|
|
1182
1231
|
See [`default_settings.py`](./default_settings.py) for more details.
|
|
1183
1232
|
|
|
@@ -1185,7 +1234,19 @@ See [`default_settings.py`](./default_settings.py) for more details.
|
|
|
1185
1234
|
|
|
1186
1235
|
#### How do I add a field to an existing model?
|
|
1187
1236
|
|
|
1188
|
-
Add the field to your model class, then run `plain migrations create` to create a migration.
|
|
1237
|
+
Add the field to your model class, then run `plain migrations create` to create a migration.
|
|
1238
|
+
|
|
1239
|
+
If the field is required (no `default=` and not `allow_null=True`), the autodetector refuses to generate the migration, since there's no value to seed existing rows with. You have two options:
|
|
1240
|
+
|
|
1241
|
+
1. Declare a `default=` on the field so the new column has a value for existing rows.
|
|
1242
|
+
2. Add the field with `allow_null=True`, scaffold a data migration with `plain migrations create --empty --name backfill_<field>` to populate existing rows, then remove `allow_null=True` from the field — convergence applies `NOT NULL` on the next `postgres sync`.
|
|
1243
|
+
|
|
1244
|
+
#### How do I make an existing column `NOT NULL`?
|
|
1245
|
+
|
|
1246
|
+
Edit the field to remove `allow_null=True`. `plain migrations create` won't detect a schema change — nullability is managed by convergence. Run `plain postgres sync`:
|
|
1247
|
+
|
|
1248
|
+
- If the column has no `NULL` rows, convergence applies the change with a non-blocking `CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL` pattern.
|
|
1249
|
+
- If `NULL` rows exist, convergence blocks and prints the table and column to backfill. Scaffold a data migration with `plain migrations create --empty --name backfill_<field>`, write the backfill, and run `postgres sync` again.
|
|
1189
1250
|
|
|
1190
1251
|
#### How do I create a unique constraint on multiple fields?
|
|
1191
1252
|
|
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.96.0](https://github.com/dropseed/plain/releases/plain-postgres@0.96.0) (2026-04-17)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **`DateTimeField` gained `create_now=True` / `update_now=True` kwargs; `auto_now_add` and `auto_now` are removed.** `create_now=True` installs a persistent `DEFAULT STATEMENT_TIMESTAMP()` column default — raw-SQL inserts now get a value, not just ORM-driven ones. `update_now=True` stamps the column on every `save()` via `pre_save`. Preflight requires `update_now=True` to be paired with `create_now=True` or `allow_null=True` so existing rows have a backfill path. `default=` is no longer accepted on `DateTimeField`. ([5d145e4](https://github.com/dropseed/plain/commit/5d145e4), [a44e5ec](https://github.com/dropseed/plain/commit/a44e5ec), [091bac7](https://github.com/dropseed/plain/commit/091bac7))
|
|
8
|
+
- **`UUIDField` gained `generate=True`; `default=GenRandomUUID()` is no longer accepted.** `generate=True` installs `DEFAULT gen_random_uuid()` on the column, so Postgres produces a fresh UUID per row (raw-SQL inserts included). ([a44e5ec](https://github.com/dropseed/plain/commit/a44e5ec))
|
|
9
|
+
- **Added `RandomStringField(length=N)`** for per-row DB-generated random hex strings. Backed by a `DEFAULT` that slices `gen_random_uuid()::text`; use in place of Python `default=secrets.token_hex` callables for tokens, slugs, and short IDs. Alphabet is always hex — an earlier draft accepted `alphabet=` but it was dropped because the generated expression grew to ~4 KB for a 40-char token. ([34858ab](https://github.com/dropseed/plain/commit/34858ab), [0918702](https://github.com/dropseed/plain/commit/0918702))
|
|
10
|
+
- **Added `GenRandomUUID()` function.** Exported at `plain.postgres.functions.GenRandomUUID`. No longer valid as `default=`; use `UUIDField(generate=True)` or reference it in annotations/expressions. ([da58230](https://github.com/dropseed/plain/commit/da58230))
|
|
11
|
+
- **Callable `default=` is banned on model fields.** `default=uuid.uuid4`, `default=secrets.token_hex`, `default=dict`, `default=lambda: ...`, etc. raise `TypeError` at field construction. Use DB-side generation (`UUIDField(generate=True)`, `RandomStringField`, `DateTimeField(create_now=True)`) or a static literal. Empty-collection defaults use literal `{}` / `[]` — the value is deep-copied on each `get_default()` call. ([091bac7](https://github.com/dropseed/plain/commit/091bac7))
|
|
12
|
+
- **Literal `default=X` values now persist as column `DEFAULT` in the catalog and are reconciled by convergence.** Previously `default=` was Python-side only; now it is compiled to a DDL `DEFAULT <literal>` clause. Raw-SQL `INSERT`s get the default, and drift is detected if someone edits it out-of-band. ([c59473d](https://github.com/dropseed/plain/commit/c59473d), [6ed95fe](https://github.com/dropseed/plain/commit/6ed95fe), [161c7f9](https://github.com/dropseed/plain/commit/161c7f9))
|
|
13
|
+
- **Column nullability and DEFAULT transitions now go through convergence, not the schema editor.** `AlterField` is a no-op when only `allow_null` or `default=` changed; `plain postgres sync` applies the change with online-safe DDL (`CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL` for NOT NULL flips; catalog-only `SET`/`DROP DEFAULT` for default changes). The old 4-way NULL → NOT NULL backfill in the schema editor is gone — if a column has NULL rows, convergence now blocks with guidance instead of silently backfilling. ([3e10ab2](https://github.com/dropseed/plain/commit/3e10ab2), [c59473d](https://github.com/dropseed/plain/commit/c59473d))
|
|
14
|
+
- **Every framework-issued DDL statement now emits `SET LOCAL lock_timeout` and, where relevant, `SET LOCAL statement_timeout`.** Defaults are `3s` each and apply to both migration operations and convergence fixes. Non-blocking operations (`CREATE INDEX CONCURRENTLY`, `VALIDATE CONSTRAINT`) skip `statement_timeout`. Configure via new settings `POSTGRES_MIGRATION_LOCK_TIMEOUT`, `POSTGRES_MIGRATION_STATEMENT_TIMEOUT`, `POSTGRES_CONVERGENCE_LOCK_TIMEOUT`, `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` (all `PLAIN_POSTGRES_*` env-var compatible). `RunSQL(no_timeout=True)` opts a single operation out — useful for batched backfills that manage their own timeouts. ([11d903b](https://github.com/dropseed/plain/commit/11d903b))
|
|
15
|
+
- **The autodetector rejects unsafe column type changes.** Base-type changes outside a lossless widening allowlist (`smallint → integer`, `smallint → bigint`, `integer → bigint`) raise `MigrationSchemaError` with scaffold guidance instead of emitting an `AlterField` that would compile to a blind `ALTER COLUMN ... TYPE ... USING col::newtype`. Parameter-only changes (e.g. `max_length`) and the widening allowlist still auto-generate. ([073a9af](https://github.com/dropseed/plain/commit/073a9af))
|
|
16
|
+
- **The autodetector rejects adding a NOT NULL column without a default.** Previously Plain prompted interactively for a one-shot value; now the autodetector errors out with two remediation options: declare a `default=`, or add the field as nullable, backfill, and drop `allow_null=True` via convergence. The `MigrationQuestioner.ask_not_null_*` prompts are gone. ([091bac7](https://github.com/dropseed/plain/commit/091bac7))
|
|
17
|
+
- **`AddField` / `AlterField` no longer accept `preserve_default`.** The argument is removed from both operation classes and from `ProjectState.add_field` / `alter_field`. Existing migration files that pass it will fail to load — regenerate them or remove the kwarg. ([c0a117f](https://github.com/dropseed/plain/commit/c0a117f))
|
|
18
|
+
- **Backslashes are banned in string `default=` values.** `default=r"C:\path"` raises `ValueError` at construction to prevent spurious DEFAULT drift on every convergence run. ([f8b6227](https://github.com/dropseed/plain/commit/f8b6227))
|
|
19
|
+
- **`choices=` is now only accepted on `TextField` (and `TimeZoneField`).** Other fields (`IntegerField`, `BooleanField`, etc.) reject `choices=` at call time. ([01584dc](https://github.com/dropseed/plain/commit/01584dc))
|
|
20
|
+
- **Removed `IntegerChoices` and the `Choices` base class.** Only `TextChoices` remains; it now subclasses `str, enum.Enum` directly. ([96acf13](https://github.com/dropseed/plain/commit/96acf13))
|
|
21
|
+
- **`max_length=` is now only accepted on `TextField`, `BinaryField`, and `EncryptedTextField`.** Other fields reject it. ([aaa0fb6](https://github.com/dropseed/plain/commit/aaa0fb6))
|
|
22
|
+
- **`default=` is no longer accepted on `ForeignKeyField`, `ManyToManyField`, `BinaryField`, `EncryptedTextField`, or `EncryptedJSONField`.** ([60299dc](https://github.com/dropseed/plain/commit/60299dc), [99ba5c2](https://github.com/dropseed/plain/commit/99ba5c2))
|
|
23
|
+
- **`ManyToManyField` signature is now explicit** — it rejects `required=`, `allow_null=`, `default=`, and `validators=` with `TypeError`. ([be7fd86](https://github.com/dropseed/plain/commit/be7fd86))
|
|
24
|
+
- **Removed `error_messages=` from model fields and `ModelForm.Meta`.** Form-field `error_messages` is unchanged; this only affects the model layer. ([4dee5ec](https://github.com/dropseed/plain/commit/4dee5ec))
|
|
25
|
+
- **`PrimaryKeyField` takes no arguments.** It is always `bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL`. Removed kwargs for `required`, `allow_null`, `default`, and `validators`; the type stub now matches the runtime signature. ([ca122c9](https://github.com/dropseed/plain/commit/ca122c9), [0ecd71e](https://github.com/dropseed/plain/commit/0ecd71e))
|
|
26
|
+
- **`plain postgres sync --check` now prints pending work.** Previously `--check` only exited non-zero; it now enumerates pending migrations, convergence items, and blocked items with guidance. ([0de289d](https://github.com/dropseed/plain/commit/0de289d))
|
|
27
|
+
- **Fixed index drift false positive for `DESC` / `NULLS FIRST|LAST` columns.** Indexes like `Index(fields=["-created_at"])` were rebuilt on every `postgres sync` because the introspection parser misread the sort direction as an opclass. ([07cb500](https://github.com/dropseed/plain/commit/07cb500))
|
|
28
|
+
- **Fixed `Field.deconstruct()` over-shortening import paths** — `plain.postgres.fields.<submod>.X` now shortens to `plain.postgres.X` only when `X` is actually re-exported at the top level. ([34858ab](https://github.com/dropseed/plain/commit/34858ab))
|
|
29
|
+
- **`ModelForm` no longer marks DB-expression-default and auto-filled fields as `required`.** Fields with `db_returning=True` (e.g. `create_now=True`, `generate=True`, `RandomStringField`) and `auto_fills_on_save=True` (`update_now=True`) produce form fields with `required=False` and preserve the `DATABASE_DEFAULT` sentinel through `construct_instance` so INSERT emits `DEFAULT` instead of NULL on empty submissions. `modelfield_to_formfield` now returns `None` for non-column-backed fields (M2M, etc.). ([6ed95fe](https://github.com/dropseed/plain/commit/6ed95fe))
|
|
30
|
+
- **Internal restructuring.** `Field` is split into `ColumnField` → `DefaultableField` → `ChoicesField` with kwargs scoped to the fields that actually accept them. `plain.postgres.fields.__init__` is split into per-type modules (`base`, `text`, `numeric`, `temporal`, `boolean`, `binary`, `uuid`, `network`, `duration`, `primary_key`). `PrimaryKeyField` moved off the `BigIntegerField → IntegerField` chain onto `ColumnField[int]` directly. `non_db_attrs` renamed to `non_migration_attrs`. Removed dead Django-era internals: `SubqueryConstraint`, `MultiColSource`, multi-column FK machinery, multi-table-inheritance UPDATE machinery, `Field.description`, `Field.value_to_string()`, `Field.get_limit_choices_to()`. ([476e1ae](https://github.com/dropseed/plain/commit/476e1ae), [9ed8cc6](https://github.com/dropseed/plain/commit/9ed8cc6), [ca122c9](https://github.com/dropseed/plain/commit/ca122c9), [21cf85f](https://github.com/dropseed/plain/commit/21cf85f), [18080ca](https://github.com/dropseed/plain/commit/18080ca), [9d4ff49](https://github.com/dropseed/plain/commit/9d4ff49), [07b5f0b](https://github.com/dropseed/plain/commit/07b5f0b), [176f56e](https://github.com/dropseed/plain/commit/176f56e), [16e4fcd](https://github.com/dropseed/plain/commit/16e4fcd), [cb98bfa](https://github.com/dropseed/plain/commit/cb98bfa))
|
|
31
|
+
|
|
32
|
+
### Upgrade instructions
|
|
33
|
+
|
|
34
|
+
- **Replace `auto_now_add=True` with `create_now=True`** on every `DateTimeField`.
|
|
35
|
+
- **Replace `auto_now=True` with `update_now=True`**. If the field was `NOT NULL`, also set `create_now=True` (or `allow_null=True`) — preflight will fail otherwise.
|
|
36
|
+
- **Replace `DateTimeField(default=timezone.now)` / `default=Now()`** with `DateTimeField(create_now=True)`. `DateTimeField(default=...)` is no longer accepted.
|
|
37
|
+
- **Replace `UUIDField(default=uuid.uuid4)` and `UUIDField(default=GenRandomUUID())`** with `UUIDField(generate=True)`. `UUIDField(default=...)` is no longer accepted.
|
|
38
|
+
- **Replace `default=secrets.token_hex` / `default=secrets.token_urlsafe`** with `RandomStringField(length=N)` (hex output only).
|
|
39
|
+
- **Replace `default=dict` / `default=list`** with `default={}` / `default=[]`. Any other callable passed as `default=` will now raise `TypeError`.
|
|
40
|
+
- **Remove `choices=` from non-text fields** (`IntegerField`, `BooleanField`, etc.).
|
|
41
|
+
- **Replace `IntegerChoices` usages** with `TextChoices` or a plain `enum.IntEnum`. `Choices` (the base class) is also gone.
|
|
42
|
+
- **Remove `max_length=` from any field that isn't `TextField`, `BinaryField`, or `EncryptedTextField`.**
|
|
43
|
+
- **Remove `default=` from `ForeignKeyField`, `BinaryField`, `EncryptedTextField`, and `EncryptedJSONField`.**
|
|
44
|
+
- **Remove `required=`, `allow_null=`, `default=`, and `validators=` from `ManyToManyField`** — its signature is now explicit (`to`, `through`, `through_fields`, `related_query_name`, `limit_choices_to`, `symmetrical`).
|
|
45
|
+
- **Remove kwargs from `PrimaryKeyField()`** — it no longer accepts any.
|
|
46
|
+
- **Remove `error_messages=` from model-level fields and `ModelForm.Meta`.** (Form-field `error_messages` on standalone form fields is unchanged.)
|
|
47
|
+
- **Escape backslashes in string `default=` values.** `default="C:\\path"` is fine; `default=r"C:\path"` now raises at construction.
|
|
48
|
+
- **Edit or regenerate migration files that pass `preserve_default=...`** to `AddField` / `AlterField` — the kwarg was removed.
|
|
49
|
+
- **Rename `non_db_attrs` to `non_migration_attrs`** in any custom field subclass.
|
|
50
|
+
- **If your migrations hit the new 3s `statement_timeout`** against a large dev/staging DB, raise it for that run via `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT=30s`, or pass `RunSQL(sql, no_timeout=True)` on individual long-running operations.
|
|
51
|
+
- **Run `plain postgres sync`** after upgrading to let convergence install persisted column DEFAULTs on existing tables.
|
|
52
|
+
|
|
3
53
|
## [0.95.0](https://github.com/dropseed/plain/releases/plain-postgres@0.95.0) (2026-04-14)
|
|
4
54
|
|
|
5
55
|
### What's changed
|
|
@@ -36,7 +36,7 @@ class User(postgres.Model):
|
|
|
36
36
|
email: str = types.EmailField()
|
|
37
37
|
password = PasswordField()
|
|
38
38
|
is_admin: bool = types.BooleanField(default=False)
|
|
39
|
-
created_at: datetime = types.DateTimeField(
|
|
39
|
+
created_at: datetime = types.DateTimeField(create_now=True)
|
|
40
40
|
|
|
41
41
|
def __str__(self) -> str:
|
|
42
42
|
return self.email
|
|
@@ -440,35 +440,42 @@ Schema changes fall into three categories, each with a different author and appl
|
|
|
440
440
|
- **Structural migrations** — tables, columns, renames, column type changes. Framework-generated from the model diff, but you review them and decide when to deploy (a column type change can rewrite the table; a column drop is destructive).
|
|
441
441
|
- **Data migrations** — backfills, transformations, one-time cleanup. Authored by you via `RunPython` or `RunSQL`. The framework only sequences them.
|
|
442
442
|
|
|
443
|
-
| Change | Category | Safe apply pattern
|
|
444
|
-
| ------------------------------------- | -------------------- |
|
|
445
|
-
| Add / drop index | Convergence | `CREATE INDEX CONCURRENTLY`
|
|
446
|
-
| Add / drop unique or check constraint | Convergence | `ADD CONSTRAINT NOT VALID` + `VALIDATE`
|
|
447
|
-
| Add / remove NOT NULL | Convergence | `CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL`
|
|
448
|
-
| Change FK `on_delete` action | Convergence | drop + re-add with `NOT VALID` + `VALIDATE`
|
|
449
|
-
|
|
|
450
|
-
|
|
|
451
|
-
|
|
|
452
|
-
|
|
|
453
|
-
|
|
|
443
|
+
| Change | Category | Safe apply pattern |
|
|
444
|
+
| ------------------------------------- | -------------------- | --------------------------------------------------- |
|
|
445
|
+
| Add / drop index | Convergence | `CREATE INDEX CONCURRENTLY` |
|
|
446
|
+
| Add / drop unique or check constraint | Convergence | `ADD CONSTRAINT NOT VALID` + `VALIDATE` |
|
|
447
|
+
| Add / remove NOT NULL | Convergence | `CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL` |
|
|
448
|
+
| Change FK `on_delete` action | Convergence | drop + re-add with `NOT VALID` + `VALIDATE` |
|
|
449
|
+
| Set / change / drop column `DEFAULT` | Convergence | catalog-only `ALTER COLUMN SET/DROP DEFAULT` |
|
|
450
|
+
| Create / drop table | Structural migration | framework-generated, you review |
|
|
451
|
+
| Add / drop / rename column | Structural migration | framework-generated, you review |
|
|
452
|
+
| Column type change (safe widening) | Structural migration | framework-generated `ALTER TYPE` with implicit cast |
|
|
453
|
+
| Column type change (other) | Data migration | you author (explicit `RunSQL` with `USING`) |
|
|
454
|
+
| Data backfill or transformation | Data migration | you author (`RunPython` / `RunSQL`) |
|
|
455
|
+
| One-time cleanup, seeding | Data migration | you author |
|
|
454
456
|
|
|
455
457
|
**The principle: who authors the change, and can the framework guarantee safety?** If the framework can derive both the change and a universally-safe apply pattern from model definitions, it belongs to convergence. If the framework can generate the DDL but safety depends on context (table size, deploy timing, destructiveness), it's a structural migration — you review it before deploying. If only you know what to do, it's a data migration.
|
|
456
458
|
|
|
457
459
|
Many convergence-managed changes produce DB-enforced behavior — cascading deletes (`ON DELETE`), validation (`CHECK`, `NOT NULL`), default generation. Whether a change is "behavioral" doesn't determine the category; whether the framework can guarantee a safe apply does.
|
|
458
460
|
|
|
459
|
-
| Property
|
|
460
|
-
|
|
|
461
|
-
| Authored by
|
|
462
|
-
| When it runs
|
|
463
|
-
| Drift correction
|
|
464
|
-
| Reversible
|
|
465
|
-
|
|
|
466
|
-
|
|
|
461
|
+
| Property | Convergence | Migrations |
|
|
462
|
+
| ------------------------ | --------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
463
|
+
| Authored by | Framework (derived from models) | Framework (structural) or you (data) |
|
|
464
|
+
| When it runs | Every `sync`, by diffing models vs database | Once, in recorded timestamp order |
|
|
465
|
+
| Drift correction | Yes — reverts undeclared DB changes on next sync | No — manual DB changes persist |
|
|
466
|
+
| Reversible (intentional) | Implicit — roll back code, re-sync re-derives | No — forward-only, fix-forward |
|
|
467
|
+
| Failure behavior | Per-operation commits — partial progress on failure (re-run to retry) | Batch transaction — failure rolls back the entire migration |
|
|
468
|
+
| Files on disk | None — derived from models live | `.py` files in `migrations/` |
|
|
469
|
+
| Safe DDL | Framework-applied patterns (CONCURRENTLY, NOT VALID + VALIDATE) | Generated DDL; you review before deploy |
|
|
467
470
|
|
|
468
471
|
**Drift correction is a convergence-only behavior.** Convergence re-runs on every `sync` and compares models against the database. An index created manually outside a model declaration will be dropped on the next run because models are the source of truth. Migrations don't behave this way — once applied, they're recorded and never re-applied.
|
|
469
472
|
|
|
470
473
|
**Caveats.** The safety promise isn't absolute. Structural migrations aren't lint-checked yet: adding a column with a volatile default (`gen_random_uuid()`, `now()`) on a large table will rewrite it without warning. Review structural migrations before deploying to production.
|
|
471
474
|
|
|
475
|
+
**Column type changes.** The autodetector only auto-generates `AlterField` for a small allowlist of lossless widenings (`smallint → integer`, `smallint → bigint`, `integer → bigint`) and for parameter-only changes like `max_length`. Every other base-type change rejects with guidance — arbitrary `USING col::newtype` casts either fail at apply time (e.g. timestamp → uuid) or silently corrupt data (e.g. bigint FK → text stringifies PKs), and migrations are forward-only. For anything outside the allowlist, scaffold `plain migrations create --empty --name alter_<model>_<field>_type` and author an explicit `RunSQL` with a `USING` expression you've reviewed.
|
|
476
|
+
|
|
477
|
+
"Safe" here means data-integrity safe, not operationally cheap. An `ALTER COLUMN ... TYPE` that changes on-disk width (any of the allowlisted widenings) takes `ACCESS EXCLUSIVE` and rewrites the table — on a large table this can block writes for minutes. Deploy these during a maintenance window, not in the middle of traffic.
|
|
478
|
+
|
|
472
479
|
**Out of scope for convergence.** Triggers, views, stored procedures, and other non-standard DDL stay outside convergence — it won't create them from models, and it won't drop them if they exist. Manage them with `RunSQL` data migrations.
|
|
473
480
|
|
|
474
481
|
### Syncing
|
|
@@ -677,6 +684,43 @@ Some changes can't be applied automatically. For example, if you add `NOT NULL`
|
|
|
677
684
|
|
|
678
685
|
When you remove an index or constraint from a model, convergence automatically drops the undeclared database object on the next `postgres sync`. Models are the source of truth — if it's not declared, it gets removed.
|
|
679
686
|
|
|
687
|
+
### DDL timeouts
|
|
688
|
+
|
|
689
|
+
Every framework-issued DDL statement — both in migrations and in convergence — is wrapped with `lock_timeout` and `statement_timeout` so a deploy can't hang indefinitely waiting for a lock, and so a backfill against an unexpectedly large table fails fast instead of holding `ACCESS EXCLUSIVE` for minutes.
|
|
690
|
+
|
|
691
|
+
```python
|
|
692
|
+
# app/settings.py — defaults shown
|
|
693
|
+
POSTGRES_MIGRATION_LOCK_TIMEOUT = "3s"
|
|
694
|
+
POSTGRES_MIGRATION_STATEMENT_TIMEOUT = "3s"
|
|
695
|
+
POSTGRES_CONVERGENCE_LOCK_TIMEOUT = "3s"
|
|
696
|
+
POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT = "3s"
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`lock_timeout` applies to every DDL. `statement_timeout` applies only to statements that take `ACCESS EXCLUSIVE` — non-blocking operations (`CREATE INDEX CONCURRENTLY`, `VALIDATE CONSTRAINT`) run unbounded because they can't cascade the lock queue.
|
|
700
|
+
|
|
701
|
+
If a migration issues a row-touching UPDATE (e.g. a hand-written `RunPython` or `RunSQL` backfill), the 3s `statement_timeout` will kill it on any non-tiny table. That's intentional — the right fix is a batched data migration, not a single long-running UPDATE. The common first-time failure mode is applying migrations against a pre-seeded dev or staging database: raise the ceiling for that one run, then lower it back for production deploys.
|
|
702
|
+
|
|
703
|
+
Use `RunSQL(no_timeout=True)` to opt out for a specific operation:
|
|
704
|
+
|
|
705
|
+
```python
|
|
706
|
+
from plain.postgres.migrations.operations import RunSQL
|
|
707
|
+
|
|
708
|
+
operations = [
|
|
709
|
+
RunSQL(
|
|
710
|
+
"UPDATE orders SET status = 'pending' WHERE status IS NULL",
|
|
711
|
+
no_timeout=True,
|
|
712
|
+
),
|
|
713
|
+
]
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Non-atomic migrations (`Migration.atomic = False`, used for `CREATE INDEX CONCURRENTLY` in a migration) skip the timeout prelude automatically — `SET LOCAL` is a no-op outside a transaction block. Manage timeouts inside your own `RunSQL` if you need them.
|
|
717
|
+
|
|
718
|
+
Environment overrides: every setting accepts `PLAIN_POSTGRES_*` env vars, so you can raise the ceiling for a specific deploy without a code change:
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT=30s plain migrations apply
|
|
722
|
+
```
|
|
723
|
+
|
|
680
724
|
## Fields
|
|
681
725
|
|
|
682
726
|
You can use many field types for different data:
|
|
@@ -701,8 +745,8 @@ class Product(postgres.Model):
|
|
|
701
745
|
is_active: bool = types.BooleanField(default=True)
|
|
702
746
|
|
|
703
747
|
# Date and time fields
|
|
704
|
-
created_at: datetime = types.DateTimeField(
|
|
705
|
-
updated_at: datetime = types.DateTimeField(
|
|
748
|
+
created_at: datetime = types.DateTimeField(create_now=True)
|
|
749
|
+
updated_at: datetime = types.DateTimeField(update_now=True)
|
|
706
750
|
```
|
|
707
751
|
|
|
708
752
|
**Text fields:**
|
|
@@ -730,10 +774,11 @@ class Product(postgres.Model):
|
|
|
730
774
|
**Other fields:**
|
|
731
775
|
|
|
732
776
|
- [`BooleanField`](./fields/__init__.py#BooleanField) - True/False
|
|
733
|
-
- [`UUIDField`](./fields/__init__.py#UUIDField) - UUID
|
|
777
|
+
- [`UUIDField`](./fields/__init__.py#UUIDField) - UUID (pass `generate=True` for a per-row `gen_random_uuid()` default)
|
|
734
778
|
- [`BinaryField`](./fields/__init__.py#BinaryField) - Raw binary data
|
|
735
779
|
- [`JSONField`](./fields/json.py#JSONField) - JSON data
|
|
736
780
|
- [`GenericIPAddressField`](./fields/__init__.py#GenericIPAddressField) - IPv4 or IPv6 address
|
|
781
|
+
- [`RandomStringField`](./fields/text.py#RandomStringField) - Per-row random hex string generated by Postgres (`length=`) — use for tokens, slugs, short IDs instead of a Python callable default. Slices `gen_random_uuid()` directly; values are uniform hex characters
|
|
737
782
|
|
|
738
783
|
**Encrypted fields:**
|
|
739
784
|
|
|
@@ -763,8 +808,8 @@ from plain.postgres import types
|
|
|
763
808
|
|
|
764
809
|
# Regular Python class for shared fields
|
|
765
810
|
class TimestampedMixin:
|
|
766
|
-
created_at: datetime = types.DateTimeField(
|
|
767
|
-
updated_at: datetime = types.DateTimeField(
|
|
811
|
+
created_at: datetime = types.DateTimeField(create_now=True)
|
|
812
|
+
updated_at: datetime = types.DateTimeField(update_now=True)
|
|
768
813
|
|
|
769
814
|
|
|
770
815
|
# Models inherit from the mixin AND postgres.Model
|
|
@@ -1155,17 +1200,21 @@ When `DATABASE_URL` is set, it is parsed into the individual connection settings
|
|
|
1155
1200
|
|
|
1156
1201
|
Set `DATABASE_URL=none` to explicitly disable the database (e.g. during Docker image builds).
|
|
1157
1202
|
|
|
1158
|
-
| Setting
|
|
1159
|
-
|
|
|
1160
|
-
| `POSTGRES_HOST`
|
|
1161
|
-
| `POSTGRES_PORT`
|
|
1162
|
-
| `POSTGRES_DATABASE`
|
|
1163
|
-
| `POSTGRES_USER`
|
|
1164
|
-
| `POSTGRES_PASSWORD`
|
|
1165
|
-
| `POSTGRES_CONN_MAX_AGE`
|
|
1166
|
-
| `POSTGRES_CONN_HEALTH_CHECKS`
|
|
1167
|
-
| `POSTGRES_OPTIONS`
|
|
1168
|
-
| `POSTGRES_TIME_ZONE`
|
|
1203
|
+
| Setting | Type | Default | Env var |
|
|
1204
|
+
| ---------------------------------------- | ------------- | ------- | ---------------------------------------------- |
|
|
1205
|
+
| `POSTGRES_HOST` | `str` | — | `PLAIN_POSTGRES_HOST` |
|
|
1206
|
+
| `POSTGRES_PORT` | `int \| None` | `None` | `PLAIN_POSTGRES_PORT` |
|
|
1207
|
+
| `POSTGRES_DATABASE` | `str` | — | `PLAIN_POSTGRES_DATABASE` |
|
|
1208
|
+
| `POSTGRES_USER` | `str` | — | `PLAIN_POSTGRES_USER` |
|
|
1209
|
+
| `POSTGRES_PASSWORD` | `Secret[str]` | — | `PLAIN_POSTGRES_PASSWORD` |
|
|
1210
|
+
| `POSTGRES_CONN_MAX_AGE` | `int` | `600` | `PLAIN_POSTGRES_CONN_MAX_AGE` |
|
|
1211
|
+
| `POSTGRES_CONN_HEALTH_CHECKS` | `bool` | `True` | `PLAIN_POSTGRES_CONN_HEALTH_CHECKS` |
|
|
1212
|
+
| `POSTGRES_OPTIONS` | `dict` | `{}` | — |
|
|
1213
|
+
| `POSTGRES_TIME_ZONE` | `str \| None` | `None` | `PLAIN_POSTGRES_TIME_ZONE` |
|
|
1214
|
+
| `POSTGRES_MIGRATION_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_LOCK_TIMEOUT` |
|
|
1215
|
+
| `POSTGRES_MIGRATION_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_MIGRATION_STATEMENT_TIMEOUT` |
|
|
1216
|
+
| `POSTGRES_CONVERGENCE_LOCK_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_LOCK_TIMEOUT` |
|
|
1217
|
+
| `POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` | `str` | `"3s"` | `PLAIN_POSTGRES_CONVERGENCE_STATEMENT_TIMEOUT` |
|
|
1169
1218
|
|
|
1170
1219
|
See [`default_settings.py`](./default_settings.py) for more details.
|
|
1171
1220
|
|
|
@@ -1173,7 +1222,19 @@ See [`default_settings.py`](./default_settings.py) for more details.
|
|
|
1173
1222
|
|
|
1174
1223
|
#### How do I add a field to an existing model?
|
|
1175
1224
|
|
|
1176
|
-
Add the field to your model class, then run `plain migrations create` to create a migration.
|
|
1225
|
+
Add the field to your model class, then run `plain migrations create` to create a migration.
|
|
1226
|
+
|
|
1227
|
+
If the field is required (no `default=` and not `allow_null=True`), the autodetector refuses to generate the migration, since there's no value to seed existing rows with. You have two options:
|
|
1228
|
+
|
|
1229
|
+
1. Declare a `default=` on the field so the new column has a value for existing rows.
|
|
1230
|
+
2. Add the field with `allow_null=True`, scaffold a data migration with `plain migrations create --empty --name backfill_<field>` to populate existing rows, then remove `allow_null=True` from the field — convergence applies `NOT NULL` on the next `postgres sync`.
|
|
1231
|
+
|
|
1232
|
+
#### How do I make an existing column `NOT NULL`?
|
|
1233
|
+
|
|
1234
|
+
Edit the field to remove `allow_null=True`. `plain migrations create` won't detect a schema change — nullability is managed by convergence. Run `plain postgres sync`:
|
|
1235
|
+
|
|
1236
|
+
- If the column has no `NULL` rows, convergence applies the change with a non-blocking `CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL` pattern.
|
|
1237
|
+
- If `NULL` rows exist, convergence blocks and prints the table and column to backfill. Scaffold a data migration with `plain migrations create --empty --name backfill_<field>`, write the backfill, and run `postgres sync` again.
|
|
1177
1238
|
|
|
1178
1239
|
#### How do I create a unique constraint on multiple fields?
|
|
1179
1240
|
|
|
@@ -9,7 +9,7 @@ from .constraints import CheckConstraint, UniqueConstraint
|
|
|
9
9
|
from .db import get_connection
|
|
10
10
|
from .deletion import CASCADE, NO_ACTION, RESTRICT, SET_NULL
|
|
11
11
|
from .expressions import F
|
|
12
|
-
from .enums import
|
|
12
|
+
from .enums import TextChoices
|
|
13
13
|
from .fields import (
|
|
14
14
|
BigIntegerField,
|
|
15
15
|
BinaryField,
|
|
@@ -23,6 +23,7 @@ from .fields import (
|
|
|
23
23
|
GenericIPAddressField,
|
|
24
24
|
IntegerField,
|
|
25
25
|
PrimaryKeyField,
|
|
26
|
+
RandomStringField,
|
|
26
27
|
SmallIntegerField,
|
|
27
28
|
TextField,
|
|
28
29
|
TimeField,
|
|
@@ -54,7 +55,6 @@ __all__ = [
|
|
|
54
55
|
"CheckConstraint",
|
|
55
56
|
"UniqueConstraint",
|
|
56
57
|
# From enums
|
|
57
|
-
"IntegerChoices",
|
|
58
58
|
"TextChoices",
|
|
59
59
|
# From fields
|
|
60
60
|
"BigIntegerField",
|
|
@@ -69,6 +69,7 @@ __all__ = [
|
|
|
69
69
|
"GenericIPAddressField",
|
|
70
70
|
"IntegerField",
|
|
71
71
|
"PrimaryKeyField",
|
|
72
|
+
"RandomStringField",
|
|
72
73
|
"SmallIntegerField",
|
|
73
74
|
"TextField",
|
|
74
75
|
"TimeField",
|