ferrum-orm 0.1.16__tar.gz → 0.1.17__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.16 → ferrum_orm-0.1.17}/Cargo.lock +4 -4
  2. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/Cargo.toml +1 -1
  3. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/PKG-INFO +1 -1
  4. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/pyproject.toml +1 -1
  5. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/__init__.py +1 -1
  6. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/postgres.py +37 -0
  7. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/ext/pgvector.py +33 -46
  8. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/queryset.py +9 -1
  9. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/LICENSE +0 -0
  10. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/README.md +0 -0
  11. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/Cargo.toml +0 -0
  12. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/benches/hydrate.rs +0 -0
  13. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/compile/mod.rs +0 -0
  14. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/error.rs +0 -0
  15. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/hydrate/mod.rs +0 -0
  16. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/ir/metadata.rs +0 -0
  17. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/ir/mod.rs +0 -0
  18. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/lib.rs +0 -0
  19. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/migrate/mod.rs +0 -0
  20. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-pyo3/Cargo.toml +0 -0
  21. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-pyo3/src/lib.rs +0 -0
  22. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/Cargo.toml +0 -0
  23. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/benches/compile.rs +0 -0
  24. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/dialect.rs +0 -0
  25. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/emit.rs +0 -0
  26. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mod.rs +0 -0
  27. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mssql.rs +0 -0
  28. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mysql.rs +0 -0
  29. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/postgres.rs +0 -0
  30. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/sqlite.rs +0 -0
  31. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/lib.rs +0 -0
  32. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/_native.pyi +0 -0
  33. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/__init__.py +0 -0
  34. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/app.py +0 -0
  35. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/bootstrap.py +0 -0
  36. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/init.py +0 -0
  37. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/inspectdb_cmd.py +0 -0
  38. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/makemigrations_cmd.py +0 -0
  39. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/migrate_cmd.py +0 -0
  40. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/migrations_cmd.py +0 -0
  41. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/resetdb_cmd.py +0 -0
  42. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/revert_cmd.py +0 -0
  43. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/showmigrations_cmd.py +0 -0
  44. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/sqlmigrate_cmd.py +0 -0
  45. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/config.py +0 -0
  46. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/connection.py +0 -0
  47. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/contrib/__init__.py +0 -0
  48. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/contrib/fastapi.py +0 -0
  49. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/__init__.py +0 -0
  50. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/mssql.py +0 -0
  51. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/mysql.py +0 -0
  52. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/protocol.py +0 -0
  53. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/sqlite.py +0 -0
  54. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/streaming.py +0 -0
  55. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/echo.py +0 -0
  56. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/errors.py +0 -0
  57. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/expressions.py +0 -0
  58. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/ext/__init__.py +0 -0
  59. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/ext/fts.py +0 -0
  60. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/hooks.py +0 -0
  61. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/__init__.py +0 -0
  62. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/base.py +0 -0
  63. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/drift.py +0 -0
  64. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/__init__.py +0 -0
  65. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/mssql.py +0 -0
  66. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/mysql.py +0 -0
  67. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/postgres.py +0 -0
  68. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/sqlite.py +0 -0
  69. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/gates.py +0 -0
  70. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/introspect.py +0 -0
  71. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/ledger.py +0 -0
  72. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/loader.py +0 -0
  73. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/operations.py +0 -0
  74. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/orchestrator.py +0 -0
  75. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/tokens.py +0 -0
  76. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/models.py +0 -0
  77. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/observability.py +0 -0
  78. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/py.typed +0 -0
  79. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/registry.py +0 -0
  80. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/relations.py +0 -0
  81. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/runtime.py +0 -0
  82. {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/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.16"
226
+ version = "0.1.17"
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.16"
237
+ version = "0.1.17"
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.16"
246
+ version = "0.1.17"
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.16"
258
+ version = "0.1.17"
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.16"
6
+ version = "0.1.17"
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.16
3
+ Version: 0.1.17
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Framework :: AsyncIO
6
6
  Classifier: Intended Audience :: Developers
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "ferrum-orm"
7
- version = "0.1.16"
7
+ version = "0.1.17"
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.16"
9
+ __version__ = "0.1.17"
10
10
  __all__ = [
11
11
  "Aggregate",
12
12
  "CreateExtension",
@@ -159,6 +159,7 @@ class AsyncpgDriver:
159
159
  self._statement_timeout_ms = statement_timeout_ms
160
160
  self._max_lifetime = max_lifetime
161
161
  self._pool: Any = None
162
+ self._extra_codecs: list[dict[str, Any]] = []
162
163
 
163
164
  async def open(self) -> None:
164
165
  try:
@@ -177,9 +178,12 @@ class AsyncpgDriver:
177
178
  pool_kwargs["max_inactive_connection_lifetime"] = self._max_lifetime
178
179
 
179
180
  statement_timeout_ms = self._statement_timeout_ms
181
+ extra_codecs = self._extra_codecs
180
182
 
181
183
  async def _init_conn(conn: Any) -> None:
182
184
  await _register_json_codecs(conn)
185
+ for codec in extra_codecs:
186
+ await conn.set_type_codec(**codec)
183
187
  if statement_timeout_ms is not None:
184
188
  await conn.execute(f"SET statement_timeout = {statement_timeout_ms}")
185
189
 
@@ -193,6 +197,39 @@ class AsyncpgDriver:
193
197
  f"{type(exc).__name__} [FERR-E101]"
194
198
  ) from None
195
199
 
200
+ async def add_type_codec(
201
+ self,
202
+ type_name: str,
203
+ *,
204
+ schema: str,
205
+ encoder: Any,
206
+ decoder: Any,
207
+ format: str = "text",
208
+ ) -> None:
209
+ """Register an asyncpg type codec on every connection in the pool.
210
+
211
+ asyncpg exposes ``set_type_codec`` on a connection, not on a pool, so
212
+ registering directly against one acquired connection only covers that
213
+ connection — every other pooled connection keeps the default codec and
214
+ the same query succeeds or fails depending on which one it lands on.
215
+ Recording the codec here applies it from the pool ``init`` hook, and
216
+ expiring the pool forces already-initialized connections to be replaced
217
+ so the registration is uniform.
218
+ """
219
+ if any(c["typename"] == type_name and c["schema"] == schema for c in self._extra_codecs):
220
+ return
221
+ self._extra_codecs.append(
222
+ {
223
+ "typename": type_name,
224
+ "schema": schema,
225
+ "encoder": encoder,
226
+ "decoder": decoder,
227
+ "format": format,
228
+ }
229
+ )
230
+ if self._pool is not None:
231
+ await self._pool.expire_connections()
232
+
196
233
  async def close(self) -> None:
197
234
  if self._pool is not None:
198
235
  await self._pool.close()
@@ -61,40 +61,43 @@ async def register_vector_codecs(
61
61
  ) -> None:
62
62
  """Ensure the ``vector`` extension exists and register asyncpg codecs.
63
63
 
64
+ The codec is installed pool-wide (every current and future connection), so
65
+ ``vector`` columns decode to ``list[float]`` no matter which pooled
66
+ connection serves a query.
67
+
68
+ Writing a vector does not require this call: Ferrum binds vector values as
69
+ pgvector text literals, which asyncpg accepts without a codec.
70
+
64
71
  Idempotent: safe to call multiple times and from concurrent startup paths.
65
- ``DuplicateObjectError`` from the extension-creation step and codec
66
- re-registration on an already-configured pool are both handled gracefully.
72
+ ``DuplicateObjectError`` from the extension-creation step and repeat
73
+ registration of the same codec are both handled gracefully.
67
74
 
68
75
  Args:
69
76
  conn: An open Ferrum ``Connection``.
70
- timeout: Statement timeout (seconds) for the ``CREATE EXTENSION`` DDL.
77
+ timeout: Timeout (seconds) for the ``CREATE EXTENSION`` DDL.
71
78
  Defaults to 5 s. Set to ``0`` to disable the timeout guard.
72
79
 
73
80
  Raises:
74
- FerrumConfigError: If the connection is not a PostgreSQL connection or
75
- the pool is not open.
81
+ FerrumConfigError: If the connection is not a PostgreSQL connection, the
82
+ pool is not open, or the driver is not the asyncpg driver.
76
83
  """
77
84
  if conn.dialect != "postgres":
78
85
  raise FerrumConfigError(
79
86
  "pgvector integration requires a PostgreSQL connection. [FERR-C001]"
80
87
  )
81
- driver = conn._require_driver()
88
+ driver = conn._driver
82
89
  pool = getattr(driver, "_pool", None)
83
- # TimedQueryExecutor wraps the real driver; fall back to its _inner attribute.
84
- if pool is None:
85
- inner = getattr(driver, "_inner", None)
86
- pool = getattr(inner, "_pool", None) if inner is not None else None
87
- if pool is None:
90
+ if driver is None or pool is None:
88
91
  raise FerrumConfigError("PostgreSQL pool is not open. [FERR-C001]")
89
92
 
90
93
  # CREATE EXTENSION — idempotent via IF NOT EXISTS; the DuplicateObjectError
91
94
  # guard covers rare race conditions where two concurrent startup paths both
92
95
  # attempt the DDL at the same moment.
93
96
  try:
94
- create_sql = "CREATE EXTENSION IF NOT EXISTS vector"
95
- if timeout > 0:
96
- create_sql = f"SET LOCAL statement_timeout = {int(timeout * 1000)}; {create_sql}"
97
- await pool.execute("CREATE EXTENSION IF NOT EXISTS vector")
97
+ await pool.execute(
98
+ "CREATE EXTENSION IF NOT EXISTS vector",
99
+ timeout=timeout if timeout > 0 else None,
100
+ )
98
101
  except Exception as exc:
99
102
  # asyncpg raises DuplicateObjectError (SQLSTATE 42710) if another
100
103
  # concurrent caller committed the extension between our IF NOT EXISTS
@@ -103,37 +106,21 @@ async def register_vector_codecs(
103
106
  if "DuplicateObject" not in exc_name:
104
107
  raise
105
108
 
106
- # Codec registration — asyncpg raises InvalidStateError if the same custom
107
- # type is registered twice on the same pool. Treat it as idempotent.
108
- try:
109
- set_pool_codec = getattr(pool, "set_type_codec", None)
110
- if set_pool_codec is not None:
111
- await set_pool_codec(
112
- "vector",
113
- schema="public",
114
- encoder=_encode_vector,
115
- decoder=_decode_vector,
116
- format="text",
117
- )
118
- else:
119
- # asyncpg exposes codecs on Connection, not Pool. Ferrum's normal
120
- # vector query path casts bound text to ``vector`` and projects the
121
- # vector column out, so registering the currently acquired
122
- # connection is sufficient for callers that explicitly hydrate a
123
- # vector immediately after startup without making pool startup fail.
124
- async with pool.acquire() as raw_conn:
125
- await raw_conn.set_type_codec(
126
- "vector",
127
- schema="public",
128
- encoder=_encode_vector,
129
- decoder=_decode_vector,
130
- format="text",
131
- )
132
- except Exception as exc:
133
- exc_name = type(exc).__name__
134
- if exc_name not in ("InvalidStateError", "AlreadyInitializedError"):
135
- # Re-raise unknown errors; swallow the known re-registration ones.
136
- raise
109
+ # Registering through the driver applies the codec from the pool's ``init``
110
+ # hook, so every connection — including ones the pool opens later — decodes
111
+ # ``vector`` columns to ``list[float]``. Registering against a single
112
+ # acquired connection instead made vector reads depend on which pooled
113
+ # connection served the query.
114
+ add_codec = getattr(driver, "add_type_codec", None)
115
+ if add_codec is None:
116
+ raise FerrumConfigError("pgvector integration requires the asyncpg driver. [FERR-C001]")
117
+ await add_codec(
118
+ "vector",
119
+ schema="public",
120
+ encoder=_encode_vector,
121
+ decoder=_decode_vector,
122
+ format="text",
123
+ )
137
124
 
138
125
 
139
126
  async def vector_search(
@@ -215,8 +215,16 @@ def _encode_bind_value(value: object) -> dict[str, object]:
215
215
 
216
216
  def _prepare_field_value(field: Any, value: object) -> object: # noqa: ANN401
217
217
  """Normalize a Python value for the field's database representation."""
218
- if value is not None and field.field_type == "json":
218
+ if value is None:
219
+ return value
220
+ if field.field_type == "json":
219
221
  return json.dumps(value, default=str, separators=(",", ":"))
222
+ if field.field_type == "vector" and isinstance(value, (list, tuple)):
223
+ # asyncpg has no built-in codec for the pgvector ``vector`` type and
224
+ # falls back to text, so a bound ``list[float]`` raises DataError
225
+ # ("expected str, got list"). Bind the pgvector text literal instead —
226
+ # the same encoding ``nearest_to()`` uses on the read path.
227
+ return _encode_vector_literal([float(v) for v in cast(list[Any], value)])
220
228
  return value
221
229
 
222
230
 
File without changes
File without changes