sdpy-kit 0.2.3__tar.gz → 0.2.4__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 (46) hide show
  1. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/PKG-INFO +41 -8
  2. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/README.md +40 -7
  3. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/__init__.py +1 -1
  4. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/base_db_kit.py +170 -4
  5. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/mysql_kit.py +31 -63
  6. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/pg_kit.py +27 -55
  7. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/sqlite_kit.py +33 -55
  8. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/.gitignore +0 -0
  9. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/__init__.py +0 -0
  10. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/base_service.py +0 -0
  11. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/chat_service.py +0 -0
  12. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/embedding_service.py +0 -0
  13. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/exceptions.py +0 -0
  14. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/failover.py +0 -0
  15. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/image_service.py +0 -0
  16. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/model_builder.py +0 -0
  17. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/model_client.py +0 -0
  18. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/pool.py +0 -0
  19. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/profiles.py +0 -0
  20. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/rerank_service.py +0 -0
  21. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/runner.py +0 -0
  22. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/ai/types.py +0 -0
  23. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/anydoc_kit.py +0 -0
  24. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/common_kit.py +0 -0
  25. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/config/__init__.py +0 -0
  26. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/config/kit_config.py +0 -0
  27. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/config/log_config.py +0 -0
  28. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/__init__.py +0 -0
  29. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/crud.py +0 -0
  30. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/engine.py +0 -0
  31. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/exceptions.py +0 -0
  32. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/models/__init__.py +0 -0
  33. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/models/base.py +0 -0
  34. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/query.py +0 -0
  35. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/session.py +0 -0
  36. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/db/transaction.py +0 -0
  37. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/docx_kit.py +0 -0
  38. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/file_kit.py +0 -0
  39. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/marker_kit.py +0 -0
  40. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/mcp_kit.py +0 -0
  41. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/md2doc_kit.py +0 -0
  42. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/mineru_kit.py +0 -0
  43. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/redis_kit.py +0 -0
  44. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/wx_article_kit.py +0 -0
  45. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/kit/zvec_kit.py +0 -0
  46. {sdpy_kit-0.2.3 → sdpy_kit-0.2.4}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sdpy-kit
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/MCP/Redis/文件/文档/向量工具集
5
5
  Author-email: Clark <changhongyuan@126.com>
6
6
  License-Expression: MIT
@@ -336,7 +336,7 @@ from kit.db import session_scope, transactional, DBError # 推荐
336
336
  from kit import session_scope, transactional, DBError # 等价
337
337
  ```
338
338
 
339
- ### 1. 三个数据库 Kit(23 个共享方法 API 完全一致)
339
+ ### 1. 三个数据库 Kit(24 个共享方法 API 完全一致)
340
340
 
341
341
  | 类 | 导入 | 数据库 | extra |
342
342
  |----|------|--------|-------|
@@ -424,8 +424,9 @@ with SQLiteKit.transaction() as session: # 多操作原子提
424
424
 
425
425
  | 方法 | 说明 |
426
426
  |------|------|
427
- | `create_vector_table(table_name, dims, id_col="id", column="embedding", extra_cols=None) -> None` | 建含向量列的表;**`dims` 必填**(SQLite 用 `CHECK` 落实为数据库层约束;PG 硬约束并回读校验已存在表;MySQL 为上限语义) |
428
- | `insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> int` | 批量插入向量(单事务提交);`ids=None` 时由 Kit 生成雪花 ID;接受 `list[list[float]]` 或二维 numpy 数组 |
427
+ | `create_vector_table(table_name, dims, id_col="id", column="embedding", extra_cols=None) -> None` | 建含向量列的表;**`dims` 必填**(SQLite 用 `CHECK` 落实为数据库层约束;PG 硬约束并回读校验已存在表;MySQL 为上限语义)。`extra_cols` 支持 `{列名: 类型串}`(推荐,与 `create_raw_table` 同形)与 `[{"name","type"}]` 两种形态 |
428
+ | `insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> int` | 批量插入向量(**自成一事务并提交**);`ids=None` 时由 Kit 生成雪花 ID;接受 `list[list[float]]` 或二维 numpy 数组 |
429
+ | `build_insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> list[tuple[str, list[dict]]]` | **只拼装、不执行、不管事务**:返回按列集分组的 `(SQL, 参数列表)`,供调用方在**自有事务**内写向量行(见「自有事务内写向量」) |
429
430
  | `search_vectors(table_name, query_vector, top_k=10, distance_type="L2", filter_sql=None, exact=True, id_col="id", column="embedding") -> list[dict]` | KNN 检索,按距离升序返回 `{"id", "distance"}`;`exact=True`(默认)禁止近似加速;**MySQL 侧抛 `VectorNotSupportedError`** |
430
431
  | `vector_version() -> str` | 向量后端版本;不可用时返回 `"unavailable"`(MySQL 侧为服务端版本号) |
431
432
 
@@ -510,6 +511,16 @@ PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
510
511
  > 新增(非破坏性):`VECTOR_CAPABILITIES` 能力声明、`id_col` / `column` 逃生口参数、
511
512
  > `insert_vectors` 支持二维 numpy 数组与批量提交。
512
513
 
514
+ #### 0.2.4 变更
515
+
516
+ | 编号 | 变更 | 影响 |
517
+ |------|------|------|
518
+ | B4 | **入参自洽性校验**(`extra_data` 的键):保留绑定名 `id` / `vec`、与 `id_col` / `column` 重复、不是合法标识符、`id_col == column` → 抛可操作的 `ValueError` | **唯一行为变更**:此前 SQLite 会**静默写坏数据**(如 `id` 列被覆盖)、PG/MySQL 报误导性错误;正常调用**零影响** |
519
+ | — | 新增共享方法 `build_insert_vectors`(只拼装不执行,由 `BaseDbKit` 收敛 → 共享方法 23 → **24**) | 纯新增:可在**自有事务**内写向量行(见下) |
520
+ | — | `create_vector_table` 的 `extra_cols` 支持 `{列名: 类型串}`(与 `create_raw_table` 同形) | 纯新增:既有 `[{"name","type"}]` 形态保留,两者产出 DDL 逐字一致 |
521
+ | — | 后端差异收口:向量列占位符由 `VECTOR_PLACEHOLDER` 驱动(MySQL = `STRING_TO_VECTOR(:vec)`) | 内部整理:公开签名与行为不变 |
522
+ | — | `SQLiteKit.insert_vectors` 的事务获取统一为 `transaction()`(与另两库同构) | 内部一致:行为等价(自成一事务并提交) |
523
+
513
524
  #### 0.2.3 变更
514
525
 
515
526
  | 编号 | 变更 | 影响 |
@@ -527,18 +538,40 @@ rows = SQLiteKit.query("SELECT id, title FROM items WHERE id IN :ids",
527
538
  {"ids": [h["id"] for h in hits]}) # IN 不保证顺序,需按 hits 重排
528
539
  ```
529
540
 
530
- **自有事务内写向量(SQLite)**:
541
+ **自有事务内写向量(推荐:`build_insert_vectors` + `transaction()`)**:
531
542
 
532
543
  ```python
533
- conn = SQLiteKit.raw_connection() # DBAPI 连接:位置参数 ? + 手工 commit
544
+ from sqlalchemy import text
545
+
546
+ with SQLiteKit.transaction() as session: # 换成 PgKit / MySqlKit 写法相同
547
+ session.execute(text("INSERT INTO docs_item (id, title) VALUES (:id, :t)"),
548
+ {"id": doc_id, "t": title})
549
+ for sql, params in SQLiteKit.build_insert_vectors("docs_doc_vectors", vectors,
550
+ ids=row_ids, extra_data=metas,
551
+ id_col="rowid"):
552
+ session.execute(text(sql), params)
553
+ # 正常退出统一提交;异常自动回滚(多处写入保持同一事务)
554
+ ```
555
+
556
+ **SQLite 的 DBAPI 变体**(`raw_connection()` 支持 `:name` 命名参数):
557
+
558
+ ```python
559
+ conn = SQLiteKit.raw_connection() # 调用方负责 commit / close
534
560
  try:
535
- conn.execute("INSERT INTO item_vectors (id, embedding) VALUES (?, ?)",
536
- (item_id, SQLiteKit.encode_vector(vec)))
561
+ for sql, params in SQLiteKit.build_insert_vectors("item_vectors", vectors,
562
+ ids=row_ids, extra_data=metas):
563
+ conn.executemany(sql, params)
537
564
  conn.commit()
538
565
  finally:
539
566
  conn.close()
540
567
  ```
541
568
 
569
+ > ⚠ 两点约束:① `build_insert_vectors` 输出为 `:name` 风格,**PG / MySQL 的原生 DBAPI 不支持**
570
+ > (psycopg2 / pymysql 需 `%(name)s`),只能经 `transaction()` 的 session 执行;
571
+ > ② SQLite 上**持有未提交写事务**时再调 kit 的**自提交**方法(如 `insert_vectors`)会因另一个连接
572
+ > 拿不到写锁而失败(`OperationalError: database is locked`,约 **6.4s** 后才报错)——
573
+ > 把写入并入**同一个** `transaction()`(如上)即可避免。
574
+
542
575
  > 相似度换算建议:SQLite 仅支持 L2,若向量入库前已**归一化**,则 `cos = 1 − L2²/2`;
543
576
  > PG 可直接用 `distance_type="COSINE"`。
544
577
 
@@ -266,7 +266,7 @@ from kit.db import session_scope, transactional, DBError # 推荐
266
266
  from kit import session_scope, transactional, DBError # 等价
267
267
  ```
268
268
 
269
- ### 1. 三个数据库 Kit(23 个共享方法 API 完全一致)
269
+ ### 1. 三个数据库 Kit(24 个共享方法 API 完全一致)
270
270
 
271
271
  | 类 | 导入 | 数据库 | extra |
272
272
  |----|------|--------|-------|
@@ -354,8 +354,9 @@ with SQLiteKit.transaction() as session: # 多操作原子提
354
354
 
355
355
  | 方法 | 说明 |
356
356
  |------|------|
357
- | `create_vector_table(table_name, dims, id_col="id", column="embedding", extra_cols=None) -> None` | 建含向量列的表;**`dims` 必填**(SQLite 用 `CHECK` 落实为数据库层约束;PG 硬约束并回读校验已存在表;MySQL 为上限语义) |
358
- | `insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> int` | 批量插入向量(单事务提交);`ids=None` 时由 Kit 生成雪花 ID;接受 `list[list[float]]` 或二维 numpy 数组 |
357
+ | `create_vector_table(table_name, dims, id_col="id", column="embedding", extra_cols=None) -> None` | 建含向量列的表;**`dims` 必填**(SQLite 用 `CHECK` 落实为数据库层约束;PG 硬约束并回读校验已存在表;MySQL 为上限语义)。`extra_cols` 支持 `{列名: 类型串}`(推荐,与 `create_raw_table` 同形)与 `[{"name","type"}]` 两种形态 |
358
+ | `insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> int` | 批量插入向量(**自成一事务并提交**);`ids=None` 时由 Kit 生成雪花 ID;接受 `list[list[float]]` 或二维 numpy 数组 |
359
+ | `build_insert_vectors(table_name, vectors, ids=None, extra_data=None, id_col="id", column="embedding") -> list[tuple[str, list[dict]]]` | **只拼装、不执行、不管事务**:返回按列集分组的 `(SQL, 参数列表)`,供调用方在**自有事务**内写向量行(见「自有事务内写向量」) |
359
360
  | `search_vectors(table_name, query_vector, top_k=10, distance_type="L2", filter_sql=None, exact=True, id_col="id", column="embedding") -> list[dict]` | KNN 检索,按距离升序返回 `{"id", "distance"}`;`exact=True`(默认)禁止近似加速;**MySQL 侧抛 `VectorNotSupportedError`** |
360
361
  | `vector_version() -> str` | 向量后端版本;不可用时返回 `"unavailable"`(MySQL 侧为服务端版本号) |
361
362
 
@@ -440,6 +441,16 @@ PgKit.search_vectors("my_vec", [0.1] * 384, top_k=5, id_col="vid", column="vec")
440
441
  > 新增(非破坏性):`VECTOR_CAPABILITIES` 能力声明、`id_col` / `column` 逃生口参数、
441
442
  > `insert_vectors` 支持二维 numpy 数组与批量提交。
442
443
 
444
+ #### 0.2.4 变更
445
+
446
+ | 编号 | 变更 | 影响 |
447
+ |------|------|------|
448
+ | B4 | **入参自洽性校验**(`extra_data` 的键):保留绑定名 `id` / `vec`、与 `id_col` / `column` 重复、不是合法标识符、`id_col == column` → 抛可操作的 `ValueError` | **唯一行为变更**:此前 SQLite 会**静默写坏数据**(如 `id` 列被覆盖)、PG/MySQL 报误导性错误;正常调用**零影响** |
449
+ | — | 新增共享方法 `build_insert_vectors`(只拼装不执行,由 `BaseDbKit` 收敛 → 共享方法 23 → **24**) | 纯新增:可在**自有事务**内写向量行(见下) |
450
+ | — | `create_vector_table` 的 `extra_cols` 支持 `{列名: 类型串}`(与 `create_raw_table` 同形) | 纯新增:既有 `[{"name","type"}]` 形态保留,两者产出 DDL 逐字一致 |
451
+ | — | 后端差异收口:向量列占位符由 `VECTOR_PLACEHOLDER` 驱动(MySQL = `STRING_TO_VECTOR(:vec)`) | 内部整理:公开签名与行为不变 |
452
+ | — | `SQLiteKit.insert_vectors` 的事务获取统一为 `transaction()`(与另两库同构) | 内部一致:行为等价(自成一事务并提交) |
453
+
443
454
  #### 0.2.3 变更
444
455
 
445
456
  | 编号 | 变更 | 影响 |
@@ -457,18 +468,40 @@ rows = SQLiteKit.query("SELECT id, title FROM items WHERE id IN :ids",
457
468
  {"ids": [h["id"] for h in hits]}) # IN 不保证顺序,需按 hits 重排
458
469
  ```
459
470
 
460
- **自有事务内写向量(SQLite)**:
471
+ **自有事务内写向量(推荐:`build_insert_vectors` + `transaction()`)**:
461
472
 
462
473
  ```python
463
- conn = SQLiteKit.raw_connection() # DBAPI 连接:位置参数 ? + 手工 commit
474
+ from sqlalchemy import text
475
+
476
+ with SQLiteKit.transaction() as session: # 换成 PgKit / MySqlKit 写法相同
477
+ session.execute(text("INSERT INTO docs_item (id, title) VALUES (:id, :t)"),
478
+ {"id": doc_id, "t": title})
479
+ for sql, params in SQLiteKit.build_insert_vectors("docs_doc_vectors", vectors,
480
+ ids=row_ids, extra_data=metas,
481
+ id_col="rowid"):
482
+ session.execute(text(sql), params)
483
+ # 正常退出统一提交;异常自动回滚(多处写入保持同一事务)
484
+ ```
485
+
486
+ **SQLite 的 DBAPI 变体**(`raw_connection()` 支持 `:name` 命名参数):
487
+
488
+ ```python
489
+ conn = SQLiteKit.raw_connection() # 调用方负责 commit / close
464
490
  try:
465
- conn.execute("INSERT INTO item_vectors (id, embedding) VALUES (?, ?)",
466
- (item_id, SQLiteKit.encode_vector(vec)))
491
+ for sql, params in SQLiteKit.build_insert_vectors("item_vectors", vectors,
492
+ ids=row_ids, extra_data=metas):
493
+ conn.executemany(sql, params)
467
494
  conn.commit()
468
495
  finally:
469
496
  conn.close()
470
497
  ```
471
498
 
499
+ > ⚠ 两点约束:① `build_insert_vectors` 输出为 `:name` 风格,**PG / MySQL 的原生 DBAPI 不支持**
500
+ > (psycopg2 / pymysql 需 `%(name)s`),只能经 `transaction()` 的 session 执行;
501
+ > ② SQLite 上**持有未提交写事务**时再调 kit 的**自提交**方法(如 `insert_vectors`)会因另一个连接
502
+ > 拿不到写锁而失败(`OperationalError: database is locked`,约 **6.4s** 后才报错)——
503
+ > 把写入并入**同一个** `transaction()`(如上)即可避免。
504
+
472
505
  > 相似度换算建议:SQLite 仅支持 L2,若向量入库前已**归一化**,则 `cos = 1 − L2²/2`;
473
506
  > PG 可直接用 `distance_type="COSINE"`。
474
507
 
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.2.3"
26
+ __version__ = "0.2.4"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -105,18 +105,30 @@ def to_vector_literal(vector: builtins.list[float]) -> str:
105
105
 
106
106
 
107
107
  def vector_insert_placeholders(
108
- columns: builtins.list[str], id_col: str, column: str
108
+ columns: builtins.list[str],
109
+ id_col: str,
110
+ column: str,
111
+ vector_placeholder: str = ":vec",
109
112
  ) -> str:
110
- """生成向量表 INSERT 的占位符串(供三个数据库 Kit 的 insert_vectors 共用)
113
+ """生成向量表 INSERT 的占位符串(供三个数据库 Kit 的 build_insert_vectors 共用)
111
114
 
112
- 绑定参数名与列名**解耦**:id 列固定为 ``:id``、向量列固定为 ``:vec``、
115
+ 绑定参数名与列名**解耦**:id 列固定为 ``:id``、向量列固定为 ``vector_placeholder``、
113
116
  其余列用 ``:列名``。这样调用方覆写 ``id_col`` / ``column``(自建 DDL 场景)时,
114
117
  参数字典的键不受列名影响。
118
+
119
+ Args:
120
+ columns: 列名列表(按 INSERT 顺序)
121
+ id_col: id 列名 → ``:id``
122
+ column: 向量列名 → ``vector_placeholder``
123
+ vector_placeholder: 向量列的占位符文本(MySQL 传 ``"STRING_TO_VECTOR(:vec)"``)
124
+
125
+ ⚠ 内部符号(不在 ``kit.db.__all__``):调用方请用
126
+ `BaseDbKit.build_insert_vectors`,无需关心绑定名。
115
127
  """
116
128
 
117
129
  def _one(name: str) -> str:
118
130
  if name == column:
119
- return ":vec"
131
+ return vector_placeholder
120
132
  if name == id_col:
121
133
  return ":id"
122
134
  return f":{name}"
@@ -124,6 +136,54 @@ def vector_insert_placeholders(
124
136
  return ", ".join(_one(name) for name in columns)
125
137
 
126
138
 
139
+ # 向量插入的内部保留绑定名(与 vector_insert_placeholders 的映射一一对应)
140
+ _RESERVED_BIND_NAMES = frozenset({"id", "vec"})
141
+
142
+ # 绑定参数名须为标识符:否则 SQLAlchemy 无法解析 `:name`(列名含空格等会得到
143
+ # "A value is required for bind parameter" 这类无法定位的报错)
144
+ _BIND_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
145
+
146
+
147
+ def _check_extra_key(key: str, id_col: str, column: str) -> None:
148
+ """校验 ``extra_data`` 的键可作为绑定参数名(拼装前抛出可操作的 ``ValueError``)
149
+
150
+ 三类问题(均实测):
151
+ ① 保留绑定名 ``id`` / ``vec`` → ``params`` 被覆盖,SQLite 静默写坏数据;
152
+ ② 与 ``id_col`` / ``column`` 重复 → SQLite 接受重复列名并丢弃调用方给的值;
153
+ ③ 不是合法标识符 → 报错无法定位("A value is required for bind parameter")。
154
+ """
155
+ if key in _RESERVED_BIND_NAMES:
156
+ raise ValueError(
157
+ f"extra_data 含保留绑定名 {key!r}:向量插入的内部绑定名固定为 "
158
+ f"'id'(id 列)与 'vec'(向量列),同名业务列会覆盖它们;"
159
+ f"请重命名该键,或改用自定义 SQL(配合 Kit.transaction())"
160
+ )
161
+ if key in (id_col, column):
162
+ raise ValueError(
163
+ f"extra_data 含重复列 {key!r}:该列已由 id_col / column 覆盖"
164
+ )
165
+ if not _BIND_NAME_RE.match(key):
166
+ raise ValueError(
167
+ f"extra_data 的键 {key!r} 不是合法标识符,无法生成绑定参数名"
168
+ )
169
+
170
+
171
+ def normalize_extra_cols(
172
+ extra_cols: dict[str, str] | builtins.list[dict[str, str]] | None,
173
+ ) -> builtins.list[tuple[str, str]]:
174
+ """归一化 ``create_vector_table`` 的额外列定义(两种入参形态)
175
+
176
+ 接受 ``{列名: 方言类型串}``(推荐,与 `create_raw_table` 同形,**键序即 DDL 列序**)
177
+ 或既有 ``[{"name": ..., "type": ...}]``;空值返回空列表。
178
+ 两种形态产出**逐字一致**的 DDL。
179
+ """
180
+ if not extra_cols:
181
+ return []
182
+ if isinstance(extra_cols, dict):
183
+ return [(name, col_type) for name, col_type in extra_cols.items()]
184
+ return [(col["name"], col["type"]) for col in extra_cols]
185
+
186
+
127
187
  def generate_snowflake_ids(count: int) -> builtins.list[int]:
128
188
  """生成 count 个雪花 ID(int64;当前 18 位,上限 19 位)
129
189
 
@@ -190,6 +250,112 @@ class BaseDbKit(ABC):
190
250
  # 数据库类型标识(postgresql / mysql / sqlite),由子类覆写
191
251
  DB_TYPE: str
192
252
 
253
+ # 向量列的占位符(后端覆写点):MySQL 需经 STRING_TO_VECTOR 转换
254
+ VECTOR_PLACEHOLDER: str = ":vec"
255
+
256
+ # ==================== 向量写入的共享构造(只拼装,不执行) ====================
257
+
258
+ @classmethod
259
+ def _vector_param(cls, vector: builtins.list[float]) -> object:
260
+ """向量 → 绑定参数值(**私有钩子**,子类覆写;基类=文本字面量)
261
+
262
+ PostgreSQL(pgvector)与 MySQL(``STRING_TO_VECTOR``)共用文本字面量;
263
+ `SQLiteKit` 覆写为 float32 小端字节。
264
+ """
265
+ return to_vector_literal(vector)
266
+
267
+ @classmethod
268
+ def build_insert_vectors(
269
+ cls,
270
+ table_name: str,
271
+ vectors: builtins.list[builtins.list[float]],
272
+ ids: builtins.list[int] | None = None,
273
+ extra_data: builtins.list[dict[str, object]] | None = None,
274
+ id_col: str = "id",
275
+ column: str = "embedding",
276
+ ) -> builtins.list[tuple[str, builtins.list[dict[str, object]]]]:
277
+ """构造向量表 INSERT 语句(**只拼装、不执行、不管事务**)
278
+
279
+ 供调用方在**自己的事务**内写入向量行(与其它表行同事务提交)::
280
+
281
+ with SQLiteKit.transaction() as session:
282
+ session.execute(text("INSERT INTO docs_item (...) VALUES (...)"), {...})
283
+ for sql, params in SQLiteKit.build_insert_vectors("docs_doc_vectors",
284
+ vectors, ids=[...],
285
+ id_col="rowid"):
286
+ session.execute(text(sql), params)
287
+
288
+ Args:
289
+ table_name: 表名
290
+ vectors: 向量列表,每个向量为 float 列表(也接受二维 numpy 数组)
291
+ ids: 可选的 ID 列表(长度需与 vectors 一致);None 时由 Kit 生成雪花 ID
292
+ (生成值可从返回的参数列表读取)
293
+ extra_data: 额外列数据列表(长度需与 vectors 一致)
294
+ id_col: 主键列名(默认 ``id``,供自建 DDL 的表使用)
295
+ column: 向量列名(默认 ``embedding``,供自建 DDL 的表使用)
296
+
297
+ Returns:
298
+ 按**列集分组**的 ``[(SQL, 参数列表), ...]``:同列集的行合并为一条可
299
+ ``executemany`` 的语句(常规同构 ``extra_data`` 只返回 1 条)。SQL 为
300
+ SQLAlchemy ``text()`` 风格(``:id`` / ``:vec`` / ``:列名``)。
301
+
302
+ Raises:
303
+ ValueError: 入参自洽性校验失败——长度/维度不一致、``extra_data`` 键为保留绑定名
304
+ (``id`` / ``vec``)或与 ``id_col`` / ``column`` 重复、键不是合法标识符、
305
+ ``id_col == column``
306
+
307
+ Note:
308
+ - **无副作用**:不建连接、不接触数据库、不改任何状态;对**不存在的表**也正常返回。
309
+ - **执行载体**:SQLite 可直接在 ``raw_connection()``(DBAPI 支持 ``:name``)或
310
+ ``transaction()`` 上执行;**PG / MySQL 的原生 DBAPI 不支持 ``:name``**
311
+ (psycopg2 / pymysql 需 ``%(name)s``),须经 ``Kit.transaction()`` 的 session 执行。
312
+ - ``insert_vectors`` 的执行语义即"本方法 + 自带事务",两者共用同一份拼装逻辑。
313
+ """
314
+ if id_col == column:
315
+ raise ValueError(f"id_col 与 column 不能同名(当前均为 {id_col!r})")
316
+ # 用 len() 而非真值判断:入参可为 list 或二维 numpy 数组
317
+ if len(vectors) == 0:
318
+ return []
319
+ if ids is None:
320
+ ids = generate_snowflake_ids(len(vectors))
321
+ if len(ids) != len(vectors):
322
+ raise ValueError("ids 长度必须与 vectors 一致")
323
+ if extra_data is not None and len(extra_data) != len(vectors):
324
+ raise ValueError("extra_data 长度必须与 vectors 一致")
325
+
326
+ dims = len(vectors[0])
327
+ if any(len(vec) != dims for vec in vectors):
328
+ raise ValueError(
329
+ f"同一批次的向量长度必须一致(首个向量 {dims} 维,存在其它维度的向量)"
330
+ )
331
+
332
+ # 按「列集合」分组:executemany 要求同一语句的参数列表**键集一致**
333
+ groups: dict[tuple[str, ...], builtins.list[dict[str, object]]] = {}
334
+ for i, vector in enumerate(vectors):
335
+ columns: builtins.list[str] = [id_col, column]
336
+ params: dict[str, object] = {"id": ids[i], "vec": cls._vector_param(vector)}
337
+ if extra_data is not None:
338
+ for key, value in extra_data[i].items():
339
+ _check_extra_key(key, id_col, column)
340
+ columns.append(key)
341
+ params[key] = value
342
+ groups.setdefault(tuple(columns), []).append(params)
343
+
344
+ statements: builtins.list[tuple[str, builtins.list[dict[str, object]]]] = []
345
+ for columns_tuple, params_list in groups.items():
346
+ columns_list = list(columns_tuple)
347
+ placeholders = vector_insert_placeholders(
348
+ columns_list, id_col, column, cls.VECTOR_PLACEHOLDER
349
+ )
350
+ statements.append(
351
+ (
352
+ f"INSERT INTO {table_name} ({', '.join(columns_list)}) "
353
+ f"VALUES ({placeholders})",
354
+ params_list,
355
+ )
356
+ )
357
+ return statements
358
+
193
359
  # ==================== 连接管理 ====================
194
360
 
195
361
  @classmethod
@@ -65,8 +65,7 @@ from kit.config.log_config import get_default_logger
65
65
  from kit.db.base_db_kit import (
66
66
  DEFAULT_DISTANCE_TYPE,
67
67
  BaseDbKit,
68
- generate_snowflake_ids,
69
- to_vector_literal,
68
+ normalize_extra_cols,
70
69
  )
71
70
  from kit.db.engine import MYSQL
72
71
  from kit.db.exceptions import DBError, VectorNotSupportedError
@@ -86,6 +85,9 @@ class MySqlKit(BaseDbKit):
86
85
 
87
86
  DB_TYPE = MYSQL
88
87
 
88
+ # 向量列占位符:本后端 VECTOR 列需经 STRING_TO_VECTOR 转换(覆写基类的 ":vec")
89
+ VECTOR_PLACEHOLDER = "STRING_TO_VECTOR(:vec)"
90
+
89
91
  # 向量能力声明(供调用方 feature-detect,详见 docs/agents/modules/kit-db.md)
90
92
  VECTOR_CAPABILITIES: dict[str, object] = {
91
93
  "store": True, # 可建表 / 可写入
@@ -151,7 +153,7 @@ class MySqlKit(BaseDbKit):
151
153
  dims: int,
152
154
  id_col: str = "id",
153
155
  column: str = "embedding",
154
- extra_cols: list[dict[str, str]] | None = None,
156
+ extra_cols: dict[str, str] | list[dict[str, str]] | None = None,
155
157
  ) -> None:
156
158
  """创建包含向量列的表(MySQL 9.x 原生 VECTOR 类型)
157
159
 
@@ -161,8 +163,10 @@ class MySqlKit(BaseDbKit):
161
163
  id_col: 主键列名(``BIGINT PRIMARY KEY``,**不自增**,id 由调用方提供
162
164
  或由 `insert_vectors` 兜底生成雪花 ID)
163
165
  column: 向量列名(默认 ``embedding``)
164
- extra_cols: 额外列定义,每项为 {"name": str, "type": str},
165
- type 按 MySQL 方言书写,如 [{"name": "label", "type": "VARCHAR(32)"}]
166
+ extra_cols: 额外列定义,**两种形态**(产出 DDL 逐字一致):
167
+ ``{列名: 方言类型串}``(**推荐**,与 `create_raw_table` 同形,键序即列序)
168
+ 或既有 ``[{"name": str, "type": str}]``;
169
+ type 按 MySQL 方言书写,如 ``{"label": "VARCHAR(32)"}``
166
170
 
167
171
  表结构示例::
168
172
 
@@ -187,11 +191,10 @@ class MySqlKit(BaseDbKit):
187
191
 
188
192
  cls._ensure_vector_supported()
189
193
 
190
- extra_defs = ""
191
- if extra_cols:
192
- extra_defs = ", " + ", ".join(
193
- f"{col['name']} {col['type']}" for col in extra_cols
194
- )
194
+ extra_defs = "".join(
195
+ f", {name} {col_type}"
196
+ for name, col_type in normalize_extra_cols(extra_cols)
197
+ )
195
198
 
196
199
  sql = (
197
200
  f"CREATE TABLE IF NOT EXISTS {table_name} ("
@@ -227,63 +230,28 @@ class MySqlKit(BaseDbKit):
227
230
  插入的行数
228
231
 
229
232
  Raises:
230
- ValueError: ids / extra_data 长度与 vectors 不一致,或批内向量长度不一致
233
+ ValueError: 入参自洽性校验失败——ids / extra_data 长度与 vectors 不一致、
234
+ 批内向量长度不一致、``extra_data`` 键为保留绑定名(``id`` / ``vec``)、
235
+ 键与 ``id_col`` / ``column`` 重复、键不是合法标识符、``id_col == column``
231
236
 
232
237
  Note:
233
- 单次事务内批量写入(与 SQLiteKit / PgKit 同语义:整体提交);
234
- ``extra_data=None`` 时走批量提交(pymysql 会合并为多值 INSERT)。
238
+ 单次事务内批量写入(``cls.transaction()``,与 SQLiteKit / PgKit 同语义:整体提交);
239
+ 语句由 `build_insert_vectors` 构造(**单一事实源**,本后端经 ``VECTOR_PLACEHOLDER``
240
+ 输出 ``STRING_TO_VECTOR(:vec)``)。需要把向量行写入**调用方自己的事务**时,
241
+ 请改用 `build_insert_vectors` + `transaction()`。
235
242
  """
236
- # 用 len() 而非真值判断:入参可为 list 或二维 numpy 数组
237
- if len(vectors) == 0:
238
- return 0
239
- if ids is None:
240
- ids = generate_snowflake_ids(len(vectors))
241
- if len(ids) != len(vectors):
242
- raise ValueError("ids 长度必须与 vectors 一致")
243
- if extra_data is not None and len(extra_data) != len(vectors):
244
- raise ValueError("extra_data 长度必须与 vectors 一致")
245
-
246
- dims = len(vectors[0])
247
- if any(len(vec) != dims for vec in vectors):
248
- raise ValueError(
249
- f"同一批次的向量长度必须一致(首个向量 {dims} 维,存在其它维度的向量)"
250
- )
251
-
252
- def _placeholder(name: str) -> str:
253
- """列名 → 占位符:向量列需经 STRING_TO_VECTOR 转换(本后端特有)"""
254
- if name == column:
255
- return "STRING_TO_VECTOR(:vec)"
256
- if name == id_col:
257
- return ":id"
258
- return f":{name}"
259
-
260
- # 按「列集合」分组:键集相同的行才能共用同一条批量语句(executemany)
261
- groups: dict[tuple[str, ...], list[dict[str, object]]] = {}
262
- for i, vector in enumerate(vectors):
263
- columns: list[str] = [id_col, column]
264
- params: dict[str, object] = {
265
- "id": ids[i],
266
- "vec": to_vector_literal(vector),
267
- }
268
-
269
- if extra_data is not None:
270
- for key, value in extra_data[i].items():
271
- columns.append(key)
272
- params[key] = value
273
-
274
- groups.setdefault(tuple(columns), []).append(params)
275
-
276
243
  with cls.transaction() as session:
277
- for columns, params_list in groups.items():
278
- placeholders = ", ".join(_placeholder(name) for name in columns)
279
- sql = (
280
- f"INSERT INTO {table_name} ({', '.join(columns)}) "
281
- f"VALUES ({placeholders})"
282
- )
283
- session.execute(text(sql), params_list)
284
-
285
- logger.info(f"✅ 插入 {len(vectors)} 条向量到 {table_name}")
286
- return len(vectors)
244
+ for sql, params in cls.build_insert_vectors(
245
+ table_name,
246
+ vectors,
247
+ ids=ids,
248
+ extra_data=extra_data,
249
+ id_col=id_col,
250
+ column=column,
251
+ ):
252
+ session.execute(text(sql), params)
253
+ logger.info(f"✅ 插入 {len(vectors)} 条向量到 {table_name}")
254
+ return len(vectors)
287
255
 
288
256
  @classmethod
289
257
  def search_vectors(
@@ -67,9 +67,8 @@ from kit.config.log_config import get_default_logger
67
67
  from kit.db.base_db_kit import (
68
68
  DEFAULT_DISTANCE_TYPE,
69
69
  BaseDbKit,
70
- generate_snowflake_ids,
70
+ normalize_extra_cols,
71
71
  to_vector_literal,
72
- vector_insert_placeholders,
73
72
  )
74
73
  from kit.db.engine import POSTGRESQL, get_engine
75
74
  from kit.db.exceptions import DBError
@@ -177,7 +176,7 @@ class PgKit(BaseDbKit):
177
176
  dims: int,
178
177
  id_col: str = "id",
179
178
  column: str = "embedding",
180
- extra_cols: list[dict[str, str]] | None = None,
179
+ extra_cols: dict[str, str] | list[dict[str, str]] | None = None,
181
180
  ) -> None:
182
181
  """创建包含向量列的表(pgvector)
183
182
 
@@ -187,8 +186,10 @@ class PgKit(BaseDbKit):
187
186
  id_col: 主键列名(``BIGINT PRIMARY KEY``,**不自增**,id 由调用方提供
188
187
  或由 `insert_vectors` 兜底生成雪花 ID)
189
188
  column: 向量列名(默认 ``embedding``)
190
- extra_cols: 额外列定义,每项为 {"name": str, "type": str},
191
- type 按 PostgreSQL 方言书写,如 [{"name": "label", "type": "TEXT"}]
189
+ extra_cols: 额外列定义,**两种形态**(产出 DDL 逐字一致):
190
+ ``{列名: 方言类型串}``(**推荐**,与 `create_raw_table` 同形,键序即列序)
191
+ 或既有 ``[{"name": str, "type": str}]``;
192
+ type 按 PostgreSQL 方言书写,如 ``{"label": "TEXT"}``
192
193
 
193
194
  内部自动执行 ``CREATE EXTENSION IF NOT EXISTS vector``(需相应权限)。
194
195
 
@@ -214,11 +215,10 @@ class PgKit(BaseDbKit):
214
215
 
215
216
  cls._ensure_vector_extension()
216
217
 
217
- extra_defs = ""
218
- if extra_cols:
219
- extra_defs = ", " + ", ".join(
220
- f"{col['name']} {col['type']}" for col in extra_cols
221
- )
218
+ extra_defs = "".join(
219
+ f", {name} {col_type}"
220
+ for name, col_type in normalize_extra_cols(extra_cols)
221
+ )
222
222
 
223
223
  sql = (
224
224
  f"CREATE TABLE IF NOT EXISTS {table_name} ("
@@ -271,55 +271,27 @@ class PgKit(BaseDbKit):
271
271
  插入的行数
272
272
 
273
273
  Raises:
274
- ValueError: ids / extra_data 长度与 vectors 不一致,或批内向量长度不一致
274
+ ValueError: 入参自洽性校验失败——ids / extra_data 长度与 vectors 不一致、
275
+ 批内向量长度不一致、``extra_data`` 键为保留绑定名(``id`` / ``vec``)、
276
+ 键与 ``id_col`` / ``column`` 重复、键不是合法标识符、``id_col == column``
275
277
 
276
278
  Note:
277
- 单次事务内批量写入(与 SQLiteKit / MySqlKit 同语义:整体提交);
278
- ``extra_data=None`` 时走驱动级批量提交(executemany)。
279
+ 单次事务内批量写入(``cls.transaction()``,与 SQLiteKit / MySqlKit 同语义:整体提交);
280
+ 语句由 `build_insert_vectors` 构造(**单一事实源**)。需要把向量行写入**调用方自己的事务**时,
281
+ 请改用 `build_insert_vectors` + `transaction()`。
279
282
  """
280
- # 用 len() 而非真值判断:入参可为 list 或二维 numpy 数组
281
- if len(vectors) == 0:
282
- return 0
283
- if ids is None:
284
- ids = generate_snowflake_ids(len(vectors))
285
- if len(ids) != len(vectors):
286
- raise ValueError("ids 长度必须与 vectors 一致")
287
- if extra_data is not None and len(extra_data) != len(vectors):
288
- raise ValueError("extra_data 长度必须与 vectors 一致")
289
-
290
- dims = len(vectors[0])
291
- if any(len(vec) != dims for vec in vectors):
292
- raise ValueError(
293
- f"同一批次的向量长度必须一致(首个向量 {dims} 维,存在其它维度的向量)"
294
- )
295
-
296
- # 按「列集合」分组:键集相同的行才能共用同一条批量语句(executemany)
297
- groups: dict[tuple[str, ...], list[dict[str, object]]] = {}
298
- for i, vector in enumerate(vectors):
299
- columns: list[str] = [id_col, column]
300
- params: dict[str, object] = {
301
- "id": ids[i],
302
- "vec": to_vector_literal(vector),
303
- }
304
-
305
- if extra_data is not None:
306
- for key, value in extra_data[i].items():
307
- columns.append(key)
308
- params[key] = value
309
-
310
- groups.setdefault(tuple(columns), []).append(params)
311
-
312
283
  with cls.transaction() as session:
313
- for columns, params_list in groups.items():
314
- placeholders = vector_insert_placeholders(list(columns), id_col, column)
315
- sql = (
316
- f"INSERT INTO {table_name} ({', '.join(columns)}) "
317
- f"VALUES ({placeholders})"
318
- )
319
- session.execute(text(sql), params_list)
320
-
321
- logger.info(f"✅ 插入 {len(vectors)} 条向量到 {table_name}")
322
- return len(vectors)
284
+ for sql, params in cls.build_insert_vectors(
285
+ table_name,
286
+ vectors,
287
+ ids=ids,
288
+ extra_data=extra_data,
289
+ id_col=id_col,
290
+ column=column,
291
+ ):
292
+ session.execute(text(sql), params)
293
+ logger.info(f"✅ 插入 {len(vectors)} 条向量到 {table_name}")
294
+ return len(vectors)
323
295
 
324
296
  @classmethod
325
297
  def search_vectors(
@@ -59,8 +59,7 @@ from kit.config.log_config import get_default_logger
59
59
  from kit.db.base_db_kit import (
60
60
  DEFAULT_DISTANCE_TYPE,
61
61
  BaseDbKit,
62
- generate_snowflake_ids,
63
- vector_insert_placeholders,
62
+ normalize_extra_cols,
64
63
  )
65
64
  from kit.db.engine import SQLITE, get_engine
66
65
  from kit.db.exceptions import DBError
@@ -124,6 +123,11 @@ class SQLiteKit(BaseDbKit):
124
123
 
125
124
  # ==================== 向量操作 ====================
126
125
 
126
+ @classmethod
127
+ def _vector_param(cls, vector: list[float]) -> object:
128
+ """向量 → BLOB(小端 float32);覆写基类的文本字面量实现"""
129
+ return cls.encode_vector(vector)
130
+
127
131
  @classmethod
128
132
  def encode_vector(cls, vector: list[float]) -> bytes:
129
133
  """把向量编码为 sqlite-vector 使用的 float32 **小端** BLOB(4 字节/维)
@@ -179,7 +183,7 @@ class SQLiteKit(BaseDbKit):
179
183
  dims: int,
180
184
  id_col: str = "id",
181
185
  column: str = "embedding",
182
- extra_cols: list[dict[str, str]] | None = None,
186
+ extra_cols: dict[str, str] | list[dict[str, str]] | None = None,
183
187
  ) -> None:
184
188
  """创建包含向量列的表
185
189
 
@@ -190,8 +194,10 @@ class SQLiteKit(BaseDbKit):
190
194
  等价于另两库的 ``BIGINT``;id 由调用方提供或由 `insert_vectors`
191
195
  兜底生成雪花 ID)。须为 rowid 别名,见 `search_vectors` 的 Note
192
196
  column: 向量列名(默认 ``embedding``)
193
- extra_cols: 额外列定义,每项为 {"name": str, "type": str},
194
- 如 [{"name": "label", "type": "TEXT"}]
197
+ extra_cols: 额外列定义,**两种形态**(产出 DDL 逐字一致):
198
+ ``{列名: 方言类型串}``(**推荐**,与 `create_raw_table` 同形,键序即列序)
199
+ 或既有 ``[{"name": str, "type": str}]``;
200
+ 如 ``{"label": "TEXT"}``
195
201
 
196
202
  表结构示例::
197
203
 
@@ -212,11 +218,10 @@ class SQLiteKit(BaseDbKit):
212
218
  if dims < 1:
213
219
  raise ValueError(f"dims 必须为正整数,当前为 {dims}")
214
220
 
215
- extra_defs = ""
216
- if extra_cols:
217
- extra_defs = ", " + ", ".join(
218
- f"{c['name']} {c['type']}" for c in extra_cols
219
- )
221
+ extra_defs = "".join(
222
+ f", {name} {col_type}"
223
+ for name, col_type in normalize_extra_cols(extra_cols)
224
+ )
220
225
 
221
226
  sql = f"""
222
227
  CREATE TABLE IF NOT EXISTS {table_name} (
@@ -253,56 +258,29 @@ class SQLiteKit(BaseDbKit):
253
258
  插入的行数
254
259
 
255
260
  Raises:
256
- ValueError: ids / extra_data 长度与 vectors 不一致,或批内向量长度不一致
261
+ ValueError: 入参自洽性校验失败——ids / extra_data 长度与 vectors 不一致、
262
+ 批内向量长度不一致、``extra_data`` 键为保留绑定名(``id`` / ``vec``)、
263
+ 键与 ``id_col`` / ``column`` 重复、键不是合法标识符、``id_col == column``
257
264
 
258
265
  Note:
259
266
  - `ids=None` 时**不使用 SQLite rowid 自增**,改由 Kit 生成雪花 ID
260
267
  (64 位,18~19 位数字),与 PgKit / MySqlKit 语义一致。
261
- - 单次事务内批量写入;``extra_data=None`` 时走驱动级批量提交(executemany)。
268
+ - 单次事务内批量写入(``cls.transaction()``);语句由 `build_insert_vectors`
269
+ 构造(**单一事实源**)。需要把向量行写入**调用方自己的事务**时,请改用
270
+ `build_insert_vectors` + `transaction()`,或 `raw_connection()`(见文档 cookbook)。
262
271
  """
263
- # 用 len() 而非真值判断:入参可为 list 或二维 numpy 数组
264
- if len(vectors) == 0:
265
- return 0
266
- if ids is None:
267
- ids = generate_snowflake_ids(len(vectors))
268
- if len(ids) != len(vectors):
269
- raise ValueError("ids 长度必须与 vectors 一致")
270
- if extra_data is not None and len(extra_data) != len(vectors):
271
- raise ValueError("extra_data 长度必须与 vectors 一致")
272
-
273
- dims = len(vectors[0])
274
- if any(len(vec) != dims for vec in vectors):
275
- raise ValueError(
276
- f"同一批次的向量长度必须一致(首个向量 {dims} 维,存在其它维度的向量)"
277
- )
278
-
279
- engine = get_engine(cls.DB_TYPE)
280
-
281
- # 按「列集合」分组:键集相同的行才能共用同一条批量语句(executemany)
282
- groups: dict[tuple[str, ...], list[dict[str, object]]] = {}
283
- for i, vec in enumerate(vectors):
284
- vec_bytes = cls.encode_vector(vec)
285
- columns: list[str] = [id_col, column]
286
- params: dict[str, object] = {"id": ids[i], "vec": vec_bytes}
287
-
288
- if extra_data is not None:
289
- for key, value in extra_data[i].items():
290
- columns.append(key)
291
- params[key] = value
292
-
293
- groups.setdefault(tuple(columns), []).append(params)
294
-
295
- with engine.connect() as conn:
296
- for columns, params_list in groups.items():
297
- placeholders = vector_insert_placeholders(list(columns), id_col, column)
298
- sql = (
299
- f"INSERT INTO {table_name} ({', '.join(columns)}) "
300
- f"VALUES ({placeholders})"
301
- )
302
- conn.execute(text(sql), params_list)
303
- conn.commit()
304
- logger.info(f"✅ 插入 {len(vectors)} 条向量到 {table_name}")
305
- return len(vectors)
272
+ with cls.transaction() as session:
273
+ for sql, params in cls.build_insert_vectors(
274
+ table_name,
275
+ vectors,
276
+ ids=ids,
277
+ extra_data=extra_data,
278
+ id_col=id_col,
279
+ column=column,
280
+ ):
281
+ session.execute(text(sql), params)
282
+ logger.info(f"✅ 插入 {len(vectors)} 条向量到 {table_name}")
283
+ return len(vectors)
306
284
 
307
285
  @classmethod
308
286
  def _vector_dims(cls, table_name: str, column: str, conn: Connection) -> int | None:
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