ferrum-orm 0.1.7__tar.gz → 0.1.8__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.
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/Cargo.lock +6 -6
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/Cargo.toml +1 -1
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/PKG-INFO +9 -1
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/README.md +8 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/pyproject.toml +1 -1
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/__init__.py +1 -1
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/queryset.py +220 -11
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/LICENSE +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/Cargo.toml +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/benches/hydrate.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/src/compile/mod.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/src/error.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/src/hydrate/mod.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/src/ir/metadata.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/src/ir/mod.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/src/lib.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-core/src/migrate/mod.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-pyo3/Cargo.toml +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-pyo3/src/lib.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/Cargo.toml +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/benches/compile.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/src/dialect.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/src/emit.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/src/fts/mod.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/src/fts/mssql.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/src/fts/mysql.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/src/fts/postgres.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/src/fts/sqlite.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/crates/ferrum-sql/src/lib.rs +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/_native.pyi +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/__init__.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/app.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/bootstrap.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/init.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/inspectdb_cmd.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/makemigrations_cmd.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/migrate_cmd.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/migrations_cmd.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/resetdb_cmd.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/revert_cmd.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/showmigrations_cmd.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/cli/sqlmigrate_cmd.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/config.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/connection.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/contrib/__init__.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/contrib/fastapi.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/drivers/__init__.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/drivers/mssql.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/drivers/mysql.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/drivers/postgres.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/drivers/protocol.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/drivers/sqlite.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/errors.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/expressions.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/ext/__init__.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/ext/fts.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/ext/pgvector.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/hooks.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/__init__.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/base.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/drift.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/fts/__init__.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/fts/mssql.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/fts/mysql.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/fts/postgres.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/fts/sqlite.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/gates.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/introspect.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/ledger.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/loader.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/operations.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/orchestrator.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/migrations/tokens.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/models.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/observability.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/py.typed +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/registry.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/relations.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/runtime.py +0 -0
- {ferrum_orm-0.1.7 → ferrum_orm-0.1.8}/python/ferrum/session.py +0 -0
|
@@ -174,9 +174,9 @@ dependencies = [
|
|
|
174
174
|
|
|
175
175
|
[[package]]
|
|
176
176
|
name = "crossbeam-epoch"
|
|
177
|
-
version = "0.9.
|
|
177
|
+
version = "0.9.20"
|
|
178
178
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
179
|
-
checksum = "
|
|
179
|
+
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
|
180
180
|
dependencies = [
|
|
181
181
|
"crossbeam-utils",
|
|
182
182
|
]
|
|
@@ -223,7 +223,7 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
|
|
223
223
|
|
|
224
224
|
[[package]]
|
|
225
225
|
name = "ferrum-core"
|
|
226
|
-
version = "0.1.
|
|
226
|
+
version = "0.1.8"
|
|
227
227
|
dependencies = [
|
|
228
228
|
"criterion",
|
|
229
229
|
"proptest",
|
|
@@ -234,7 +234,7 @@ dependencies = [
|
|
|
234
234
|
|
|
235
235
|
[[package]]
|
|
236
236
|
name = "ferrum-migrate"
|
|
237
|
-
version = "0.1.
|
|
237
|
+
version = "0.1.8"
|
|
238
238
|
dependencies = [
|
|
239
239
|
"ferrum-core",
|
|
240
240
|
"serde",
|
|
@@ -243,7 +243,7 @@ dependencies = [
|
|
|
243
243
|
|
|
244
244
|
[[package]]
|
|
245
245
|
name = "ferrum-pyo3"
|
|
246
|
-
version = "0.1.
|
|
246
|
+
version = "0.1.8"
|
|
247
247
|
dependencies = [
|
|
248
248
|
"ferrum-core",
|
|
249
249
|
"ferrum-sql",
|
|
@@ -255,7 +255,7 @@ dependencies = [
|
|
|
255
255
|
|
|
256
256
|
[[package]]
|
|
257
257
|
name = "ferrum-sql"
|
|
258
|
-
version = "0.1.
|
|
258
|
+
version = "0.1.8"
|
|
259
259
|
dependencies = [
|
|
260
260
|
"criterion",
|
|
261
261
|
"ferrum-core",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ferrum-orm
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Framework :: AsyncIO
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -121,6 +121,14 @@ user = await User.objects.create(
|
|
|
121
121
|
email="john@example.com",
|
|
122
122
|
)
|
|
123
123
|
|
|
124
|
+
# Insert from an instance (sentinel PK is dropped so the DB default runs)
|
|
125
|
+
draft = User(id=0, email="dana@example.com")
|
|
126
|
+
created = await User.objects.create(conn, draft)
|
|
127
|
+
|
|
128
|
+
# Persist one instance's changes by primary key
|
|
129
|
+
created.is_active = False
|
|
130
|
+
await User.objects.update_instance(conn, created, fields=["is_active"])
|
|
131
|
+
|
|
124
132
|
users = await (
|
|
125
133
|
User.objects
|
|
126
134
|
.filter(is_active=True)
|
|
@@ -49,6 +49,14 @@ user = await User.objects.create(
|
|
|
49
49
|
email="john@example.com",
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
+
# Insert from an instance (sentinel PK is dropped so the DB default runs)
|
|
53
|
+
draft = User(id=0, email="dana@example.com")
|
|
54
|
+
created = await User.objects.create(conn, draft)
|
|
55
|
+
|
|
56
|
+
# Persist one instance's changes by primary key
|
|
57
|
+
created.is_active = False
|
|
58
|
+
await User.objects.update_instance(conn, created, fields=["is_active"])
|
|
59
|
+
|
|
52
60
|
users = await (
|
|
53
61
|
User.objects
|
|
54
62
|
.filter(is_active=True)
|
|
@@ -1295,10 +1295,29 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
1295
1295
|
# Danger API guards (AGENTS.md §3 / ARCHITECTURE.md §3.9)
|
|
1296
1296
|
# ------------------------------------------------------------------
|
|
1297
1297
|
|
|
1298
|
-
async def create(
|
|
1298
|
+
async def create(
|
|
1299
|
+
self,
|
|
1300
|
+
conn: ConnectionLike,
|
|
1301
|
+
_obj: _M | dict[str, Any] | None = None,
|
|
1302
|
+
**values: Any, # noqa: ANN401
|
|
1303
|
+
) -> _M:
|
|
1299
1304
|
"""Insert a single row. Returns the hydrated model instance.
|
|
1300
1305
|
|
|
1301
|
-
|
|
1306
|
+
Accepts either a model instance / dict positionally, or keyword field
|
|
1307
|
+
values — not both::
|
|
1308
|
+
|
|
1309
|
+
await User.objects.create(conn, user) # instance form
|
|
1310
|
+
await User.objects.create(conn, {"email": email}) # dict form
|
|
1311
|
+
await User.objects.create(conn, email=email) # kwargs form
|
|
1312
|
+
|
|
1313
|
+
The instance/dict form mirrors ``bulk_create()`` semantics: values come
|
|
1314
|
+
from ``model_dump()`` and an auto-generated primary key carrying a
|
|
1315
|
+
sentinel value (``0`` / ``None`` / ``""``) is omitted so the database
|
|
1316
|
+
default runs. The kwargs form inserts exactly the given values (no
|
|
1317
|
+
sentinel dropping). The passed instance is never mutated — the return
|
|
1318
|
+
value is a new instance hydrated from ``RETURNING *``.
|
|
1319
|
+
|
|
1320
|
+
Builds an INSERT IR from the values, compiles it through the Rust
|
|
1302
1321
|
extension, executes ``INSERT … RETURNING *`` via asyncpg ``fetchrow``,
|
|
1303
1322
|
and constructs the model instance via the ADR-003 trusted hydration path.
|
|
1304
1323
|
|
|
@@ -1307,15 +1326,31 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
1307
1326
|
|
|
1308
1327
|
Args:
|
|
1309
1328
|
conn: An open ``Connection`` (obtained from ``ferrum.connect()``).
|
|
1329
|
+
_obj: A model instance or dict of field values. Named with a leading
|
|
1330
|
+
underscore so it can never collide with a model field passed via
|
|
1331
|
+
``**values`` (Pydantic forbids underscore-prefixed field names).
|
|
1310
1332
|
**values: Field names and their values to insert. Field names are
|
|
1311
1333
|
validated against the model's allowlist before compilation.
|
|
1312
1334
|
|
|
1313
1335
|
Raises:
|
|
1314
1336
|
FerrumConfigError: if the native extension is not built.
|
|
1315
|
-
FerrumCompileError: if a field name is not in the model's allowlist
|
|
1337
|
+
FerrumCompileError: if a field name is not in the model's allowlist,
|
|
1338
|
+
if both an instance/dict and keyword values are given, or if the
|
|
1339
|
+
instance carries deferred fields.
|
|
1316
1340
|
FerrumInternalError: if the INSERT returned no row (should not occur
|
|
1317
1341
|
when the DB is healthy and the table exists).
|
|
1318
1342
|
"""
|
|
1343
|
+
if _obj is not None:
|
|
1344
|
+
if values:
|
|
1345
|
+
raise FerrumCompileError(
|
|
1346
|
+
"create() accepts either a model instance/dict or keyword values, not both.",
|
|
1347
|
+
model=self._model.__name__,
|
|
1348
|
+
)
|
|
1349
|
+
row = self._object_to_row_dict(_obj)
|
|
1350
|
+
row_metadata = self._get_metadata()
|
|
1351
|
+
if row_metadata is not None:
|
|
1352
|
+
row = self._drop_auto_pk_sentinel(row, row_metadata)
|
|
1353
|
+
values = row
|
|
1319
1354
|
if _native_ext is None:
|
|
1320
1355
|
raise FerrumConfigError(_EXT_NOT_BUILT_MSG)
|
|
1321
1356
|
if conn is None:
|
|
@@ -1374,14 +1409,47 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
1374
1409
|
return pk_names if pk_names else [metadata.fields[0].name] if metadata.fields else ["id"]
|
|
1375
1410
|
|
|
1376
1411
|
def _object_to_row_dict(self, obj: _M | dict[str, Any]) -> dict[str, Any]:
|
|
1377
|
-
"""Normalize a
|
|
1412
|
+
"""Normalize a write-input object to a mutable field-value dict.
|
|
1413
|
+
|
|
1414
|
+
Rejects instances carrying deferred fields: ``model_dump()`` bypasses
|
|
1415
|
+
the deferred-field access guard and would silently write class defaults
|
|
1416
|
+
for columns that were never loaded.
|
|
1417
|
+
"""
|
|
1378
1418
|
if isinstance(obj, dict):
|
|
1379
1419
|
return dict(obj)
|
|
1380
1420
|
if hasattr(obj, "model_dump"):
|
|
1421
|
+
deferred = getattr(obj, "__ferrum_deferred__", None)
|
|
1422
|
+
if deferred:
|
|
1423
|
+
raise FerrumCompileError(
|
|
1424
|
+
f"Cannot write an instance of model {type(obj).__name__!r} with "
|
|
1425
|
+
f"deferred fields {sorted(deferred)!r}; load the full row first.",
|
|
1426
|
+
model=type(obj).__name__,
|
|
1427
|
+
)
|
|
1381
1428
|
return obj.model_dump()
|
|
1382
|
-
msg = f"
|
|
1429
|
+
msg = f"Expected a model instance or dict, got {type(obj)!r}."
|
|
1383
1430
|
raise TypeError(msg)
|
|
1384
1431
|
|
|
1432
|
+
def _drop_auto_pk_sentinel(
|
|
1433
|
+
self, values: dict[str, Any], metadata: ModelMetadata
|
|
1434
|
+
) -> dict[str, Any]:
|
|
1435
|
+
"""Drop the auto-generated PK column when it carries a sentinel value.
|
|
1436
|
+
|
|
1437
|
+
A sentinel value (``0`` / ``None`` / ``""``) on the first PK field means
|
|
1438
|
+
"let the database default generate this". Raises a structured error when
|
|
1439
|
+
nothing is left to insert, so an empty INSERT never reaches SQL emission.
|
|
1440
|
+
"""
|
|
1441
|
+
out = dict(values)
|
|
1442
|
+
pk_name = self._pk_field_name(metadata)
|
|
1443
|
+
if pk_name in out and out[pk_name] in (0, None, ""):
|
|
1444
|
+
out.pop(pk_name, None)
|
|
1445
|
+
if not out:
|
|
1446
|
+
raise FerrumCompileError(
|
|
1447
|
+
f"Insert on model {metadata.model_name!r} requires at least one field "
|
|
1448
|
+
"value after dropping auto-generated primary-key sentinel values.",
|
|
1449
|
+
model=metadata.model_name,
|
|
1450
|
+
)
|
|
1451
|
+
return out
|
|
1452
|
+
|
|
1385
1453
|
def _build_bulk_insert_ir(
|
|
1386
1454
|
self,
|
|
1387
1455
|
rows: list[dict[str, Any]],
|
|
@@ -1407,14 +1475,10 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
1407
1475
|
model=metadata.model_name,
|
|
1408
1476
|
)
|
|
1409
1477
|
field_index: dict[str, int] = {f.name: i for i, f in enumerate(metadata.fields)}
|
|
1410
|
-
pk_name = self._pk_field_name(metadata)
|
|
1411
1478
|
ir_rows: list[list[Any]] = []
|
|
1412
1479
|
column_order: list[str] | None = None
|
|
1413
1480
|
for row in rows:
|
|
1414
|
-
values =
|
|
1415
|
-
# Drop auto-generated PK columns with sentinel zero/null values.
|
|
1416
|
-
if pk_name in values and values[pk_name] in (0, None, ""):
|
|
1417
|
-
values.pop(pk_name, None)
|
|
1481
|
+
values = self._drop_auto_pk_sentinel(row, metadata)
|
|
1418
1482
|
if column_order is None:
|
|
1419
1483
|
column_order = sorted(values.keys())
|
|
1420
1484
|
elif sorted(values.keys()) != column_order:
|
|
@@ -1715,7 +1779,7 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
1715
1779
|
field_list = list(fields)
|
|
1716
1780
|
rows: list[tuple[Any, dict[str, Any]]] = []
|
|
1717
1781
|
for obj in objects:
|
|
1718
|
-
data =
|
|
1782
|
+
data = self._object_to_row_dict(obj)
|
|
1719
1783
|
for pk_name in pk_names:
|
|
1720
1784
|
if pk_name not in data:
|
|
1721
1785
|
raise FerrumCompileError(
|
|
@@ -2146,6 +2210,32 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
2146
2210
|
|
|
2147
2211
|
return upserted if returning else total
|
|
2148
2212
|
|
|
2213
|
+
def _check_write_scope(self, api: str) -> None:
|
|
2214
|
+
"""Reject queryset state that UPDATE/DELETE cannot honor.
|
|
2215
|
+
|
|
2216
|
+
``limit``/``offset`` and join/ranking state are silently absent from the
|
|
2217
|
+
write IR; letting them through would change which rows are affected
|
|
2218
|
+
(e.g. ``filter(...)[:10].delete()`` deleting every matching row). Fail
|
|
2219
|
+
loudly before compilation instead.
|
|
2220
|
+
"""
|
|
2221
|
+
if self._limit is not None or self._offset is not None:
|
|
2222
|
+
raise FerrumCompileError(
|
|
2223
|
+
f"{api} cannot be used on a sliced QuerySet; "
|
|
2224
|
+
"LIMIT/OFFSET do not apply to UPDATE/DELETE.",
|
|
2225
|
+
model=self._model.__name__,
|
|
2226
|
+
)
|
|
2227
|
+
if self._select_related:
|
|
2228
|
+
raise FerrumCompileError(
|
|
2229
|
+
f"{api} cannot be used with select_related(); joins do not apply to UPDATE/DELETE.",
|
|
2230
|
+
model=self._model.__name__,
|
|
2231
|
+
)
|
|
2232
|
+
if self._vector_order_by is not None or self._text_rank_by is not None:
|
|
2233
|
+
raise FerrumCompileError(
|
|
2234
|
+
f"{api} cannot be used with nearest_to()/rank_by()/search(); "
|
|
2235
|
+
"ranking does not apply to UPDATE/DELETE.",
|
|
2236
|
+
model=self._model.__name__,
|
|
2237
|
+
)
|
|
2238
|
+
|
|
2149
2239
|
async def delete(self, conn: ConnectionLike | None = None) -> int:
|
|
2150
2240
|
"""Delete filtered rows. Returns the row count.
|
|
2151
2241
|
|
|
@@ -2171,6 +2261,7 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
2171
2261
|
"Refusing unscoped delete(). Use QuerySet.danger_delete_all() "
|
|
2172
2262
|
"to explicitly delete all rows in the table."
|
|
2173
2263
|
)
|
|
2264
|
+
self._check_write_scope("delete()")
|
|
2174
2265
|
if _native_ext is None:
|
|
2175
2266
|
raise FerrumConfigError(_EXT_NOT_BUILT_MSG)
|
|
2176
2267
|
if conn is None:
|
|
@@ -2301,6 +2392,7 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
2301
2392
|
"Refusing unscoped update(). Use QuerySet.danger_update_all() "
|
|
2302
2393
|
"to explicitly update all rows in the table."
|
|
2303
2394
|
)
|
|
2395
|
+
self._check_write_scope("update()")
|
|
2304
2396
|
if _native_ext is None:
|
|
2305
2397
|
raise FerrumConfigError(_EXT_NOT_BUILT_MSG)
|
|
2306
2398
|
if conn is None:
|
|
@@ -2348,6 +2440,123 @@ class QuerySet(_QuerySetBase[_M], Generic[_M]):
|
|
|
2348
2440
|
)
|
|
2349
2441
|
return row_count
|
|
2350
2442
|
|
|
2443
|
+
async def update_instance(
|
|
2444
|
+
self,
|
|
2445
|
+
conn: ConnectionLike,
|
|
2446
|
+
obj: _M,
|
|
2447
|
+
*,
|
|
2448
|
+
fields: Sequence[str] | None = None,
|
|
2449
|
+
) -> int:
|
|
2450
|
+
"""Persist one instance's field values to its row, targeted by primary key.
|
|
2451
|
+
|
|
2452
|
+
The singular counterpart of ``bulk_update()``::
|
|
2453
|
+
|
|
2454
|
+
user = await User.objects.filter(email=email).first(conn)
|
|
2455
|
+
user.active = False
|
|
2456
|
+
count = await User.objects.update_instance(conn, user, fields=["active"])
|
|
2457
|
+
if count == 0:
|
|
2458
|
+
... # row was deleted concurrently (stale instance)
|
|
2459
|
+
|
|
2460
|
+
Values come from ``model_dump()`` (same as the bulk paths), the WHERE
|
|
2461
|
+
clause is the instance's primary key (composite PKs supported), and the
|
|
2462
|
+
call delegates to ``filter(pk=...).update(conn, ...)`` — inheriting the
|
|
2463
|
+
allowlist validation, bound parameters, Tier A hooks, and error mapping.
|
|
2464
|
+
|
|
2465
|
+
Prefer an explicit ``fields=[...]`` subset: ``fields=None`` writes every
|
|
2466
|
+
non-PK column and is last-writer-wins against concurrent updates.
|
|
2467
|
+
|
|
2468
|
+
Args:
|
|
2469
|
+
conn: An open ``Connection`` (obtained from ``ferrum.connect()``).
|
|
2470
|
+
obj: The model instance to persist. Never mutated.
|
|
2471
|
+
fields: Field names to update. ``None`` updates all non-PK fields.
|
|
2472
|
+
|
|
2473
|
+
Returns:
|
|
2474
|
+
The number of rows updated: ``1``, or ``0`` when no row matches the
|
|
2475
|
+
primary key (missing or concurrently deleted — caller decides).
|
|
2476
|
+
|
|
2477
|
+
Raises:
|
|
2478
|
+
FerrumCompileError: if called on a filtered QuerySet, if a primary
|
|
2479
|
+
key value is missing or a sentinel (``0``/``None``/``""``), if
|
|
2480
|
+
``fields`` is empty, unknown, contains a PK field, or overlaps
|
|
2481
|
+
the instance's deferred fields.
|
|
2482
|
+
"""
|
|
2483
|
+
model_name = self._model.__name__
|
|
2484
|
+
if self._is_filtered:
|
|
2485
|
+
raise FerrumCompileError(
|
|
2486
|
+
"update_instance() must be called on an unfiltered QuerySet — it "
|
|
2487
|
+
"targets the row by primary key. Use filter(...).update(conn, ...) "
|
|
2488
|
+
"for filtered updates.",
|
|
2489
|
+
model=model_name,
|
|
2490
|
+
)
|
|
2491
|
+
metadata = self._get_metadata()
|
|
2492
|
+
if metadata is None:
|
|
2493
|
+
raise FerrumCompileError(
|
|
2494
|
+
f"Model {model_name!r} has no metadata.",
|
|
2495
|
+
model=model_name,
|
|
2496
|
+
)
|
|
2497
|
+
field_index = {f.name: i for i, f in enumerate(metadata.fields)}
|
|
2498
|
+
pk_names = self._pk_field_names(metadata)
|
|
2499
|
+
deferred = frozenset(getattr(obj, "__ferrum_deferred__", None) or ())
|
|
2500
|
+
deferred_pks = sorted(set(pk_names) & deferred)
|
|
2501
|
+
if deferred_pks:
|
|
2502
|
+
raise FerrumCompileError(
|
|
2503
|
+
f"update_instance() requires loaded primary-key fields; "
|
|
2504
|
+
f"{deferred_pks!r} are deferred on model {metadata.model_name!r}.",
|
|
2505
|
+
model=metadata.model_name,
|
|
2506
|
+
)
|
|
2507
|
+
if fields is None:
|
|
2508
|
+
if deferred:
|
|
2509
|
+
raise FerrumCompileError(
|
|
2510
|
+
"update_instance() without fields=[...] cannot be used on an "
|
|
2511
|
+
f"instance with deferred fields {sorted(deferred)!r}; pass an "
|
|
2512
|
+
"explicit fields subset.",
|
|
2513
|
+
model=metadata.model_name,
|
|
2514
|
+
)
|
|
2515
|
+
field_list = [f.name for f in metadata.fields if f.name not in pk_names]
|
|
2516
|
+
else:
|
|
2517
|
+
field_list = list(dict.fromkeys(fields))
|
|
2518
|
+
if not field_list:
|
|
2519
|
+
raise FerrumCompileError(
|
|
2520
|
+
"update_instance() requires at least one field.",
|
|
2521
|
+
model=metadata.model_name,
|
|
2522
|
+
)
|
|
2523
|
+
for name in field_list:
|
|
2524
|
+
if name not in field_index:
|
|
2525
|
+
raise FerrumCompileError(
|
|
2526
|
+
f"Unknown field {name!r} on model {metadata.model_name!r}.",
|
|
2527
|
+
model=metadata.model_name,
|
|
2528
|
+
field=name,
|
|
2529
|
+
)
|
|
2530
|
+
if name in pk_names:
|
|
2531
|
+
raise FerrumCompileError(
|
|
2532
|
+
f"update_instance() cannot assign primary-key field {name!r}; "
|
|
2533
|
+
"primary keys identify the target row.",
|
|
2534
|
+
model=metadata.model_name,
|
|
2535
|
+
field=name,
|
|
2536
|
+
)
|
|
2537
|
+
if name in deferred:
|
|
2538
|
+
raise FerrumCompileError(
|
|
2539
|
+
f"update_instance() cannot write deferred field {name!r}; "
|
|
2540
|
+
"it was never loaded on this instance.",
|
|
2541
|
+
model=metadata.model_name,
|
|
2542
|
+
field=name,
|
|
2543
|
+
)
|
|
2544
|
+
row = obj.model_dump(include=set(field_list) | set(pk_names))
|
|
2545
|
+
pk_map: dict[str, Any] = {}
|
|
2546
|
+
for pk_name in pk_names:
|
|
2547
|
+
pk_value = row.get(pk_name)
|
|
2548
|
+
if pk_value in (0, None, ""):
|
|
2549
|
+
raise FerrumCompileError(
|
|
2550
|
+
f"update_instance() requires a primary-key value for field "
|
|
2551
|
+
f"{pk_name!r} on model {metadata.model_name!r}.",
|
|
2552
|
+
model=metadata.model_name,
|
|
2553
|
+
field=pk_name,
|
|
2554
|
+
)
|
|
2555
|
+
pk_map[pk_name] = pk_value
|
|
2556
|
+
assignments = {name: row[name] for name in field_list}
|
|
2557
|
+
target: QuerySet[_M] = QuerySet(self._model).filter(**pk_map)
|
|
2558
|
+
return await target.update(conn, **assignments)
|
|
2559
|
+
|
|
2351
2560
|
async def danger_update_all(self, conn: ConnectionLike, **assignments: Any) -> int: # noqa: ANN401
|
|
2352
2561
|
"""Update ALL rows in the table without a filter.
|
|
2353
2562
|
|
|
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
|
|
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
|