@an5/adapters 0.2.3 → 0.2.5

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 (96) hide show
  1. package/README.md +305 -283
  2. package/{typescript → dist}/browser.d.ts +1 -1
  3. package/{typescript → dist}/browser.js +2 -1
  4. package/{typescript → dist}/index.d.ts +2 -0
  5. package/{typescript → dist}/index.js +6 -1
  6. package/dotnet/An5Adapter.cs +275 -275
  7. package/dotnet/Base/Core.cs +82 -82
  8. package/dotnet/Mssql/MssqlEngine.cs +144 -144
  9. package/dotnet/Postgres/PostgresEngine.cs +121 -121
  10. package/package.json +21 -11
  11. package/python/an5_adapter.py +97 -97
  12. package/python/base/__init__.py +3 -3
  13. package/python/base/dialects.py +8 -8
  14. package/python/base/metadata.py +10 -10
  15. package/python/base/sql.py +121 -121
  16. package/python/mssql/__init__.py +1 -1
  17. package/python/mssql/provider.py +62 -62
  18. package/python/postgres/__init__.py +1 -1
  19. package/python/postgres/provider.py +15 -15
  20. package/python/table_client.py +216 -216
  21. package/typescript/src/an5Adapter.ts +505 -505
  22. package/typescript/src/base/index.ts +3 -3
  23. package/typescript/src/base/metadata.ts +28 -28
  24. package/typescript/src/base/sql.ts +98 -98
  25. package/typescript/src/base/types.ts +24 -24
  26. package/typescript/src/browser.ts +6 -6
  27. package/typescript/src/config.ts +95 -95
  28. package/typescript/src/googlesheets/adapter.ts +259 -259
  29. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  30. package/typescript/src/googlesheets/config.ts +47 -47
  31. package/typescript/src/googlesheets/helpers.ts +143 -143
  32. package/typescript/src/googlesheets/index.ts +3 -3
  33. package/typescript/src/googlesheets/parseConnectionString.ts +46 -46
  34. package/typescript/src/googlesheets/retry.ts +126 -126
  35. package/typescript/src/googlesheets/sqlExecutor.ts +301 -301
  36. package/typescript/src/googlesheets/tableClient.ts +455 -455
  37. package/typescript/src/googlesheets/types.ts +6 -6
  38. package/typescript/src/index.ts +28 -19
  39. package/typescript/src/mssql/engine.ts +132 -132
  40. package/typescript/src/mssql/index.ts +1 -1
  41. package/typescript/src/mysql/engine.ts +80 -80
  42. package/typescript/src/mysql/index.ts +1 -1
  43. package/typescript/src/postgres/engine.ts +87 -87
  44. package/typescript/src/postgres/index.ts +1 -1
  45. package/typescript/src/sqlite/engine.ts +55 -55
  46. package/typescript/src/sqlite/index.ts +1 -1
  47. /package/{typescript → dist}/an5Adapter.d.ts +0 -0
  48. /package/{typescript → dist}/an5Adapter.js +0 -0
  49. /package/{typescript → dist}/base/index.d.ts +0 -0
  50. /package/{typescript → dist}/base/index.js +0 -0
  51. /package/{typescript → dist}/base/metadata.d.ts +0 -0
  52. /package/{typescript → dist}/base/metadata.js +0 -0
  53. /package/{typescript → dist}/base/sql.d.ts +0 -0
  54. /package/{typescript → dist}/base/sql.js +0 -0
  55. /package/{typescript → dist}/base/types.d.ts +0 -0
  56. /package/{typescript → dist}/base/types.js +0 -0
  57. /package/{typescript → dist}/base/uuid.d.ts +0 -0
  58. /package/{typescript → dist}/base/uuid.js +0 -0
  59. /package/{typescript → dist}/config.d.ts +0 -0
  60. /package/{typescript → dist}/config.js +0 -0
  61. /package/{typescript → dist}/googlesheets/adapter.d.ts +0 -0
  62. /package/{typescript → dist}/googlesheets/adapter.js +0 -0
  63. /package/{typescript → dist}/googlesheets/an5SheetsAdapter.d.ts +0 -0
  64. /package/{typescript → dist}/googlesheets/an5SheetsAdapter.js +0 -0
  65. /package/{typescript → dist}/googlesheets/config.d.ts +0 -0
  66. /package/{typescript → dist}/googlesheets/config.js +0 -0
  67. /package/{typescript → dist}/googlesheets/helpers.d.ts +0 -0
  68. /package/{typescript → dist}/googlesheets/helpers.js +0 -0
  69. /package/{typescript → dist}/googlesheets/index.d.ts +0 -0
  70. /package/{typescript → dist}/googlesheets/index.js +0 -0
  71. /package/{typescript → dist}/googlesheets/parseConnectionString.d.ts +0 -0
  72. /package/{typescript → dist}/googlesheets/parseConnectionString.js +0 -0
  73. /package/{typescript → dist}/googlesheets/retry.d.ts +0 -0
  74. /package/{typescript → dist}/googlesheets/retry.js +0 -0
  75. /package/{typescript → dist}/googlesheets/sqlExecutor.d.ts +0 -0
  76. /package/{typescript → dist}/googlesheets/sqlExecutor.js +0 -0
  77. /package/{typescript → dist}/googlesheets/tableClient.d.ts +0 -0
  78. /package/{typescript → dist}/googlesheets/tableClient.js +0 -0
  79. /package/{typescript → dist}/googlesheets/types.d.ts +0 -0
  80. /package/{typescript → dist}/googlesheets/types.js +0 -0
  81. /package/{typescript → dist}/mssql/engine.d.ts +0 -0
  82. /package/{typescript → dist}/mssql/engine.js +0 -0
  83. /package/{typescript → dist}/mssql/index.d.ts +0 -0
  84. /package/{typescript → dist}/mssql/index.js +0 -0
  85. /package/{typescript → dist}/mysql/engine.d.ts +0 -0
  86. /package/{typescript → dist}/mysql/engine.js +0 -0
  87. /package/{typescript → dist}/mysql/index.d.ts +0 -0
  88. /package/{typescript → dist}/mysql/index.js +0 -0
  89. /package/{typescript → dist}/postgres/engine.d.ts +0 -0
  90. /package/{typescript → dist}/postgres/engine.js +0 -0
  91. /package/{typescript → dist}/postgres/index.d.ts +0 -0
  92. /package/{typescript → dist}/postgres/index.js +0 -0
  93. /package/{typescript → dist}/sqlite/engine.d.ts +0 -0
  94. /package/{typescript → dist}/sqlite/engine.js +0 -0
  95. /package/{typescript → dist}/sqlite/index.d.ts +0 -0
  96. /package/{typescript → dist}/sqlite/index.js +0 -0
@@ -1,216 +1,216 @@
1
- import json
2
- import uuid
3
- from typing import Dict, List, Optional
4
- try:
5
- from .base import DIALECT_MSSQL, DIALECT_POSTGRES, model_fields, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table
6
- except ImportError:
7
- from base import DIALECT_MSSQL, DIALECT_POSTGRES, model_fields, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table
8
-
9
- # ─── Table Client ───────────────────────────────────────────────────────────────────
10
-
11
- class AdapterTableClient:
12
- def __init__(self, adapter: An5Adapter, model_name: str):
13
- self._adapter = adapter
14
- self._model = model_name
15
- self._dialect = adapter._dialect
16
-
17
- @property
18
- def _table(self) -> str:
19
- return _resolve_table(self._model)
20
-
21
- @property
22
- def _table_sql(self) -> str:
23
- return _quote_table(self._table, self._dialect)
24
-
25
- @property
26
- def _nolock(self) -> str:
27
- return "" if self._dialect == DIALECT_POSTGRES else " WITH (NOLOCK)"
28
-
29
- @property
30
- def _fields(self) -> List[Dict]:
31
- fields = model_fields.get(self._model, [])
32
- if isinstance(fields, dict):
33
- raise TypeError(
34
- "AN5 Python metadata is out of date. Regenerate with @an5/orm >= 1.0.4 "
35
- "so MODEL_FIELDS uses a list of field objects."
36
- )
37
- return fields
38
-
39
- def _pagination(self, take: Optional[int], skip: int, order_sql: str) -> str:
40
- if take is None:
41
- return ""
42
- if self._dialect == DIALECT_POSTGRES:
43
- return f" LIMIT {take} OFFSET {skip}"
44
- order_prefix = "" if order_sql else " ORDER BY (SELECT NULL)"
45
- return f"{order_prefix} OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY"
46
-
47
- def find_many(self, where=None, order_by=None, skip: int = 0, take: Optional[int] = None, select=None) -> List[Dict]:
48
- params: Dict = {}
49
- where_sql = _parse_where(self._model, where, params, self._dialect)
50
- order_sql = _build_order_by(order_by, self._dialect)
51
-
52
- query = f"SELECT * FROM {self._table_sql}{self._nolock}"
53
- if where_sql:
54
- query += f" WHERE {where_sql}"
55
- if order_sql:
56
- query += f" {order_sql}"
57
- query += self._pagination(take, skip, order_sql)
58
- return self._adapter.exec(query, list(params.values()))
59
-
60
- def find_first(self, where=None, order_by=None, select=None) -> Optional[Dict]:
61
- rows = self.find_many(where=where, order_by=order_by, take=1, select=select)
62
- return rows[0] if rows else None
63
-
64
- def find_unique(self, where: Dict) -> Optional[Dict]:
65
- return self.find_first(where=where)
66
-
67
- def count(self, where=None) -> int:
68
- params: Dict = {}
69
- where_sql = _parse_where(self._model, where, params, self._dialect)
70
- query = f"SELECT COUNT(*) AS cnt FROM {self._table_sql}{self._nolock}"
71
- if where_sql:
72
- query += f" WHERE {where_sql}"
73
- rows = self._adapter.exec(query, list(params.values()))
74
- return int(rows[0]["cnt"]) if rows else 0
75
-
76
- def create(self, data: Dict) -> Dict:
77
- id_field = next((f for f in self._fields if f.get("isId")), None)
78
- if id_field and id_field["name"] not in data:
79
- data = {**data, id_field["name"]: str(uuid.uuid4())}
80
-
81
- cols = [k for k, v in data.items() if v is not None]
82
- values = [data[c] for c in cols]
83
- placeholders = ", ".join(["?" if self._dialect == DIALECT_MSSQL else "%s"] * len(cols))
84
- col_list = ", ".join(_quote(c, self._dialect) for c in cols)
85
- query = f"INSERT INTO {self._table_sql} ({col_list}) VALUES ({placeholders})"
86
- self._adapter.execute(query, values)
87
-
88
- if id_field:
89
- return self.find_first(where={id_field["name"]: data[id_field["name"]]}) or data
90
- return data
91
-
92
- def create_many(self, data: List[Dict], skip_duplicates: bool = False) -> Dict:
93
- count = 0
94
- for row in data:
95
- try:
96
- self.create(row)
97
- count += 1
98
- except Exception:
99
- if not skip_duplicates:
100
- raise
101
- return {"count": count}
102
-
103
- def update(self, where: Dict, data: Dict) -> Optional[Dict]:
104
- params: Dict = {}
105
- where_sql = _parse_where(self._model, where, params, self._dialect, "w_")
106
- set_parts: List[str] = []
107
- set_values: List = []
108
- placeholder = "?" if self._dialect == DIALECT_MSSQL else "%s"
109
- for col, val in data.items():
110
- if val is not None:
111
- set_parts.append(f"{_quote(col, self._dialect)} = {placeholder}")
112
- set_values.append(val)
113
-
114
- all_values = set_values + list(params.values())
115
- query = f"UPDATE {self._table_sql} SET {', '.join(set_parts)}"
116
- if where_sql:
117
- query += f" WHERE {where_sql}"
118
- self._adapter.execute(query, all_values)
119
- return self.find_first(where=where)
120
-
121
- def update_many(self, where: Optional[Dict], data: Dict) -> Dict:
122
- params: Dict = {}
123
- where_sql = _parse_where(self._model, where, params, self._dialect, "w_")
124
- set_parts: List[str] = []
125
- set_values: List = []
126
- placeholder = "?" if self._dialect == DIALECT_MSSQL else "%s"
127
- for col, val in data.items():
128
- set_parts.append(f"{_quote(col, self._dialect)} = {placeholder}")
129
- set_values.append(val)
130
- all_values = set_values + list(params.values())
131
- query = f"UPDATE {self._table_sql} SET {', '.join(set_parts)}"
132
- if where_sql:
133
- query += f" WHERE {where_sql}"
134
- count = self._adapter.execute(query, all_values)
135
- return {"count": count}
136
-
137
- def delete(self, where: Dict) -> Optional[Dict]:
138
- existing = self.find_first(where=where)
139
- params: Dict = {}
140
- where_sql = _parse_where(self._model, where, params, self._dialect)
141
- query = f"DELETE FROM {self._table_sql} WHERE {where_sql}"
142
- self._adapter.execute(query, list(params.values()))
143
- return existing
144
-
145
- def delete_many(self, where: Optional[Dict] = None) -> Dict:
146
- params: Dict = {}
147
- where_sql = _parse_where(self._model, where, params, self._dialect)
148
- query = f"DELETE FROM {self._table_sql}"
149
- if where_sql:
150
- query += f" WHERE {where_sql}"
151
- count = self._adapter.execute(query, list(params.values()))
152
- return {"count": count}
153
-
154
- def upsert(self, where: Dict, create: Dict, update: Dict) -> Dict:
155
- existing = self.find_first(where=where)
156
- if existing:
157
- return self.update(where=where, data=update) or existing
158
- return self.create(data=create)
159
-
160
- def aggregate(self, where=None, _count=None, _sum=None, _avg=None, _min=None, _max=None) -> Dict:
161
- params: Dict = {}
162
- where_sql = _parse_where(self._model, where, params, self._dialect)
163
- aggs: List[str] = []
164
- if _count:
165
- aggs.append("COUNT(*) AS _count")
166
- if _sum:
167
- for f in (_sum if isinstance(_sum, list) else [_sum]):
168
- aggs.append(f"SUM({_quote(f, self._dialect)}) AS _sum_{f}")
169
- if _avg:
170
- for f in (_avg if isinstance(_avg, list) else [_avg]):
171
- aggs.append(f"AVG({_quote(f, self._dialect)}) AS _avg_{f}")
172
- if _min:
173
- for f in (_min if isinstance(_min, list) else [_min]):
174
- aggs.append(f"MIN({_quote(f, self._dialect)}) AS _min_{f}")
175
- if _max:
176
- for f in (_max if isinstance(_max, list) else [_max]):
177
- aggs.append(f"MAX({_quote(f, self._dialect)}) AS _max_{f}")
178
- if not aggs:
179
- aggs = ["COUNT(*) AS _count"]
180
-
181
- query = f"SELECT {', '.join(aggs)} FROM {self._table_sql}"
182
- if where_sql:
183
- query += f" WHERE {where_sql}"
184
- rows = self._adapter.exec(query, list(params.values()))
185
- return rows[0] if rows else {}
186
-
187
- def vector_search(self, vector: List[float], take: int = 10, where=None, vector_field: str = "embedding", distance_metric: str = "cosine") -> List[Dict]:
188
- rows = self.find_many(where=where)
189
- scored = []
190
- for row in rows:
191
- raw = row.get(vector_field)
192
- if raw is None:
193
- continue
194
- try:
195
- vec = json.loads(raw) if isinstance(raw, str) else list(raw)
196
- except Exception:
197
- continue
198
- if not vec or len(vec) != len(vector):
199
- continue
200
-
201
- dot = sum(a * b for a, b in zip(vector, vec))
202
- m1 = sum(a ** 2 for a in vector) ** 0.5
203
- m2 = sum(b ** 2 for b in vec) ** 0.5
204
- cosine = dot / (m1 * m2) if m1 and m2 else 0.0
205
-
206
- if distance_metric == "cosine":
207
- dist = 1.0 - cosine
208
- elif distance_metric == "dot":
209
- dist = -dot
210
- else:
211
- dist = sum((a - b) ** 2 for a, b in zip(vector, vec)) ** 0.5
212
-
213
- scored.append((row, dist))
214
-
215
- scored.sort(key=lambda x: x[1])
216
- return [{**row, "distance": dist} for row, dist in scored[:take]]
1
+ import json
2
+ import uuid
3
+ from typing import Dict, List, Optional
4
+ try:
5
+ from .base import DIALECT_MSSQL, DIALECT_POSTGRES, model_fields, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table
6
+ except ImportError:
7
+ from base import DIALECT_MSSQL, DIALECT_POSTGRES, model_fields, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table
8
+
9
+ # ─── Table Client ───────────────────────────────────────────────────────────────────
10
+
11
+ class AdapterTableClient:
12
+ def __init__(self, adapter: An5Adapter, model_name: str):
13
+ self._adapter = adapter
14
+ self._model = model_name
15
+ self._dialect = adapter._dialect
16
+
17
+ @property
18
+ def _table(self) -> str:
19
+ return _resolve_table(self._model)
20
+
21
+ @property
22
+ def _table_sql(self) -> str:
23
+ return _quote_table(self._table, self._dialect)
24
+
25
+ @property
26
+ def _nolock(self) -> str:
27
+ return "" if self._dialect == DIALECT_POSTGRES else " WITH (NOLOCK)"
28
+
29
+ @property
30
+ def _fields(self) -> List[Dict]:
31
+ fields = model_fields.get(self._model, [])
32
+ if isinstance(fields, dict):
33
+ raise TypeError(
34
+ "AN5 Python metadata is out of date. Regenerate with @an5/orm >= 1.0.4 "
35
+ "so MODEL_FIELDS uses a list of field objects."
36
+ )
37
+ return fields
38
+
39
+ def _pagination(self, take: Optional[int], skip: int, order_sql: str) -> str:
40
+ if take is None:
41
+ return ""
42
+ if self._dialect == DIALECT_POSTGRES:
43
+ return f" LIMIT {take} OFFSET {skip}"
44
+ order_prefix = "" if order_sql else " ORDER BY (SELECT NULL)"
45
+ return f"{order_prefix} OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY"
46
+
47
+ def find_many(self, where=None, order_by=None, skip: int = 0, take: Optional[int] = None, select=None) -> List[Dict]:
48
+ params: Dict = {}
49
+ where_sql = _parse_where(self._model, where, params, self._dialect)
50
+ order_sql = _build_order_by(order_by, self._dialect)
51
+
52
+ query = f"SELECT * FROM {self._table_sql}{self._nolock}"
53
+ if where_sql:
54
+ query += f" WHERE {where_sql}"
55
+ if order_sql:
56
+ query += f" {order_sql}"
57
+ query += self._pagination(take, skip, order_sql)
58
+ return self._adapter.exec(query, list(params.values()))
59
+
60
+ def find_first(self, where=None, order_by=None, select=None) -> Optional[Dict]:
61
+ rows = self.find_many(where=where, order_by=order_by, take=1, select=select)
62
+ return rows[0] if rows else None
63
+
64
+ def find_unique(self, where: Dict) -> Optional[Dict]:
65
+ return self.find_first(where=where)
66
+
67
+ def count(self, where=None) -> int:
68
+ params: Dict = {}
69
+ where_sql = _parse_where(self._model, where, params, self._dialect)
70
+ query = f"SELECT COUNT(*) AS cnt FROM {self._table_sql}{self._nolock}"
71
+ if where_sql:
72
+ query += f" WHERE {where_sql}"
73
+ rows = self._adapter.exec(query, list(params.values()))
74
+ return int(rows[0]["cnt"]) if rows else 0
75
+
76
+ def create(self, data: Dict) -> Dict:
77
+ id_field = next((f for f in self._fields if f.get("isId")), None)
78
+ if id_field and id_field["name"] not in data:
79
+ data = {**data, id_field["name"]: str(uuid.uuid4())}
80
+
81
+ cols = [k for k, v in data.items() if v is not None]
82
+ values = [data[c] for c in cols]
83
+ placeholders = ", ".join(["?" if self._dialect == DIALECT_MSSQL else "%s"] * len(cols))
84
+ col_list = ", ".join(_quote(c, self._dialect) for c in cols)
85
+ query = f"INSERT INTO {self._table_sql} ({col_list}) VALUES ({placeholders})"
86
+ self._adapter.execute(query, values)
87
+
88
+ if id_field:
89
+ return self.find_first(where={id_field["name"]: data[id_field["name"]]}) or data
90
+ return data
91
+
92
+ def create_many(self, data: List[Dict], skip_duplicates: bool = False) -> Dict:
93
+ count = 0
94
+ for row in data:
95
+ try:
96
+ self.create(row)
97
+ count += 1
98
+ except Exception:
99
+ if not skip_duplicates:
100
+ raise
101
+ return {"count": count}
102
+
103
+ def update(self, where: Dict, data: Dict) -> Optional[Dict]:
104
+ params: Dict = {}
105
+ where_sql = _parse_where(self._model, where, params, self._dialect, "w_")
106
+ set_parts: List[str] = []
107
+ set_values: List = []
108
+ placeholder = "?" if self._dialect == DIALECT_MSSQL else "%s"
109
+ for col, val in data.items():
110
+ if val is not None:
111
+ set_parts.append(f"{_quote(col, self._dialect)} = {placeholder}")
112
+ set_values.append(val)
113
+
114
+ all_values = set_values + list(params.values())
115
+ query = f"UPDATE {self._table_sql} SET {', '.join(set_parts)}"
116
+ if where_sql:
117
+ query += f" WHERE {where_sql}"
118
+ self._adapter.execute(query, all_values)
119
+ return self.find_first(where=where)
120
+
121
+ def update_many(self, where: Optional[Dict], data: Dict) -> Dict:
122
+ params: Dict = {}
123
+ where_sql = _parse_where(self._model, where, params, self._dialect, "w_")
124
+ set_parts: List[str] = []
125
+ set_values: List = []
126
+ placeholder = "?" if self._dialect == DIALECT_MSSQL else "%s"
127
+ for col, val in data.items():
128
+ set_parts.append(f"{_quote(col, self._dialect)} = {placeholder}")
129
+ set_values.append(val)
130
+ all_values = set_values + list(params.values())
131
+ query = f"UPDATE {self._table_sql} SET {', '.join(set_parts)}"
132
+ if where_sql:
133
+ query += f" WHERE {where_sql}"
134
+ count = self._adapter.execute(query, all_values)
135
+ return {"count": count}
136
+
137
+ def delete(self, where: Dict) -> Optional[Dict]:
138
+ existing = self.find_first(where=where)
139
+ params: Dict = {}
140
+ where_sql = _parse_where(self._model, where, params, self._dialect)
141
+ query = f"DELETE FROM {self._table_sql} WHERE {where_sql}"
142
+ self._adapter.execute(query, list(params.values()))
143
+ return existing
144
+
145
+ def delete_many(self, where: Optional[Dict] = None) -> Dict:
146
+ params: Dict = {}
147
+ where_sql = _parse_where(self._model, where, params, self._dialect)
148
+ query = f"DELETE FROM {self._table_sql}"
149
+ if where_sql:
150
+ query += f" WHERE {where_sql}"
151
+ count = self._adapter.execute(query, list(params.values()))
152
+ return {"count": count}
153
+
154
+ def upsert(self, where: Dict, create: Dict, update: Dict) -> Dict:
155
+ existing = self.find_first(where=where)
156
+ if existing:
157
+ return self.update(where=where, data=update) or existing
158
+ return self.create(data=create)
159
+
160
+ def aggregate(self, where=None, _count=None, _sum=None, _avg=None, _min=None, _max=None) -> Dict:
161
+ params: Dict = {}
162
+ where_sql = _parse_where(self._model, where, params, self._dialect)
163
+ aggs: List[str] = []
164
+ if _count:
165
+ aggs.append("COUNT(*) AS _count")
166
+ if _sum:
167
+ for f in (_sum if isinstance(_sum, list) else [_sum]):
168
+ aggs.append(f"SUM({_quote(f, self._dialect)}) AS _sum_{f}")
169
+ if _avg:
170
+ for f in (_avg if isinstance(_avg, list) else [_avg]):
171
+ aggs.append(f"AVG({_quote(f, self._dialect)}) AS _avg_{f}")
172
+ if _min:
173
+ for f in (_min if isinstance(_min, list) else [_min]):
174
+ aggs.append(f"MIN({_quote(f, self._dialect)}) AS _min_{f}")
175
+ if _max:
176
+ for f in (_max if isinstance(_max, list) else [_max]):
177
+ aggs.append(f"MAX({_quote(f, self._dialect)}) AS _max_{f}")
178
+ if not aggs:
179
+ aggs = ["COUNT(*) AS _count"]
180
+
181
+ query = f"SELECT {', '.join(aggs)} FROM {self._table_sql}"
182
+ if where_sql:
183
+ query += f" WHERE {where_sql}"
184
+ rows = self._adapter.exec(query, list(params.values()))
185
+ return rows[0] if rows else {}
186
+
187
+ def vector_search(self, vector: List[float], take: int = 10, where=None, vector_field: str = "embedding", distance_metric: str = "cosine") -> List[Dict]:
188
+ rows = self.find_many(where=where)
189
+ scored = []
190
+ for row in rows:
191
+ raw = row.get(vector_field)
192
+ if raw is None:
193
+ continue
194
+ try:
195
+ vec = json.loads(raw) if isinstance(raw, str) else list(raw)
196
+ except Exception:
197
+ continue
198
+ if not vec or len(vec) != len(vector):
199
+ continue
200
+
201
+ dot = sum(a * b for a, b in zip(vector, vec))
202
+ m1 = sum(a ** 2 for a in vector) ** 0.5
203
+ m2 = sum(b ** 2 for b in vec) ** 0.5
204
+ cosine = dot / (m1 * m2) if m1 and m2 else 0.0
205
+
206
+ if distance_metric == "cosine":
207
+ dist = 1.0 - cosine
208
+ elif distance_metric == "dot":
209
+ dist = -dot
210
+ else:
211
+ dist = sum((a - b) ** 2 for a, b in zip(vector, vec)) ** 0.5
212
+
213
+ scored.append((row, dist))
214
+
215
+ scored.sort(key=lambda x: x[1])
216
+ return [{**row, "distance": dist} for row, dist in scored[:take]]