plain.postgres 0.100.0__tar.gz → 0.102.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.100.0 → plain_postgres-0.102.0}/.gitignore +2 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/PKG-INFO +1 -2
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/CHANGELOG.md +25 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/__init__.py +4 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +4 -4
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/base.py +5 -13
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/connection.py +12 -37
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/constraints.py +32 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/convergence/__init__.py +2 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/convergence/analysis.py +398 -236
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/convergence/fixes.py +16 -6
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/base.py +5 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/related_managers.py +6 -6
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/indexes.py +1 -1
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/__init__.py +0 -10
- plain_postgres-0.102.0/plain/postgres/introspection/schema.py +201 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/base.py +3 -2
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/query.py +2 -2
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/compiler.py +5 -5
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/query.py +15 -8
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/pyproject.toml +2 -2
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_constraint_violation_error.py +43 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_constraints.py +276 -15
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_indexes.py +453 -41
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_timeouts.py +5 -2
- plain_postgres-0.102.0/tests/test_introspection.py +226 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_literal_default_persistence.py +122 -29
- plain_postgres-0.100.0/plain/postgres/introspection/schema.py +0 -584
- plain_postgres-0.100.0/tests/test_introspection.py +0 -456
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/CLAUDE.md +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/LICENSE +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/README.md +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/README.md +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.100.0/tests/app/examples/migrations → plain_postgres-0.102.0/plain/postgres/test}/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/types.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.100.0/plain/postgres/test → plain_postgres-0.102.0/tests/app/examples/migrations}/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/conftest.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_connection_isolation.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_connection_pool.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_defaults.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_fk.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_convergence_nullability.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_database_url.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_delete_behaviors.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_diagnose.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_encrypted_fields.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_exceptions.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_field_defaults.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_functions_uuid.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_health.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_iterator.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_m2m.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_management_connection.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_manager_assignment.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_migration_executor.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_mixins.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_otel_metrics.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_random_string_field.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_raw_query.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_read_only_transactions.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_related.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.100.0 → plain_postgres-0.102.0}/tests/test_schema_timeouts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.102.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
|
|
@@ -9,7 +9,6 @@ Requires-Python: >=3.13
|
|
|
9
9
|
Requires-Dist: plain<1.0.0,>=0.134.0
|
|
10
10
|
Requires-Dist: psycopg-pool>=3.2
|
|
11
11
|
Requires-Dist: psycopg>=3.2
|
|
12
|
-
Requires-Dist: sqlparse>=0.3.1
|
|
13
12
|
Description-Content-Type: text/markdown
|
|
14
13
|
|
|
15
14
|
# plain.postgres
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.102.0](https://github.com/dropseed/plain/releases/plain-postgres@0.102.0) (2026-05-05)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **`Model.query` is now bound to `Self` (PEP 673), so subclasses specialize automatically.** `User.query` types as `QuerySet[User]` and `User.query.first()` as `User | None` without per-model annotations. Custom `QuerySet` subclasses (e.g. `TaskQuerySet`) are still preserved by the existing `Self`-returning descriptor. Now-redundant `cast(T, ...)` wrappers in the FK/M2M related managers are gone — `self.model.query.create(...)` already types as `T`. ([0f5b2f66](https://github.com/dropseed/plain/commit/0f5b2f66))
|
|
8
|
+
- **Convergence diffs are now canonicalized through Postgres `pg_get_*` round-trips on a session-private temp table** instead of sqlparse-based text normalization. Both sides of every index/constraint/default comparison are deparsed by Postgres itself, eliminating false-positive drift from formatting differences. Adds `ReadOnlyConnectionError` when the round-trip can't get DDL. The `normalize_check_definition`, `normalize_default_sql`, `normalize_expression`, `normalize_index_definition`, and `normalize_unique_definition` helpers are removed from `plain.postgres.introspection`, and `sqlparse` is no longer a dependency. ([4b42b4d1](https://github.com/dropseed/plain/commit/4b42b4d1))
|
|
9
|
+
- **`CheckConstraint.validate()` now exits early when a referenced field is missing from the value map**, deferring to the field-level error that excluded it. Calling `full_clean()` on a model with both `choices=` and a `CheckConstraint` referencing the same field used to crash with `AssertionError: Field lookups require a model` — the choice error excluded the field, then constraint validation tried to resolve the missing annotation. The walker is exposed as the public `CheckConstraint.referenced_fields()` method. ([d13f47d1](https://github.com/dropseed/plain/commit/d13f47d1))
|
|
10
|
+
- Tightened class-level annotations on `Query.select` and friends, `Operation.atomic`, and `ChoicesField.choices` for ty 0.0.33; replaced the `ModelState` `fields_cache` descriptor with a plain `__init__`. ([4b9d1db1](https://github.com/dropseed/plain/commit/4b9d1db1))
|
|
11
|
+
- Exposes `__version__` from `importlib.metadata` on `plain.postgres`. ([c6cf6edb](https://github.com/dropseed/plain/commit/c6cf6edb))
|
|
12
|
+
|
|
13
|
+
### Upgrade instructions
|
|
14
|
+
|
|
15
|
+
- If you imported any of `normalize_check_definition`, `normalize_default_sql`, `normalize_expression`, `normalize_index_definition`, or `normalize_unique_definition` from `plain.postgres.introspection`, those helpers are gone — use `pg_get_indexdef` / `pg_get_constraintdef` directly or rely on the new convergence round-trip path.
|
|
16
|
+
|
|
17
|
+
## [0.101.0](https://github.com/dropseed/plain/releases/plain-postgres@0.101.0) (2026-04-30)
|
|
18
|
+
|
|
19
|
+
### What's changed
|
|
20
|
+
|
|
21
|
+
- **Validate CHECK constraints in the same converge run that adds them.** `AddConstraintFix` now runs `ALTER TABLE ... ADD CONSTRAINT ... NOT VALID` followed by `ALTER TABLE ... VALIDATE CONSTRAINT` in a single `apply()`. The add is catalog-only (brief lock) and validate uses `SHARE UPDATE EXCLUSIVE` (doesn't block writes), so there's no benefit to deferring validation to a later run. Existing rows are checked before convergence reports success — previously, a CHECK constraint could be added in `NOT VALID` state and the validation step was its own follow-up fix. ([dc7eb8d3c2b7](https://github.com/dropseed/plain/commit/dc7eb8d3c2b7))
|
|
22
|
+
- `plain-postgres` rule references updated for the simpler `plain docs` CLI (no more `--section`). ([e03c3bd8b6d3](https://github.com/dropseed/plain/commit/e03c3bd8b6d3))
|
|
23
|
+
|
|
24
|
+
### Upgrade instructions
|
|
25
|
+
|
|
26
|
+
- No changes required. The next `plain postgres sync` (or scheduled converge run) on a database with pending CHECK constraints will now both add and validate them in one step instead of two.
|
|
27
|
+
|
|
3
28
|
## [0.100.0](https://github.com/dropseed/plain/releases/plain-postgres@0.100.0) (2026-04-28)
|
|
4
29
|
|
|
5
30
|
### What's changed
|
|
@@ -42,7 +42,7 @@ This means: when you add an `Index` or `UniqueConstraint` to a model, no migrati
|
|
|
42
42
|
|
|
43
43
|
For custom data migrations, use `uv run plain migrations create --empty --name <name>` to scaffold the file.
|
|
44
44
|
|
|
45
|
-
Run `uv run plain docs postgres
|
|
45
|
+
Run `uv run plain docs postgres` for full workflow details.
|
|
46
46
|
|
|
47
47
|
## Querying
|
|
48
48
|
|
|
@@ -57,7 +57,7 @@ Use `Model.query` to build querysets (e.g., `User.query.filter(is_active=True)`)
|
|
|
57
57
|
- Wrap multi-step writes in `transaction.atomic()`
|
|
58
58
|
- Always paginate list queries — unbounded querysets get slower as data grows
|
|
59
59
|
|
|
60
|
-
Run `uv run plain docs postgres
|
|
60
|
+
Run `uv run plain docs postgres` for full patterns with code examples.
|
|
61
61
|
|
|
62
62
|
## Schema Design
|
|
63
63
|
|
|
@@ -68,13 +68,13 @@ Run `uv run plain docs postgres --section querying` for full patterns with code
|
|
|
68
68
|
- Choose `on_delete` deliberately: CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional references
|
|
69
69
|
- No `allow_null` on string fields — use `default=""`
|
|
70
70
|
|
|
71
|
-
Run `uv run plain docs postgres
|
|
71
|
+
Run `uv run plain docs postgres` for full patterns with code examples.
|
|
72
72
|
|
|
73
73
|
## Database Doctor
|
|
74
74
|
|
|
75
75
|
Use the `/plain-postgres-doctor` skill to check overall database health — migration sync, schema correctness, and operational health.
|
|
76
76
|
|
|
77
|
-
Run `uv run plain docs postgres
|
|
77
|
+
Run `uv run plain docs postgres` for check details, thresholds, and production usage.
|
|
78
78
|
|
|
79
79
|
## Differences from Django
|
|
80
80
|
|
|
@@ -4,7 +4,7 @@ import copy
|
|
|
4
4
|
import warnings
|
|
5
5
|
from collections.abc import Iterable, Iterator, Sequence
|
|
6
6
|
from itertools import chain
|
|
7
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from plain.postgres.meta import Meta
|
|
@@ -68,16 +68,6 @@ class ModelBase(type):
|
|
|
68
68
|
return super().__new__(cls, name, bases, attrs, **kwargs)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
class ModelStateFieldsCacheDescriptor:
|
|
72
|
-
def __get__(
|
|
73
|
-
self, instance: ModelState | None, cls: type | None = None
|
|
74
|
-
) -> ModelStateFieldsCacheDescriptor | dict[str, Any]:
|
|
75
|
-
if instance is None:
|
|
76
|
-
return self
|
|
77
|
-
res = instance.fields_cache = {}
|
|
78
|
-
return res
|
|
79
|
-
|
|
80
|
-
|
|
81
71
|
class ModelState:
|
|
82
72
|
"""Store model instance state."""
|
|
83
73
|
|
|
@@ -86,7 +76,9 @@ class ModelState:
|
|
|
86
76
|
# explicit (non-auto) PKs. This impacts validation only; it has no effect
|
|
87
77
|
# on the actual save.
|
|
88
78
|
adding = True
|
|
89
|
-
|
|
79
|
+
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
self.fields_cache: dict[str, Any] = {}
|
|
90
82
|
|
|
91
83
|
|
|
92
84
|
class Model(metaclass=ModelBase):
|
|
@@ -94,7 +86,7 @@ class Model(metaclass=ModelBase):
|
|
|
94
86
|
id: int = types.PrimaryKeyField()
|
|
95
87
|
|
|
96
88
|
# Descriptors for other model behavior
|
|
97
|
-
query: QuerySet[
|
|
89
|
+
query: QuerySet[Self] = QuerySet()
|
|
98
90
|
model_options: Options = Options()
|
|
99
91
|
_model_meta: Meta = Meta()
|
|
100
92
|
DoesNotExist = DoesNotExistDescriptor()
|
|
@@ -15,7 +15,6 @@ from plain.logs import get_framework_logger
|
|
|
15
15
|
from plain.postgres import utils
|
|
16
16
|
from plain.postgres.dialect import quote_name
|
|
17
17
|
from plain.postgres.fields import GenericIPAddressField, TimeField, UUIDField
|
|
18
|
-
from plain.postgres.indexes import Index
|
|
19
18
|
from plain.postgres.schema import DatabaseSchemaEditor
|
|
20
19
|
from plain.postgres.sources import ConnectionSource
|
|
21
20
|
from plain.postgres.transaction import TransactionManagementError
|
|
@@ -63,7 +62,6 @@ class DatabaseConnection:
|
|
|
63
62
|
|
|
64
63
|
queries_limit: int = 9000
|
|
65
64
|
|
|
66
|
-
index_default_access_method = "btree"
|
|
67
65
|
ignored_tables: list[str] = []
|
|
68
66
|
|
|
69
67
|
def __init__(self, source: ConnectionSource):
|
|
@@ -607,7 +605,6 @@ class DatabaseConnection:
|
|
|
607
605
|
FROM pg_attribute AS fka
|
|
608
606
|
JOIN pg_class AS fkc ON fka.attrelid = fkc.oid
|
|
609
607
|
WHERE fka.attrelid = c.confrelid AND fka.attnum = c.confkey[1]),
|
|
610
|
-
cl.reloptions,
|
|
611
608
|
c.convalidated,
|
|
612
609
|
pg_get_constraintdef(c.oid),
|
|
613
610
|
c.confdeltype
|
|
@@ -622,54 +619,42 @@ class DatabaseConnection:
|
|
|
622
619
|
columns,
|
|
623
620
|
kind,
|
|
624
621
|
used_cols,
|
|
625
|
-
options,
|
|
626
622
|
validated,
|
|
627
623
|
constraintdef,
|
|
628
624
|
confdeltype,
|
|
629
625
|
) in cursor.fetchall():
|
|
630
626
|
constraints[constraint] = {
|
|
631
627
|
"columns": columns,
|
|
632
|
-
"primary_key": kind == "p",
|
|
633
|
-
"unique": kind in ["p", "u"],
|
|
634
628
|
"foreign_key": tuple(used_cols.split(".", 1)) if kind == "f" else None,
|
|
635
|
-
"check": kind == "c",
|
|
636
629
|
"contype": kind,
|
|
637
630
|
"index": False,
|
|
638
631
|
"definition": constraintdef,
|
|
639
|
-
"options": options,
|
|
640
632
|
"validated": validated,
|
|
641
633
|
"on_delete_action": confdeltype if kind == "f" else None,
|
|
642
634
|
}
|
|
643
|
-
# Now get indexes
|
|
635
|
+
# Now get indexes. Sort order, opclasses, INCLUDE, and predicates all
|
|
636
|
+
# ride along inside `pg_get_indexdef` and are compared via the
|
|
637
|
+
# canonical-tail round-trip in convergence — no need to introspect
|
|
638
|
+
# them here as separate columns.
|
|
644
639
|
cursor.execute(
|
|
645
640
|
"""
|
|
646
641
|
SELECT
|
|
647
642
|
indexname,
|
|
648
643
|
array_agg(attname ORDER BY arridx),
|
|
649
644
|
indisunique,
|
|
650
|
-
indisprimary,
|
|
651
|
-
array_agg(ordering ORDER BY arridx),
|
|
652
645
|
amname,
|
|
653
646
|
exprdef,
|
|
654
|
-
|
|
655
|
-
s2.indisvalid
|
|
647
|
+
indisvalid
|
|
656
648
|
FROM (
|
|
657
649
|
SELECT
|
|
658
650
|
c2.relname as indexname, idx.*, attr.attname, am.amname,
|
|
659
|
-
pg_get_indexdef(idx.indexrelid) AS exprdef
|
|
660
|
-
CASE am.amname
|
|
661
|
-
WHEN %s THEN
|
|
662
|
-
CASE (option & 1)
|
|
663
|
-
WHEN 1 THEN 'DESC' ELSE 'ASC'
|
|
664
|
-
END
|
|
665
|
-
END as ordering,
|
|
666
|
-
c2.reloptions as attoptions
|
|
651
|
+
pg_get_indexdef(idx.indexrelid) AS exprdef
|
|
667
652
|
FROM (
|
|
668
653
|
SELECT *
|
|
669
654
|
FROM
|
|
670
655
|
pg_index i,
|
|
671
|
-
unnest(i.indkey
|
|
672
|
-
WITH ORDINALITY koi(key,
|
|
656
|
+
unnest(i.indkey)
|
|
657
|
+
WITH ORDINALITY koi(key, arridx)
|
|
673
658
|
) idx
|
|
674
659
|
LEFT JOIN pg_class c ON idx.indrelid = c.oid
|
|
675
660
|
LEFT JOIN pg_class c2 ON idx.indexrelid = c2.oid
|
|
@@ -678,36 +663,26 @@ class DatabaseConnection:
|
|
|
678
663
|
pg_attribute attr ON attr.attrelid = c.oid AND attr.attnum = idx.key
|
|
679
664
|
WHERE c.relname = %s AND pg_catalog.pg_table_is_visible(c.oid)
|
|
680
665
|
) s2
|
|
681
|
-
GROUP BY
|
|
666
|
+
GROUP BY
|
|
667
|
+
indexname, indisunique, amname, exprdef, indisvalid;
|
|
682
668
|
""",
|
|
683
|
-
[
|
|
669
|
+
[table_name],
|
|
684
670
|
)
|
|
685
671
|
for (
|
|
686
672
|
index,
|
|
687
673
|
columns,
|
|
688
674
|
unique,
|
|
689
|
-
primary,
|
|
690
|
-
orders,
|
|
691
675
|
type_,
|
|
692
676
|
definition,
|
|
693
|
-
options,
|
|
694
677
|
valid,
|
|
695
678
|
) in cursor.fetchall():
|
|
696
679
|
if index not in constraints:
|
|
697
|
-
basic_index = (
|
|
698
|
-
type_ == self.index_default_access_method and options is None
|
|
699
|
-
)
|
|
700
680
|
constraints[index] = {
|
|
701
681
|
"columns": columns if columns != [None] else [],
|
|
702
|
-
"orders": orders if orders != [None] else [],
|
|
703
|
-
"primary_key": primary,
|
|
704
682
|
"unique": unique,
|
|
705
|
-
"foreign_key": None,
|
|
706
|
-
"check": False,
|
|
707
683
|
"index": True,
|
|
708
|
-
"type":
|
|
684
|
+
"type": type_,
|
|
709
685
|
"definition": definition,
|
|
710
|
-
"options": options,
|
|
711
686
|
"valid": valid,
|
|
712
687
|
}
|
|
713
688
|
return constraints
|
|
@@ -5,6 +5,7 @@ from types import NoneType
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from plain.exceptions import ValidationError
|
|
8
|
+
from plain.postgres.constants import LOOKUP_SEP
|
|
8
9
|
from plain.postgres.ddl import (
|
|
9
10
|
build_include_sql,
|
|
10
11
|
compile_expression_sql,
|
|
@@ -104,10 +105,41 @@ class CheckConstraint(BaseConstraint):
|
|
|
104
105
|
sql += " NOT VALID"
|
|
105
106
|
return sql
|
|
106
107
|
|
|
108
|
+
def referenced_fields(self) -> set[str]:
|
|
109
|
+
"""Top-level model field names referenced by `self.check`.
|
|
110
|
+
|
|
111
|
+
Walks lookup keys (`field__regex` → `field`), nested Q nodes, and
|
|
112
|
+
F-expressions in values or other source expressions.
|
|
113
|
+
"""
|
|
114
|
+
fields: set[str] = set()
|
|
115
|
+
|
|
116
|
+
def visit(node: Any) -> None:
|
|
117
|
+
if isinstance(node, Q):
|
|
118
|
+
for child in node.children:
|
|
119
|
+
visit(child)
|
|
120
|
+
elif isinstance(node, tuple) and len(node) == 2:
|
|
121
|
+
lookup, value = node
|
|
122
|
+
fields.add(lookup.split(LOOKUP_SEP, 1)[0])
|
|
123
|
+
visit(value)
|
|
124
|
+
elif isinstance(node, F):
|
|
125
|
+
fields.add(node.name.split(LOOKUP_SEP, 1)[0])
|
|
126
|
+
elif hasattr(node, "get_source_expressions"):
|
|
127
|
+
for sub in node.get_source_expressions():
|
|
128
|
+
visit(sub)
|
|
129
|
+
|
|
130
|
+
visit(self.check)
|
|
131
|
+
return fields
|
|
132
|
+
|
|
107
133
|
def validate(
|
|
108
134
|
self, model: type[Model], instance: Model, exclude: set[str] | None = None
|
|
109
135
|
) -> None:
|
|
110
136
|
against = instance._get_field_value_map(meta=model._model_meta, exclude=exclude)
|
|
137
|
+
# Skip the check entirely when any field referenced by `self.check` was
|
|
138
|
+
# excluded — the in-Python pipeline can't resolve a missing field's
|
|
139
|
+
# annotation, and surfacing a constraint violation here would just
|
|
140
|
+
# duplicate the field-level error that caused the exclusion.
|
|
141
|
+
if not self.referenced_fields().issubset(against):
|
|
142
|
+
return
|
|
111
143
|
try:
|
|
112
144
|
if not Q(self.check).check(against):
|
|
113
145
|
raise self._build_violation_error()
|
|
@@ -11,6 +11,7 @@ from .analysis import (
|
|
|
11
11
|
IndexStatus,
|
|
12
12
|
ModelAnalysis,
|
|
13
13
|
NullabilityDrift,
|
|
14
|
+
ReadOnlyConnectionError,
|
|
14
15
|
analyze_model,
|
|
15
16
|
)
|
|
16
17
|
from .fixes import (
|
|
@@ -65,6 +66,7 @@ __all__ = [
|
|
|
65
66
|
"ModelAnalysis",
|
|
66
67
|
"NullabilityDrift",
|
|
67
68
|
"PlanItem",
|
|
69
|
+
"ReadOnlyConnectionError",
|
|
68
70
|
"RebuildIndexFix",
|
|
69
71
|
"RenameConstraintFix",
|
|
70
72
|
"RenameIndexFix",
|