plain.postgres 0.88.2__tar.gz → 0.89.1__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.88.2 → plain_postgres-0.89.1}/PKG-INFO +1 -1
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/CHANGELOG.md +58 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/cli/core.py +2 -0
- plain_postgres-0.89.1/plain/postgres/cli/schema.py +248 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/diagnose/checks.py +2 -30
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/dialect.py +1 -1
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/related.py +0 -5
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/indexes.py +3 -36
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/meta.py +0 -7
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/options.py +1 -6
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/preflight.py +15 -37
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/schema.py +4 -75
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/types.pyi +0 -2
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/pyproject.toml +1 -1
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/.gitignore +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/CLAUDE.md +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/LICENSE +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/README.md +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/README.md +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/agents/.claude/skills/plain-postgres-diagnose/SKILL.md +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/backups/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/backups/cli.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/backups/clients.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/backups/core.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/base.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/config.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/connections.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/db.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/diagnose/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/diagnose/context.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/diagnose/tables.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/diagnose/types.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/query.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/test/utils.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/types.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/models.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/settings.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/urls.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_connection_isolation.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_database_url.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_delete_behaviors.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_encrypted_fields.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_exceptions.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_iterator.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_manager_assignment.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_models.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_read_only_transactions.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_related_descriptors.py +0 -0
- {plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/test_related_manager_api.py +0 -0
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.89.1](https://github.com/dropseed/plain/releases/plain-postgres@0.89.1) (2026-03-26)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Fixed `schema` command type mismatches for `time`, `timestamp`, and `DecimalField` types that caused false drift reports ([187e39e3faeb](https://github.com/dropseed/plain/commit/187e39e3faeb))
|
|
8
|
+
- Fixed `schema` command crash on expression-based unique constraints (e.g. `UniqueConstraint` with `expressions` instead of `fields`) ([187e39e3faeb](https://github.com/dropseed/plain/commit/187e39e3faeb))
|
|
9
|
+
- Improved 0.89.0 upgrade instructions with clearer ordering and step descriptions ([a59062327ed5](https://github.com/dropseed/plain/commit/a59062327ed5), [c0520bdca709](https://github.com/dropseed/plain/commit/c0520bdca709))
|
|
10
|
+
|
|
11
|
+
### Upgrade instructions
|
|
12
|
+
|
|
13
|
+
- No changes required.
|
|
14
|
+
|
|
15
|
+
## [0.89.0](https://github.com/dropseed/plain/releases/plain-postgres@0.89.0) (2026-03-25)
|
|
16
|
+
|
|
17
|
+
### What's changed
|
|
18
|
+
|
|
19
|
+
- **Removed `db_index` from `ForeignKeyField`** — FK fields no longer create indexes automatically. Declare an explicit `Index(fields=["field"], name="...")` for any FK column that needs one. The `db_index` parameter has been removed entirely. ([061b97f5d538](https://github.com/dropseed/plain/commit/061b97f5d538))
|
|
20
|
+
- **Removed `Index.set_name_with_model()`** — the hash-based auto-naming machinery is gone. `Index.name` is now validated as non-empty at construction time. ([9a4ecf8ac2f0](https://github.com/dropseed/plain/commit/9a4ecf8ac2f0))
|
|
21
|
+
- **Index/constraint name collision detection** — preflight now checks index and constraint names together (they share the same Postgres namespace), catching cross-type collisions that would fail at migrate time. ([292f8d6791d6](https://github.com/dropseed/plain/commit/292f8d6791d6))
|
|
22
|
+
- **New `plain postgres schema` command** — shows expected DB schema from model definitions and compares it against the actual database. Detects column type mismatches, nullability drift, missing/extra columns, and orphan indexes. Use `--check` for CI (exits non-zero on drift). ([ee336078483f](https://github.com/dropseed/plain/commit/ee336078483f))
|
|
23
|
+
|
|
24
|
+
### Upgrade instructions
|
|
25
|
+
|
|
26
|
+
1. Remove any `db_index=False` from FK fields in models and migration files — the parameter no longer exists.
|
|
27
|
+
|
|
28
|
+
2. For each `ForeignKeyField`, check if it's covered by an explicit `Index` or `UniqueConstraint` (with the FK as the leading field). Most FK columns should have an index.
|
|
29
|
+
|
|
30
|
+
3. **If uncovered**, add an explicit index:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
model_options = postgres.Options(
|
|
34
|
+
indexes=[
|
|
35
|
+
postgres.Index(name="myapp_mymodel_author_id_idx", fields=["author"]),
|
|
36
|
+
],
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
4. Run `makemigrations`. Before the `AddIndex` operation, add a `RunSQL` to drop the orphan auto-index left behind by the old `db_index=True` default:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
operations = [
|
|
44
|
+
migrations.RunSQL('DROP INDEX IF EXISTS "myapp_mymodel_author_id_abc12345"'),
|
|
45
|
+
migrations.AddIndex(...),
|
|
46
|
+
]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The old auto-index name follows the pattern `{table}_{column}_{hash}`. Find orphan names by running `plain postgres schema`.
|
|
50
|
+
|
|
51
|
+
5. **If already covered** by a composite index or unique constraint, the orphan auto-index is redundant. Generate a migration to drop it:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
operations = [
|
|
55
|
+
migrations.RunSQL('DROP INDEX IF EXISTS "myapp_mymodel_author_id_abc12345"'),
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
6. Run `migrate`.
|
|
60
|
+
|
|
3
61
|
## [0.88.2](https://github.com/dropseed/plain/releases/plain-postgres@0.88.2) (2026-03-25)
|
|
4
62
|
|
|
5
63
|
### What's changed
|
|
@@ -15,6 +15,7 @@ from ..db import get_connection
|
|
|
15
15
|
from ..dialect import quote_name
|
|
16
16
|
from ..migrations.recorder import MIGRATION_TABLE_NAME
|
|
17
17
|
from .diagnose import diagnose
|
|
18
|
+
from .schema import schema
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@register_cli("postgres")
|
|
@@ -25,6 +26,7 @@ def cli() -> None:
|
|
|
25
26
|
|
|
26
27
|
cli.add_command(backups_cli)
|
|
27
28
|
cli.add_command(diagnose)
|
|
29
|
+
cli.add_command(schema)
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
@cli.command()
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ..db import get_connection
|
|
10
|
+
from ..registry import models_registry
|
|
11
|
+
|
|
12
|
+
# Map from db_type() short names to format_type() canonical names.
|
|
13
|
+
# Postgres stores these as aliases; format_type() returns the long form.
|
|
14
|
+
_TYPE_ALIASES: dict[str, str] = {
|
|
15
|
+
"bool": "boolean",
|
|
16
|
+
"varchar": "character varying",
|
|
17
|
+
"int2": "smallint",
|
|
18
|
+
"int4": "integer",
|
|
19
|
+
"int8": "bigint",
|
|
20
|
+
"float4": "real",
|
|
21
|
+
"float8": "double precision",
|
|
22
|
+
"time": "time without time zone",
|
|
23
|
+
"timestamp": "timestamp without time zone",
|
|
24
|
+
"timestamptz": "timestamp with time zone",
|
|
25
|
+
"timetz": "time with time zone",
|
|
26
|
+
"serial": "integer",
|
|
27
|
+
"bigserial": "bigint",
|
|
28
|
+
"smallserial": "smallint",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_TYPE_PARTS = re.compile(r"^(\w+)(.*)")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_type(db_type: str) -> str:
|
|
35
|
+
"""Normalize a db_type() string to match Postgres format_type() output."""
|
|
36
|
+
m = _TYPE_PARTS.match(db_type)
|
|
37
|
+
if not m:
|
|
38
|
+
return db_type
|
|
39
|
+
base, suffix = m.group(1), m.group(2)
|
|
40
|
+
canonical = _TYPE_ALIASES.get(base, base)
|
|
41
|
+
return canonical + suffix
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ok() -> None:
|
|
45
|
+
click.secho(" ✓", fg="green", dim=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _err(msg: str) -> None:
|
|
49
|
+
click.secho(f" ✗ {msg}", fg="red")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_actual_columns(cursor: Any, table_name: str) -> dict[str, tuple[str, bool]]:
|
|
53
|
+
"""Return {column_name: (type_string, is_not_null)} from the actual DB."""
|
|
54
|
+
cursor.execute(
|
|
55
|
+
"""
|
|
56
|
+
SELECT a.attname, format_type(a.atttypid, a.atttypmod), a.attnotnull
|
|
57
|
+
FROM pg_attribute a
|
|
58
|
+
JOIN pg_class c ON a.attrelid = c.oid
|
|
59
|
+
WHERE c.relname = %s AND pg_catalog.pg_table_is_visible(c.oid)
|
|
60
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
|
61
|
+
ORDER BY a.attnum
|
|
62
|
+
""",
|
|
63
|
+
[table_name],
|
|
64
|
+
)
|
|
65
|
+
return {
|
|
66
|
+
name: (type_str, is_not_null)
|
|
67
|
+
for name, type_str, is_not_null in cursor.fetchall()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _show_model(conn: Any, cursor: Any, model: Any) -> int:
|
|
72
|
+
"""Print schema for a model, annotating any drift. Returns issue count."""
|
|
73
|
+
from ..constraints import UniqueConstraint
|
|
74
|
+
|
|
75
|
+
table_name = model.model_options.db_table
|
|
76
|
+
issues = 0
|
|
77
|
+
|
|
78
|
+
actual_columns = _get_actual_columns(cursor, table_name)
|
|
79
|
+
actual_constraints = conn.get_constraints(cursor, table_name)
|
|
80
|
+
|
|
81
|
+
actual_indexes: dict[str, dict[str, Any]] = {}
|
|
82
|
+
actual_unique: dict[str, dict[str, Any]] = {}
|
|
83
|
+
for name, info in actual_constraints.items():
|
|
84
|
+
if info.get("primary_key"):
|
|
85
|
+
continue
|
|
86
|
+
if info.get("index"):
|
|
87
|
+
actual_indexes[name] = info
|
|
88
|
+
elif info.get("unique"):
|
|
89
|
+
actual_unique[name] = info
|
|
90
|
+
|
|
91
|
+
click.secho(model.model_options.label, bold=True, nl=False)
|
|
92
|
+
click.secho(f" → {table_name}", dim=True)
|
|
93
|
+
|
|
94
|
+
if not actual_columns:
|
|
95
|
+
click.echo(" ", nl=False)
|
|
96
|
+
_err("table missing from database")
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
# Columns
|
|
100
|
+
expected_col_names: set[str] = set()
|
|
101
|
+
|
|
102
|
+
for field in model._model_meta.local_fields:
|
|
103
|
+
db_type = field.db_type()
|
|
104
|
+
if db_type is None:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
expected_col_names.add(field.column)
|
|
108
|
+
expected_type = _normalize_type(db_type)
|
|
109
|
+
|
|
110
|
+
col_display = field.column
|
|
111
|
+
if field.column != field.name:
|
|
112
|
+
col_display = f"{field.name} → {field.column}"
|
|
113
|
+
|
|
114
|
+
type_parts = [click.style(db_type, fg="cyan")]
|
|
115
|
+
if field.allow_null:
|
|
116
|
+
type_parts.append(click.style("NULL", dim=True))
|
|
117
|
+
if field.primary_key:
|
|
118
|
+
type_parts.append(click.style("PK", fg="yellow"))
|
|
119
|
+
suffix = field.db_type_suffix()
|
|
120
|
+
if suffix:
|
|
121
|
+
type_parts.append(click.style(suffix, dim=True))
|
|
122
|
+
|
|
123
|
+
click.echo(f" {col_display:30s} {' '.join(type_parts)}", nl=False)
|
|
124
|
+
|
|
125
|
+
if field.column not in actual_columns:
|
|
126
|
+
_err("missing from database")
|
|
127
|
+
issues += 1
|
|
128
|
+
else:
|
|
129
|
+
actual_type, actual_not_null = actual_columns[field.column]
|
|
130
|
+
col_issues = []
|
|
131
|
+
if expected_type != actual_type:
|
|
132
|
+
col_issues.append(
|
|
133
|
+
f"type: expected {expected_type}, actual {actual_type}"
|
|
134
|
+
)
|
|
135
|
+
if (not field.allow_null) != actual_not_null:
|
|
136
|
+
exp = "NOT NULL" if not field.allow_null else "NULL"
|
|
137
|
+
act = "NOT NULL" if actual_not_null else "NULL"
|
|
138
|
+
col_issues.append(f"expected {exp}, actual {act}")
|
|
139
|
+
if col_issues:
|
|
140
|
+
_err("; ".join(col_issues))
|
|
141
|
+
issues += len(col_issues)
|
|
142
|
+
else:
|
|
143
|
+
_ok()
|
|
144
|
+
|
|
145
|
+
for col_name in sorted(actual_columns.keys() - expected_col_names):
|
|
146
|
+
click.echo(f" {col_name:30s} ", nl=False)
|
|
147
|
+
_err("extra column, not in model")
|
|
148
|
+
issues += 1
|
|
149
|
+
|
|
150
|
+
# Indexes
|
|
151
|
+
model_indexes = model.model_options.indexes
|
|
152
|
+
extra_indexes = actual_indexes.keys() - {idx.name for idx in model_indexes}
|
|
153
|
+
|
|
154
|
+
if model_indexes or extra_indexes:
|
|
155
|
+
click.echo()
|
|
156
|
+
click.secho(" Indexes:", dim=True)
|
|
157
|
+
|
|
158
|
+
for index in model_indexes:
|
|
159
|
+
fields_str = ", ".join(index.fields) if index.fields else "expressions"
|
|
160
|
+
click.echo(f" {index.name} ({fields_str})", nl=False)
|
|
161
|
+
|
|
162
|
+
if index.name not in actual_indexes:
|
|
163
|
+
_err("missing from database")
|
|
164
|
+
issues += 1
|
|
165
|
+
else:
|
|
166
|
+
_ok()
|
|
167
|
+
|
|
168
|
+
for name in sorted(extra_indexes):
|
|
169
|
+
cols = actual_indexes[name].get("columns", [])
|
|
170
|
+
cols_str = ", ".join(cols) if cols else "expression"
|
|
171
|
+
click.echo(f" {name} ({cols_str})", nl=False)
|
|
172
|
+
_err("not in model")
|
|
173
|
+
issues += 1
|
|
174
|
+
|
|
175
|
+
# Unique constraints
|
|
176
|
+
model_constraints = [
|
|
177
|
+
c for c in model.model_options.constraints if isinstance(c, UniqueConstraint)
|
|
178
|
+
]
|
|
179
|
+
extra_constraints = actual_unique.keys() - {c.name for c in model_constraints}
|
|
180
|
+
|
|
181
|
+
if model_constraints or extra_constraints:
|
|
182
|
+
click.echo()
|
|
183
|
+
click.secho(" Constraints:", dim=True)
|
|
184
|
+
|
|
185
|
+
for constraint in model_constraints:
|
|
186
|
+
if constraint.fields:
|
|
187
|
+
fields_str = ", ".join(constraint.fields)
|
|
188
|
+
else:
|
|
189
|
+
fields_str = "expressions"
|
|
190
|
+
click.echo(f" {constraint.name} UNIQUE ({fields_str})", nl=False)
|
|
191
|
+
|
|
192
|
+
if constraint.name not in actual_unique:
|
|
193
|
+
_err("missing from database")
|
|
194
|
+
issues += 1
|
|
195
|
+
else:
|
|
196
|
+
_ok()
|
|
197
|
+
|
|
198
|
+
for name in sorted(extra_constraints):
|
|
199
|
+
cols = actual_unique[name].get("columns", [])
|
|
200
|
+
cols_str = ", ".join(cols) if cols else "?"
|
|
201
|
+
click.echo(f" {name} UNIQUE ({cols_str})", nl=False)
|
|
202
|
+
_err("not in model")
|
|
203
|
+
issues += 1
|
|
204
|
+
|
|
205
|
+
return issues
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@click.command()
|
|
209
|
+
@click.argument("model_label", required=False)
|
|
210
|
+
def schema(model_label: str | None) -> None:
|
|
211
|
+
"""Show database schema from models, compared against the actual database"""
|
|
212
|
+
models = models_registry.get_models()
|
|
213
|
+
|
|
214
|
+
if model_label:
|
|
215
|
+
model_label_lower = model_label.lower()
|
|
216
|
+
models = [
|
|
217
|
+
m
|
|
218
|
+
for m in models
|
|
219
|
+
if m.model_options.label_lower == model_label_lower
|
|
220
|
+
or m.model_options.db_table == model_label
|
|
221
|
+
or m.__name__.lower() == model_label_lower
|
|
222
|
+
]
|
|
223
|
+
if not models:
|
|
224
|
+
raise click.ClickException(f"No model found matching '{model_label}'")
|
|
225
|
+
|
|
226
|
+
conn = get_connection()
|
|
227
|
+
total_issues = 0
|
|
228
|
+
models_checked = 0
|
|
229
|
+
|
|
230
|
+
with conn.cursor() as cursor:
|
|
231
|
+
for i, model in enumerate(models):
|
|
232
|
+
if i > 0:
|
|
233
|
+
click.echo()
|
|
234
|
+
models_checked += 1
|
|
235
|
+
total_issues += _show_model(conn, cursor, model)
|
|
236
|
+
|
|
237
|
+
click.echo()
|
|
238
|
+
if total_issues == 0:
|
|
239
|
+
click.secho(
|
|
240
|
+
f"{models_checked} models, all match the database.",
|
|
241
|
+
fg="green",
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
click.secho(
|
|
245
|
+
f"{models_checked} models, {total_issues} issue{'s' if total_issues != 1 else ''}.",
|
|
246
|
+
fg="red",
|
|
247
|
+
)
|
|
248
|
+
sys.exit(1)
|
|
@@ -101,24 +101,6 @@ def check_duplicate_indexes(
|
|
|
101
101
|
""")
|
|
102
102
|
rows = cursor.fetchall()
|
|
103
103
|
|
|
104
|
-
# Identify FK columns: (table_name, column_number) -> column_name
|
|
105
|
-
cursor.execute("""
|
|
106
|
-
SELECT
|
|
107
|
-
ct.relname AS table_name,
|
|
108
|
-
c.conkey[1] AS col_number,
|
|
109
|
-
a.attname AS column_name
|
|
110
|
-
FROM pg_catalog.pg_constraint c
|
|
111
|
-
JOIN pg_catalog.pg_class ct ON ct.oid = c.conrelid
|
|
112
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = ct.relnamespace
|
|
113
|
-
JOIN pg_catalog.pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1]
|
|
114
|
-
WHERE c.contype = 'f'
|
|
115
|
-
AND array_length(c.conkey, 1) = 1
|
|
116
|
-
AND n.nspname = 'public'
|
|
117
|
-
""")
|
|
118
|
-
fk_columns: dict[tuple[str, int], str] = {
|
|
119
|
-
(row[0], row[1]): row[2] for row in cursor.fetchall()
|
|
120
|
-
}
|
|
121
|
-
|
|
122
104
|
# Group by table
|
|
123
105
|
by_table: dict[str, list[tuple[str, list[int], list[int], bool, str, int]]] = {}
|
|
124
106
|
for table_name, index_name, cols, opclasses, is_unique, size, size_bytes in rows:
|
|
@@ -142,18 +124,8 @@ def check_duplicate_indexes(
|
|
|
142
124
|
and ops_l[: len(cols_s)] == ops_s
|
|
143
125
|
and not unique_s # unique indexes serve a constraint purpose
|
|
144
126
|
):
|
|
145
|
-
# Single-column index on a FK column = auto-generated FK index
|
|
146
|
-
fk_field = (
|
|
147
|
-
fk_columns.get((table_name, cols_s[0]))
|
|
148
|
-
if len(cols_s) == 1
|
|
149
|
-
else None
|
|
150
|
-
)
|
|
151
|
-
|
|
152
127
|
source, package = _table_source(table_name, table_owners)
|
|
153
|
-
|
|
154
|
-
app_suggestion = f'Set db_index=False on the "{fk_field}" FK field, then run makemigrations'
|
|
155
|
-
else:
|
|
156
|
-
app_suggestion = f'Remove "{name_s}" from model constraints, then run makemigrations'
|
|
128
|
+
app_suggestion = f'Remove "{name_s}" from model indexes/constraints, then run makemigrations'
|
|
157
129
|
|
|
158
130
|
items.append(
|
|
159
131
|
CheckItem(
|
|
@@ -245,7 +217,7 @@ def check_unused_indexes(
|
|
|
245
217
|
suggestion=_index_suggestion(
|
|
246
218
|
source=source,
|
|
247
219
|
package=package,
|
|
248
|
-
app_suggestion=f'Remove "{index_name}" from model constraints, then run makemigrations',
|
|
220
|
+
app_suggestion=f'Remove "{index_name}" from model indexes/constraints, then run makemigrations',
|
|
249
221
|
unmanaged_suggestion=f'DROP INDEX CONCURRENTLY "{index_name}";',
|
|
250
222
|
),
|
|
251
223
|
)
|
|
@@ -109,7 +109,7 @@ DATA_TYPES: dict[str, Any] = {
|
|
|
109
109
|
"CharField": _get_varchar_column,
|
|
110
110
|
"DateField": "date",
|
|
111
111
|
"DateTimeField": "timestamp with time zone",
|
|
112
|
-
"DecimalField": "numeric(%(max_digits)s
|
|
112
|
+
"DecimalField": "numeric(%(max_digits)s,%(decimal_places)s)",
|
|
113
113
|
"DurationField": "interval",
|
|
114
114
|
"FloatField": "double precision",
|
|
115
115
|
"IntegerField": "integer",
|
|
@@ -339,7 +339,6 @@ class ForeignKeyField(RelatedField):
|
|
|
339
339
|
on_delete: Any,
|
|
340
340
|
related_query_name: str | None = None,
|
|
341
341
|
limit_choices_to: Any = None,
|
|
342
|
-
db_index: bool = True,
|
|
343
342
|
db_constraint: bool = True,
|
|
344
343
|
**kwargs: Any,
|
|
345
344
|
):
|
|
@@ -367,7 +366,6 @@ class ForeignKeyField(RelatedField):
|
|
|
367
366
|
limit_choices_to=limit_choices_to,
|
|
368
367
|
**kwargs,
|
|
369
368
|
)
|
|
370
|
-
self.db_index = db_index
|
|
371
369
|
self.db_constraint = db_constraint
|
|
372
370
|
|
|
373
371
|
def __copy__(self) -> ForeignKeyField:
|
|
@@ -556,9 +554,6 @@ class ForeignKeyField(RelatedField):
|
|
|
556
554
|
else:
|
|
557
555
|
kwargs["to"] = self.remote_field.model.model_options.label_lower
|
|
558
556
|
|
|
559
|
-
if self.db_index is not True:
|
|
560
|
-
kwargs["db_index"] = self.db_index
|
|
561
|
-
|
|
562
557
|
if self.db_constraint is not True:
|
|
563
558
|
kwargs["db_constraint"] = self.db_constraint
|
|
564
559
|
|
|
@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Any, Self
|
|
|
6
6
|
from plain.postgres.expressions import Col, ExpressionList, F, Func, OrderBy
|
|
7
7
|
from plain.postgres.query_utils import Q
|
|
8
8
|
from plain.postgres.sql.query import Query
|
|
9
|
-
from plain.postgres.utils import names_digest, split_identifier
|
|
10
9
|
from plain.utils.functional import partition
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
@@ -65,7 +64,9 @@ class Index:
|
|
|
65
64
|
(field_name.removeprefix("-"), "DESC" if field_name.startswith("-") else "")
|
|
66
65
|
for field_name in self.fields
|
|
67
66
|
]
|
|
68
|
-
|
|
67
|
+
if not name:
|
|
68
|
+
raise ValueError("Index.name is required.")
|
|
69
|
+
self.name = name
|
|
69
70
|
self.opclasses: tuple[str, ...] = tuple(opclasses)
|
|
70
71
|
self.condition = condition
|
|
71
72
|
self.include = tuple(include) if include else ()
|
|
@@ -151,40 +152,6 @@ class Index:
|
|
|
151
152
|
_, args, kwargs = self.deconstruct()
|
|
152
153
|
return self.__class__(*args, **kwargs)
|
|
153
154
|
|
|
154
|
-
def set_name_with_model(self, model: type[Model]) -> None:
|
|
155
|
-
"""
|
|
156
|
-
Generate a unique name for the index.
|
|
157
|
-
|
|
158
|
-
The name is divided into 3 parts - table name (12 chars), field name
|
|
159
|
-
(8 chars) and unique hash + suffix (10 chars). Each part is made to
|
|
160
|
-
fit its size by truncating the excess length.
|
|
161
|
-
"""
|
|
162
|
-
_, table_name = split_identifier(model.model_options.db_table)
|
|
163
|
-
column_names = [
|
|
164
|
-
model._model_meta.get_forward_field(field_name).column
|
|
165
|
-
for field_name, order in self.fields_orders
|
|
166
|
-
]
|
|
167
|
-
column_names_with_order = [
|
|
168
|
-
(("-%s" if order else "%s") % column_name)
|
|
169
|
-
for column_name, (field_name, order) in zip(
|
|
170
|
-
column_names, self.fields_orders
|
|
171
|
-
)
|
|
172
|
-
]
|
|
173
|
-
# The length of the parts of the name is based on the default max
|
|
174
|
-
# length of 30 characters.
|
|
175
|
-
hash_data = [table_name] + column_names_with_order + [self.suffix]
|
|
176
|
-
self.name = "{}_{}_{}".format(
|
|
177
|
-
table_name[:11],
|
|
178
|
-
column_names[0][:7],
|
|
179
|
-
f"{names_digest(*hash_data, length=6)}_{self.suffix}",
|
|
180
|
-
)
|
|
181
|
-
if len(self.name) > self.max_name_length:
|
|
182
|
-
raise ValueError(
|
|
183
|
-
"Index name too long. Is self.suffix longer than 3 characters?"
|
|
184
|
-
)
|
|
185
|
-
if self.name[0] == "_" or self.name[0].isdigit():
|
|
186
|
-
self.name = f"D{self.name[1:]}"
|
|
187
|
-
|
|
188
155
|
def __repr__(self) -> str:
|
|
189
156
|
return "<{}:{}{}{}{}{}{}>".format(
|
|
190
157
|
self.__class__.__qualname__,
|
|
@@ -132,13 +132,6 @@ class Meta:
|
|
|
132
132
|
instance.local_fields.sort(key=lambda f: (not f.primary_key, f.name))
|
|
133
133
|
instance.local_many_to_many.sort(key=lambda f: f.name)
|
|
134
134
|
|
|
135
|
-
# Set index names now that fields are contributed
|
|
136
|
-
# Trigger model_options descriptor to ensure it's initialized
|
|
137
|
-
# (accessing it will cache the instance)
|
|
138
|
-
for index in model.model_options.indexes:
|
|
139
|
-
if not index.name:
|
|
140
|
-
index.set_name_with_model(model)
|
|
141
|
-
|
|
142
135
|
return instance
|
|
143
136
|
|
|
144
137
|
@property
|
|
@@ -168,12 +168,7 @@ class Options:
|
|
|
168
168
|
options = {}
|
|
169
169
|
for name in self._provided_options:
|
|
170
170
|
if name == "indexes":
|
|
171
|
-
|
|
172
|
-
indexes = [idx.clone() for idx in self.indexes]
|
|
173
|
-
for index in indexes:
|
|
174
|
-
if not index.name:
|
|
175
|
-
index.set_name_with_model(self.model)
|
|
176
|
-
options["indexes"] = indexes
|
|
171
|
+
options["indexes"] = [idx.clone() for idx in self.indexes]
|
|
177
172
|
elif name == "constraints":
|
|
178
173
|
# Clone constraints
|
|
179
174
|
options["constraints"] = [con.clone() for con in self.constraints]
|
|
@@ -38,14 +38,6 @@ def _collect_model_indexes(model: Any) -> list[tuple[str, list[str], bool]]:
|
|
|
38
38
|
if isinstance(constraint, UniqueConstraint) and constraint.fields:
|
|
39
39
|
all_indexes.append((constraint.name, list(constraint.fields), True))
|
|
40
40
|
|
|
41
|
-
for field in model._model_meta.local_fields:
|
|
42
|
-
if (
|
|
43
|
-
isinstance(field, ForeignKeyField)
|
|
44
|
-
and field.db_index
|
|
45
|
-
and not field.primary_key
|
|
46
|
-
):
|
|
47
|
-
all_indexes.append((f"{field.name} (auto)", [field.name], False))
|
|
48
|
-
|
|
49
41
|
return all_indexes
|
|
50
42
|
|
|
51
43
|
|
|
@@ -55,8 +47,9 @@ class CheckAllModels(PreflightCheck):
|
|
|
55
47
|
|
|
56
48
|
def run(self) -> list[PreflightResult]:
|
|
57
49
|
db_table_models = defaultdict(list)
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
# Indexes and constraints share the same Postgres namespace,
|
|
51
|
+
# so track them together to catch cross-type collisions.
|
|
52
|
+
relation_names = defaultdict(list)
|
|
60
53
|
errors = []
|
|
61
54
|
models = models_registry.get_models()
|
|
62
55
|
for model in models:
|
|
@@ -74,9 +67,9 @@ class CheckAllModels(PreflightCheck):
|
|
|
74
67
|
else:
|
|
75
68
|
errors.extend(model.preflight())
|
|
76
69
|
for model_index in model.model_options.indexes:
|
|
77
|
-
|
|
70
|
+
relation_names[model_index.name].append(model.model_options.label)
|
|
78
71
|
for model_constraint in model.model_options.constraints:
|
|
79
|
-
|
|
72
|
+
relation_names[model_constraint.name].append(model.model_options.label)
|
|
80
73
|
for db_table, model_labels in db_table_models.items():
|
|
81
74
|
if len(model_labels) != 1:
|
|
82
75
|
model_labels_str = ", ".join(model_labels)
|
|
@@ -87,34 +80,20 @@ class CheckAllModels(PreflightCheck):
|
|
|
87
80
|
id="postgres.duplicate_db_table",
|
|
88
81
|
)
|
|
89
82
|
)
|
|
90
|
-
for
|
|
91
|
-
if len(model_labels) > 1:
|
|
92
|
-
model_labels = set(model_labels)
|
|
93
|
-
errors.append(
|
|
94
|
-
PreflightResult(
|
|
95
|
-
fix="index name '{}' is not unique {} {}.".format(
|
|
96
|
-
index_name,
|
|
97
|
-
"for model" if len(model_labels) == 1 else "among models:",
|
|
98
|
-
", ".join(sorted(model_labels)),
|
|
99
|
-
),
|
|
100
|
-
id="postgres.index_name_not_unique_single"
|
|
101
|
-
if len(model_labels) == 1
|
|
102
|
-
else "postgres.index_name_not_unique_multiple",
|
|
103
|
-
),
|
|
104
|
-
)
|
|
105
|
-
for constraint_name, model_labels in constraints.items():
|
|
83
|
+
for relation_name, model_labels in relation_names.items():
|
|
106
84
|
if len(model_labels) > 1:
|
|
107
|
-
|
|
85
|
+
unique_models = set(model_labels)
|
|
86
|
+
single_model = len(unique_models) == 1
|
|
108
87
|
errors.append(
|
|
109
88
|
PreflightResult(
|
|
110
|
-
fix="constraint name '{}' is not unique {} {}.".format(
|
|
111
|
-
|
|
112
|
-
"for model" if
|
|
113
|
-
", ".join(sorted(
|
|
89
|
+
fix="index/constraint name '{}' is not unique {} {}.".format(
|
|
90
|
+
relation_name,
|
|
91
|
+
"for model" if single_model else "among models:",
|
|
92
|
+
", ".join(sorted(unique_models)),
|
|
114
93
|
),
|
|
115
|
-
id="postgres.
|
|
116
|
-
if
|
|
117
|
-
else "postgres.
|
|
94
|
+
id="postgres.relation_name_not_unique_single"
|
|
95
|
+
if single_model
|
|
96
|
+
else "postgres.relation_name_not_unique_multiple",
|
|
118
97
|
),
|
|
119
98
|
)
|
|
120
99
|
return errors
|
|
@@ -391,7 +370,6 @@ class CheckMissingFKIndexes(PreflightCheck):
|
|
|
391
370
|
isinstance(field, ForeignKeyField)
|
|
392
371
|
and not field.primary_key
|
|
393
372
|
and field.name not in covered_fields
|
|
394
|
-
and not field.db_index
|
|
395
373
|
):
|
|
396
374
|
results.append(
|
|
397
375
|
PreflightResult(
|
|
@@ -951,41 +951,6 @@ class DatabaseSchemaEditor:
|
|
|
951
951
|
self.sql_delete_fk, new_rel.related_model, fk_name
|
|
952
952
|
)
|
|
953
953
|
)
|
|
954
|
-
# Removed an index? (no strict check, as multiple indexes are possible)
|
|
955
|
-
# Remove indexes if db_index switched to False or a unique constraint
|
|
956
|
-
# will now be used in lieu of an index. The following lines from the
|
|
957
|
-
# truth table show all True cases; the rest are False:
|
|
958
|
-
#
|
|
959
|
-
# old_field.db_index | old_field.primary_key | new_field.db_index | new_field.primary_key
|
|
960
|
-
# ------------------------------------------------------------------------------
|
|
961
|
-
# True | False | False | False
|
|
962
|
-
# True | False | False | True
|
|
963
|
-
# True | False | True | True
|
|
964
|
-
if (
|
|
965
|
-
isinstance(old_field, ForeignKeyField)
|
|
966
|
-
and old_field.db_index
|
|
967
|
-
and not old_field.primary_key
|
|
968
|
-
and (
|
|
969
|
-
not (isinstance(new_field, ForeignKeyField) and new_field.db_index)
|
|
970
|
-
or new_field.primary_key
|
|
971
|
-
)
|
|
972
|
-
):
|
|
973
|
-
# Find the index for this field
|
|
974
|
-
meta_index_names = {index.name for index in model.model_options.indexes}
|
|
975
|
-
# Retrieve only BTREE indexes since this is what's created with
|
|
976
|
-
# db_index=True.
|
|
977
|
-
index_names = self._constraint_names(
|
|
978
|
-
model,
|
|
979
|
-
[old_field.column],
|
|
980
|
-
index=True,
|
|
981
|
-
type_=Index.suffix,
|
|
982
|
-
exclude=meta_index_names,
|
|
983
|
-
)
|
|
984
|
-
for index_name in index_names:
|
|
985
|
-
# The only way to check if an index was created with
|
|
986
|
-
# db_index=True or with Index(['field'], name='foo')
|
|
987
|
-
# is to look at its name (refs #28053).
|
|
988
|
-
self.execute(self._delete_index_sql(model, index_name))
|
|
989
954
|
# Change check constraints?
|
|
990
955
|
old_db_check = self._field_db_check(old_field, old_db_params)
|
|
991
956
|
new_db_check = self._field_db_check(new_field, new_db_params)
|
|
@@ -1109,25 +1074,6 @@ class DatabaseSchemaEditor:
|
|
|
1109
1074
|
if old_field.primary_key and not new_field.primary_key:
|
|
1110
1075
|
self._delete_primary_key(model, strict)
|
|
1111
1076
|
|
|
1112
|
-
# Added an index? Add an index if db_index switched to True or a unique
|
|
1113
|
-
# constraint will no longer be used in lieu of an index. The following
|
|
1114
|
-
# lines from the truth table show all True cases; the rest are False:
|
|
1115
|
-
#
|
|
1116
|
-
# old_field.db_index | old_field.primary_key | new_field.db_index | new_field.primary_key
|
|
1117
|
-
# ------------------------------------------------------------------------------
|
|
1118
|
-
# False | False | True | False
|
|
1119
|
-
# False | True | True | False
|
|
1120
|
-
# True | True | True | False
|
|
1121
|
-
if (
|
|
1122
|
-
(
|
|
1123
|
-
not (isinstance(old_field, ForeignKeyField) and old_field.db_index)
|
|
1124
|
-
or old_field.primary_key
|
|
1125
|
-
)
|
|
1126
|
-
and isinstance(new_field, ForeignKeyField)
|
|
1127
|
-
and new_field.db_index
|
|
1128
|
-
and not new_field.primary_key
|
|
1129
|
-
):
|
|
1130
|
-
self.execute(self._create_index_sql(model, fields=[new_field]))
|
|
1131
1077
|
# Type alteration on primary key? Then we need to alter the column
|
|
1132
1078
|
# referring to us.
|
|
1133
1079
|
rels_to_update = []
|
|
@@ -1201,24 +1147,14 @@ class DatabaseSchemaEditor:
|
|
|
1201
1147
|
}
|
|
1202
1148
|
self.execute(sql, params)
|
|
1203
1149
|
|
|
1204
|
-
# Added
|
|
1205
|
-
if
|
|
1206
|
-
not (
|
|
1207
|
-
(isinstance(old_field, ForeignKeyField) and old_field.db_index)
|
|
1208
|
-
or old_field.primary_key
|
|
1209
|
-
)
|
|
1210
|
-
and isinstance(new_field, ForeignKeyField)
|
|
1211
|
-
and new_field.db_index
|
|
1212
|
-
) or (not old_field.primary_key and new_field.primary_key):
|
|
1150
|
+
# Added a primary key? Create any PostgreSQL-specific indexes.
|
|
1151
|
+
if not old_field.primary_key and new_field.primary_key:
|
|
1213
1152
|
like_index_statement = self._create_like_index_sql(model, new_field)
|
|
1214
1153
|
if like_index_statement is not None:
|
|
1215
1154
|
self.execute(like_index_statement)
|
|
1216
1155
|
|
|
1217
|
-
# Removed
|
|
1218
|
-
if old_field.primary_key and not
|
|
1219
|
-
(isinstance(new_field, ForeignKeyField) and new_field.db_index)
|
|
1220
|
-
or new_field.primary_key
|
|
1221
|
-
):
|
|
1156
|
+
# Removed a primary key? Drop any PostgreSQL-specific indexes.
|
|
1157
|
+
if old_field.primary_key and not new_field.primary_key:
|
|
1222
1158
|
index_to_remove = self._create_index_name(
|
|
1223
1159
|
model.model_options.db_table, [old_field.column], suffix="_like"
|
|
1224
1160
|
)
|
|
@@ -1570,8 +1506,6 @@ class DatabaseSchemaEditor:
|
|
|
1570
1506
|
Return a list of all index SQL statements for the specified field.
|
|
1571
1507
|
"""
|
|
1572
1508
|
output: list[Statement] = []
|
|
1573
|
-
if self._field_should_be_indexed(model, field):
|
|
1574
|
-
output.append(self._create_index_sql(model, fields=[field]))
|
|
1575
1509
|
# Add LIKE index for varchar/text primary keys
|
|
1576
1510
|
like_index_statement = self._create_like_index_sql(model, field)
|
|
1577
1511
|
if like_index_statement is not None:
|
|
@@ -1632,11 +1566,6 @@ class DatabaseSchemaEditor:
|
|
|
1632
1566
|
old_kwargs,
|
|
1633
1567
|
) != (new_path, new_args, new_kwargs)
|
|
1634
1568
|
|
|
1635
|
-
def _field_should_be_indexed(self, model: type[Model], field: Field) -> bool:
|
|
1636
|
-
if isinstance(field, ForeignKeyField):
|
|
1637
|
-
return bool(field.remote_field) and field.db_index and not field.primary_key
|
|
1638
|
-
return False
|
|
1639
|
-
|
|
1640
1569
|
def _field_became_primary_key(self, old_field: Field, new_field: Field) -> bool:
|
|
1641
1570
|
return not old_field.primary_key and new_field.primary_key
|
|
1642
1571
|
|
|
@@ -628,7 +628,6 @@ def ForeignKeyField[T: Model](
|
|
|
628
628
|
*,
|
|
629
629
|
related_query_name: str | None = None,
|
|
630
630
|
limit_choices_to: Any = None,
|
|
631
|
-
db_index: bool = True,
|
|
632
631
|
db_constraint: bool = True,
|
|
633
632
|
max_length: int | None = None,
|
|
634
633
|
required: bool = True,
|
|
@@ -645,7 +644,6 @@ def ForeignKeyField[T: Model](
|
|
|
645
644
|
*,
|
|
646
645
|
related_query_name: str | None = None,
|
|
647
646
|
limit_choices_to: Any = None,
|
|
648
|
-
db_index: bool = True,
|
|
649
647
|
db_constraint: bool = True,
|
|
650
648
|
max_length: int | None = None,
|
|
651
649
|
required: bool = True,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/related_descriptors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/fields/reverse_descriptors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/__init__.py
RENAMED
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/base.py
RENAMED
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/fields.py
RENAMED
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/models.py
RENAMED
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/plain/postgres/migrations/operations/special.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{plain_postgres-0.88.2 → plain_postgres-0.89.1}/tests/app/examples/migrations/0006_secretstore.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|