ferrum-orm 0.1.15__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.15 → ferrum_orm-0.1.17}/Cargo.lock +4 -4
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/Cargo.toml +1 -1
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/PKG-INFO +1 -1
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/emit.rs +85 -3
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/pyproject.toml +1 -1
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/__init__.py +1 -1
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/drivers/postgres.py +37 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/ext/pgvector.py +33 -46
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/queryset.py +9 -1
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/LICENSE +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/README.md +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/Cargo.toml +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/benches/hydrate.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/src/compile/mod.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/src/error.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/src/hydrate/mod.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/src/ir/metadata.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/src/ir/mod.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/src/lib.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-core/src/migrate/mod.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-pyo3/Cargo.toml +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-pyo3/src/lib.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/Cargo.toml +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/benches/compile.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/dialect.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mod.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mssql.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/mysql.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/postgres.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/fts/sqlite.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/crates/ferrum-sql/src/lib.rs +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/_native.pyi +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/__init__.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/app.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/bootstrap.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/init.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/inspectdb_cmd.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/makemigrations_cmd.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/migrate_cmd.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/migrations_cmd.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/resetdb_cmd.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/revert_cmd.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/showmigrations_cmd.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/cli/sqlmigrate_cmd.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/config.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/connection.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/contrib/__init__.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/contrib/fastapi.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/drivers/__init__.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/drivers/mssql.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/drivers/mysql.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/drivers/protocol.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/drivers/sqlite.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/drivers/streaming.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/echo.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/errors.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/expressions.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/ext/__init__.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/ext/fts.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/hooks.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/__init__.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/base.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/drift.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/__init__.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/mssql.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/mysql.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/postgres.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/fts/sqlite.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/gates.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/introspect.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/ledger.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/loader.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/operations.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/orchestrator.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/migrations/tokens.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/models.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/observability.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/py.typed +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/registry.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/relations.py +0 -0
- {ferrum_orm-0.1.15 → ferrum_orm-0.1.17}/python/ferrum/runtime.py +0 -0
- {ferrum_orm-0.1.15 → 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",
|
|
@@ -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
|
|
1230
|
-
FieldType::
|
|
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
|
|
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
|
|
@@ -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
|