@an5/adapters 0.2.3 → 0.2.4
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.
- package/README.md +304 -283
- package/dotnet/An5Adapter.cs +275 -275
- package/dotnet/Base/Core.cs +82 -82
- package/dotnet/Mssql/MssqlEngine.cs +144 -144
- package/dotnet/Postgres/PostgresEngine.cs +121 -121
- package/package.json +20 -10
- package/python/an5_adapter.py +97 -97
- package/python/base/__init__.py +3 -3
- package/python/base/dialects.py +8 -8
- package/python/base/metadata.py +10 -10
- package/python/base/sql.py +121 -121
- package/python/mssql/__init__.py +1 -1
- package/python/mssql/provider.py +62 -62
- package/python/postgres/__init__.py +1 -1
- package/python/postgres/provider.py +15 -15
- package/python/table_client.py +216 -216
- package/typescript/src/an5Adapter.ts +505 -505
- package/typescript/src/base/index.ts +3 -3
- package/typescript/src/base/metadata.ts +28 -28
- package/typescript/src/base/sql.ts +98 -98
- package/typescript/src/base/types.ts +24 -24
- package/typescript/src/browser.ts +6 -6
- package/typescript/src/config.ts +95 -95
- package/typescript/src/googlesheets/adapter.ts +259 -259
- package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
- package/typescript/src/googlesheets/config.ts +47 -47
- package/typescript/src/googlesheets/helpers.ts +143 -143
- package/typescript/src/googlesheets/index.ts +3 -3
- package/typescript/src/googlesheets/parseConnectionString.ts +46 -46
- package/typescript/src/googlesheets/retry.ts +126 -126
- package/typescript/src/googlesheets/sqlExecutor.ts +301 -301
- package/typescript/src/googlesheets/tableClient.ts +455 -455
- package/typescript/src/googlesheets/types.ts +6 -6
- package/typescript/src/index.ts +18 -18
- package/typescript/src/mssql/engine.ts +132 -132
- package/typescript/src/mssql/index.ts +1 -1
- package/typescript/src/mysql/engine.ts +80 -80
- package/typescript/src/mysql/index.ts +1 -1
- package/typescript/src/postgres/engine.ts +87 -87
- package/typescript/src/postgres/index.ts +1 -1
- package/typescript/src/sqlite/engine.ts +55 -55
- package/typescript/src/sqlite/index.ts +1 -1
- /package/{typescript → dist}/an5Adapter.d.ts +0 -0
- /package/{typescript → dist}/an5Adapter.js +0 -0
- /package/{typescript → dist}/base/index.d.ts +0 -0
- /package/{typescript → dist}/base/index.js +0 -0
- /package/{typescript → dist}/base/metadata.d.ts +0 -0
- /package/{typescript → dist}/base/metadata.js +0 -0
- /package/{typescript → dist}/base/sql.d.ts +0 -0
- /package/{typescript → dist}/base/sql.js +0 -0
- /package/{typescript → dist}/base/types.d.ts +0 -0
- /package/{typescript → dist}/base/types.js +0 -0
- /package/{typescript → dist}/base/uuid.d.ts +0 -0
- /package/{typescript → dist}/base/uuid.js +0 -0
- /package/{typescript → dist}/browser.d.ts +0 -0
- /package/{typescript → dist}/browser.js +0 -0
- /package/{typescript → dist}/config.d.ts +0 -0
- /package/{typescript → dist}/config.js +0 -0
- /package/{typescript → dist}/googlesheets/adapter.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/adapter.js +0 -0
- /package/{typescript → dist}/googlesheets/an5SheetsAdapter.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/an5SheetsAdapter.js +0 -0
- /package/{typescript → dist}/googlesheets/config.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/config.js +0 -0
- /package/{typescript → dist}/googlesheets/helpers.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/helpers.js +0 -0
- /package/{typescript → dist}/googlesheets/index.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/index.js +0 -0
- /package/{typescript → dist}/googlesheets/parseConnectionString.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/parseConnectionString.js +0 -0
- /package/{typescript → dist}/googlesheets/retry.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/retry.js +0 -0
- /package/{typescript → dist}/googlesheets/sqlExecutor.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/sqlExecutor.js +0 -0
- /package/{typescript → dist}/googlesheets/tableClient.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/tableClient.js +0 -0
- /package/{typescript → dist}/googlesheets/types.d.ts +0 -0
- /package/{typescript → dist}/googlesheets/types.js +0 -0
- /package/{typescript → dist}/index.d.ts +0 -0
- /package/{typescript → dist}/index.js +0 -0
- /package/{typescript → dist}/mssql/engine.d.ts +0 -0
- /package/{typescript → dist}/mssql/engine.js +0 -0
- /package/{typescript → dist}/mssql/index.d.ts +0 -0
- /package/{typescript → dist}/mssql/index.js +0 -0
- /package/{typescript → dist}/mysql/engine.d.ts +0 -0
- /package/{typescript → dist}/mysql/engine.js +0 -0
- /package/{typescript → dist}/mysql/index.d.ts +0 -0
- /package/{typescript → dist}/mysql/index.js +0 -0
- /package/{typescript → dist}/postgres/engine.d.ts +0 -0
- /package/{typescript → dist}/postgres/engine.js +0 -0
- /package/{typescript → dist}/postgres/index.d.ts +0 -0
- /package/{typescript → dist}/postgres/index.js +0 -0
- /package/{typescript → dist}/sqlite/engine.d.ts +0 -0
- /package/{typescript → dist}/sqlite/engine.js +0 -0
- /package/{typescript → dist}/sqlite/index.d.ts +0 -0
- /package/{typescript → dist}/sqlite/index.js +0 -0
package/python/table_client.py
CHANGED
|
@@ -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]]
|