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.
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/Cargo.lock +4 -4
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/Cargo.toml +1 -1
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/PKG-INFO +1 -1
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/pyproject.toml +1 -1
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/__init__.py +1 -1
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/postgres.py +37 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/ext/pgvector.py +33 -46
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/queryset.py +9 -1
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/LICENSE +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/README.md +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/Cargo.toml +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/benches/hydrate.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/compile/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/error.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/hydrate/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/ir/metadata.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/ir/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/lib.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-core/src/migrate/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-pyo3/Cargo.toml +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-pyo3/src/lib.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/Cargo.toml +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/benches/compile.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/dialect.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/emit.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mod.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mssql.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mysql.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/postgres.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/sqlite.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/lib.rs +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/_native.pyi +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/app.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/bootstrap.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/init.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/inspectdb_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/makemigrations_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/migrate_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/migrations_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/resetdb_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/revert_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/showmigrations_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/cli/sqlmigrate_cmd.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/config.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/connection.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/contrib/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/contrib/fastapi.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/mssql.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/mysql.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/protocol.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/sqlite.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/drivers/streaming.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/echo.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/errors.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/expressions.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/ext/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/ext/fts.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/hooks.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/base.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/drift.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/__init__.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/mssql.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/mysql.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/postgres.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/sqlite.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/gates.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/introspect.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/ledger.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/loader.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/operations.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/orchestrator.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/migrations/tokens.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/models.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/observability.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/py.typed +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/registry.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/relations.py +0 -0
- {ferrum_orm-0.1.16 → ferrum_orm-0.1.17}/python/ferrum/runtime.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
258
|
+
version = "0.1.17"
|
|
259
259
|
dependencies = [
|
|
260
260
|
"criterion",
|
|
261
261
|
"ferrum-core",
|
|
@@ -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
|
|
66
|
-
|
|
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:
|
|
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
|
|
75
|
-
the
|
|
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.
|
|
88
|
+
driver = conn._driver
|
|
82
89
|
pool = getattr(driver, "_pool", None)
|
|
83
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
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
|