ferrum-orm 0.1.14__tar.gz → 0.1.16__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.
Files changed (82) hide show
  1. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/Cargo.lock +4 -4
  2. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/Cargo.toml +1 -1
  3. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/PKG-INFO +1 -1
  4. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/src/emit.rs +85 -3
  5. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/pyproject.toml +1 -1
  6. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/__init__.py +1 -1
  7. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/drivers/postgres.py +43 -5
  8. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/LICENSE +0 -0
  9. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/README.md +0 -0
  10. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/Cargo.toml +0 -0
  11. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/benches/hydrate.rs +0 -0
  12. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/src/compile/mod.rs +0 -0
  13. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/src/error.rs +0 -0
  14. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/src/hydrate/mod.rs +0 -0
  15. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/src/ir/metadata.rs +0 -0
  16. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/src/ir/mod.rs +0 -0
  17. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/src/lib.rs +0 -0
  18. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-core/src/migrate/mod.rs +0 -0
  19. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-pyo3/Cargo.toml +0 -0
  20. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-pyo3/src/lib.rs +0 -0
  21. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/Cargo.toml +0 -0
  22. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/benches/compile.rs +0 -0
  23. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/src/dialect.rs +0 -0
  24. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/src/fts/mod.rs +0 -0
  25. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/src/fts/mssql.rs +0 -0
  26. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/src/fts/mysql.rs +0 -0
  27. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/src/fts/postgres.rs +0 -0
  28. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/src/fts/sqlite.rs +0 -0
  29. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/crates/ferrum-sql/src/lib.rs +0 -0
  30. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/_native.pyi +0 -0
  31. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/__init__.py +0 -0
  32. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/app.py +0 -0
  33. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/bootstrap.py +0 -0
  34. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/init.py +0 -0
  35. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/inspectdb_cmd.py +0 -0
  36. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/makemigrations_cmd.py +0 -0
  37. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/migrate_cmd.py +0 -0
  38. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/migrations_cmd.py +0 -0
  39. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/resetdb_cmd.py +0 -0
  40. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/revert_cmd.py +0 -0
  41. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/showmigrations_cmd.py +0 -0
  42. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/cli/sqlmigrate_cmd.py +0 -0
  43. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/config.py +0 -0
  44. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/connection.py +0 -0
  45. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/contrib/__init__.py +0 -0
  46. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/contrib/fastapi.py +0 -0
  47. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/drivers/__init__.py +0 -0
  48. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/drivers/mssql.py +0 -0
  49. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/drivers/mysql.py +0 -0
  50. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/drivers/protocol.py +0 -0
  51. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/drivers/sqlite.py +0 -0
  52. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/drivers/streaming.py +0 -0
  53. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/echo.py +0 -0
  54. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/errors.py +0 -0
  55. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/expressions.py +0 -0
  56. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/ext/__init__.py +0 -0
  57. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/ext/fts.py +0 -0
  58. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/ext/pgvector.py +0 -0
  59. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/hooks.py +0 -0
  60. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/__init__.py +0 -0
  61. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/base.py +0 -0
  62. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/drift.py +0 -0
  63. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/fts/__init__.py +0 -0
  64. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/fts/mssql.py +0 -0
  65. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/fts/mysql.py +0 -0
  66. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/fts/postgres.py +0 -0
  67. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/fts/sqlite.py +0 -0
  68. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/gates.py +0 -0
  69. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/introspect.py +0 -0
  70. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/ledger.py +0 -0
  71. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/loader.py +0 -0
  72. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/operations.py +0 -0
  73. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/orchestrator.py +0 -0
  74. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/migrations/tokens.py +0 -0
  75. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/models.py +0 -0
  76. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/observability.py +0 -0
  77. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/py.typed +0 -0
  78. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/queryset.py +0 -0
  79. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/registry.py +0 -0
  80. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/relations.py +0 -0
  81. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/runtime.py +0 -0
  82. {ferrum_orm-0.1.14 → ferrum_orm-0.1.16}/python/ferrum/session.py +0 -0
@@ -223,7 +223,7 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
223
223
 
224
224
  [[package]]
225
225
  name = "ferrum-core"
226
- version = "0.1.14"
226
+ version = "0.1.16"
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.14"
237
+ version = "0.1.16"
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.14"
246
+ version = "0.1.16"
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.14"
258
+ version = "0.1.16"
259
259
  dependencies = [
260
260
  "criterion",
261
261
  "ferrum-core",
@@ -3,7 +3,7 @@ members = ["crates/ferrum-core", "crates/ferrum-sql", "crates/ferrum-pyo3"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.1.14"
6
+ version = "0.1.16"
7
7
  edition = "2021"
8
8
  authors = ["Ferrum Contributors"]
9
9
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ferrum-orm
3
- Version: 0.1.14
3
+ Version: 0.1.16
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Framework :: AsyncIO
6
6
  Classifier: Intended Audience :: Developers
@@ -1219,6 +1219,11 @@ fn filter_clause(
1219
1219
  /// `UPDATE … FROM (VALUES …)` requires explicit casts so `PostgreSQL` can infer
1220
1220
  /// the correct column types when the VALUES clause contains multiple rows.
1221
1221
  /// The cast is appended to the placeholder, e.g. `$1::bigint`, `$2::text`.
1222
+ ///
1223
+ /// The cast must be assignment-compatible with the DDL column type, and for PK
1224
+ /// columns it must match exactly: the join predicate `t.pk = v.pk` resolves an
1225
+ /// operator rather than an assignment cast, so a `uuid` column compared against
1226
+ /// a `text` VALUES column fails with `UndefinedFunction` (42883).
1222
1227
  fn postgres_value_cast(
1223
1228
  field_type: ferrum_core::ir::metadata::FieldType,
1224
1229
  placeholder: &str,
@@ -1226,8 +1231,11 @@ fn postgres_value_cast(
1226
1231
  use ferrum_core::ir::metadata::FieldType;
1227
1232
  let cast = match field_type {
1228
1233
  FieldType::Int | FieldType::BigInt => "bigint",
1229
- FieldType::Float | FieldType::Decimal => "double precision",
1230
- FieldType::Text | FieldType::Uuid | FieldType::TsVector | FieldType::Enum => "text",
1234
+ FieldType::Float => "double precision",
1235
+ FieldType::Decimal => "numeric",
1236
+ FieldType::Text | FieldType::Enum => "text",
1237
+ FieldType::Uuid => "uuid",
1238
+ FieldType::TsVector => "tsvector",
1231
1239
  FieldType::Bool => "boolean",
1232
1240
  FieldType::Datetime => "timestamptz",
1233
1241
  FieldType::Date => "date",
@@ -1235,7 +1243,8 @@ fn postgres_value_cast(
1235
1243
  FieldType::Json => "jsonb",
1236
1244
  FieldType::Bytes => "bytea",
1237
1245
  FieldType::Vector => "vector",
1238
- FieldType::ArrayText | FieldType::ArrayUuid => "text[]",
1246
+ FieldType::ArrayText => "text[]",
1247
+ FieldType::ArrayUuid => "uuid[]",
1239
1248
  FieldType::ArrayInt => "bigint[]",
1240
1249
  FieldType::ArrayFloat => "double precision[]",
1241
1250
  };
@@ -2112,6 +2121,79 @@ mod tests {
2112
2121
  assert!(matches!(err, CompileError::MalformedIr { .. }));
2113
2122
  }
2114
2123
 
2124
+ /// Every VALUES placeholder must carry the cast of its DDL column type.
2125
+ /// A `uuid` PK cast to `text` made `t.id = v.id` resolve `uuid = text`,
2126
+ /// which `PostgreSQL` rejects with `UndefinedFunction` (42883); `uuid[]` and
2127
+ /// `tsvector` columns failed the SET assignment for the same reason.
2128
+ #[test]
2129
+ fn bulk_update_casts_match_column_types() {
2130
+ let field = |name: &str, field_type: FieldType| FieldMeta {
2131
+ name: name.into(),
2132
+ column_name: name.into(),
2133
+ field_type,
2134
+ allowed_operators: vec!["eq".into()],
2135
+ nullable: true,
2136
+ vector_dimensions: None,
2137
+ fts_config: None,
2138
+ fts_source_columns: None,
2139
+ };
2140
+ let meta = ModelMetadata {
2141
+ model_name: "Ticket".into(),
2142
+ table_name: "tickets".into(),
2143
+ fields: vec![
2144
+ field("id", FieldType::Uuid),
2145
+ field("tags", FieldType::ArrayUuid),
2146
+ field("search", FieldType::TsVector),
2147
+ field("amount", FieldType::Decimal),
2148
+ ],
2149
+ pk_index: 0,
2150
+ pk_fields: vec![0],
2151
+ full_text_indexes: vec![],
2152
+ };
2153
+ let field_ref = |name: &str, index: usize| FieldRef {
2154
+ name: name.into(),
2155
+ index,
2156
+ };
2157
+ let ir = QuerySetIR {
2158
+ version: IR_VERSION,
2159
+ model_name: "Ticket".into(),
2160
+ operation: Operation::BulkUpdate {
2161
+ pk_fields: vec![field_ref("id", 0)],
2162
+ fields: vec![
2163
+ field_ref("tags", 1),
2164
+ field_ref("search", 2),
2165
+ field_ref("amount", 3),
2166
+ ],
2167
+ rows: vec![ferrum_core::ir::BulkUpdateRow {
2168
+ pk_values: vec![BindValue::Text(
2169
+ "00000000-0000-0000-0000-000000000001".into(),
2170
+ )],
2171
+ values: vec![
2172
+ BindValue::TextArray(vec![]),
2173
+ BindValue::Text("lexeme".into()),
2174
+ BindValue::Text("1.005".into()),
2175
+ ],
2176
+ }],
2177
+ },
2178
+ filters: vec![],
2179
+ order_by: vec![],
2180
+ limit: None,
2181
+ offset: None,
2182
+ vector_order_by: None,
2183
+ text_rank_by: None,
2184
+ predicate: None,
2185
+ distinct: false,
2186
+ exists: false,
2187
+ joins: vec![],
2188
+ aggregation: None,
2189
+ };
2190
+ let q = emit_bulk_update(Dialect::Postgres, &meta, &ir).unwrap();
2191
+ assert!(q.sql_text.contains("$1::uuid,"), "{}", q.sql_text);
2192
+ assert!(q.sql_text.contains("$2::uuid[]"), "{}", q.sql_text);
2193
+ assert!(q.sql_text.contains("$3::tsvector"), "{}", q.sql_text);
2194
+ assert!(q.sql_text.contains("$4::numeric"), "{}", q.sql_text);
2195
+ }
2196
+
2115
2197
  // ── MessagePack wire-format round-trip ───────────────────────────────────
2116
2198
 
2117
2199
  /// IR + metadata `MessagePack`-encoded (NAMED) decode back into the same serde
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "ferrum-orm"
7
- version = "0.1.14"
7
+ version = "0.1.16"
8
8
  description = "Next-generation async ORM for Python with a Rust-powered core"
9
9
  license = { text = "Apache-2.0" }
10
10
  readme = "README.md"
@@ -6,7 +6,7 @@ Import paths are stable API; internal module paths are not.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- __version__ = "0.1.14"
9
+ __version__ = "0.1.16"
10
10
  __all__ = [
11
11
  "Aggregate",
12
12
  "CreateExtension",
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import contextlib
6
+ import json
6
7
  from collections.abc import AsyncGenerator
7
8
  from typing import Any
8
9
 
@@ -11,6 +12,41 @@ from ferrum.drivers.streaming import AsyncpgChunkStream
11
12
  from ferrum.errors import FerrumConfigError, FerrumConnectionError, map_db_error
12
13
 
13
14
 
15
+ def _encode_json_param(value: Any) -> str:
16
+ """Encode a Python value for asyncpg ``json`` / ``jsonb`` text codecs.
17
+
18
+ Ferrum's bind path already ``json.dumps`` JSON fields to text before the
19
+ driver (with SQL ``::jsonb`` casts). Accept pre-serialized strings so a
20
+ codec encoder never double-encodes those binds if a future path sends the
21
+ parameter as a native json/jsonb OID.
22
+ """
23
+ if isinstance(value, str):
24
+ return value
25
+ return json.dumps(value, default=str, separators=(",", ":"))
26
+
27
+
28
+ def _decode_json_param(value: str) -> Any:
29
+ """Decode asyncpg ``json`` / ``jsonb`` text into native Python objects."""
30
+ return json.loads(value)
31
+
32
+
33
+ async def _register_json_codecs(conn: Any) -> None:
34
+ """Make JSONB/JSON columns hydrate as ``dict`` / ``list``, not ``str``.
35
+
36
+ asyncpg's default is to return ``json``/``jsonb`` as text. Without this,
37
+ ``model_construct`` leaves JSON fields as strings and Pydantic consumers
38
+ that expect ``dict`` fail at the agent/MCP boundary.
39
+ """
40
+ for type_name in ("jsonb", "json"):
41
+ await conn.set_type_codec(
42
+ type_name,
43
+ schema="pg_catalog",
44
+ encoder=_encode_json_param,
45
+ decoder=_decode_json_param,
46
+ format="text",
47
+ )
48
+
49
+
14
50
  class _BoundConnection:
15
51
  """Execution surface pinned to a single raw connection inside a transaction.
16
52
 
@@ -139,13 +175,15 @@ class AsyncpgDriver:
139
175
  }
140
176
  if self._max_lifetime is not None:
141
177
  pool_kwargs["max_inactive_connection_lifetime"] = self._max_lifetime
142
- if self._statement_timeout_ms is not None:
143
- timeout_ms = self._statement_timeout_ms
144
178
 
145
- async def _init_conn(conn: Any) -> None:
146
- await conn.execute(f"SET statement_timeout = {timeout_ms}")
179
+ statement_timeout_ms = self._statement_timeout_ms
180
+
181
+ async def _init_conn(conn: Any) -> None:
182
+ await _register_json_codecs(conn)
183
+ if statement_timeout_ms is not None:
184
+ await conn.execute(f"SET statement_timeout = {statement_timeout_ms}")
147
185
 
148
- pool_kwargs["init"] = _init_conn
186
+ pool_kwargs["init"] = _init_conn
149
187
  try:
150
188
  self._pool = await asyncpg.create_pool(self._dsn, **pool_kwargs)
151
189
  except Exception as exc:
File without changes
File without changes