apexdevkit 1.17.5__tar.gz → 1.17.7__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.
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/PKG-INFO +1 -1
- apexdevkit-1.17.7/apexdevkit/repository/mssql.py +467 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/sqlite.py +101 -16
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/pyproject.toml +1 -1
- apexdevkit-1.17.5/apexdevkit/repository/mssql.py +0 -104
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/LICENSE +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/README.md +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/__init__.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/annotation/__init__.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/annotation/deprecate.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/environment.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/error.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/__init__.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/builder.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/dependable.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/docs.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/name.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/request.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/resource.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/response.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/router.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/schema.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fastapi/service.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/fluent.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/formatter.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/__init__.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/fake.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/fluent.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/httpx.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/json.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/http/url.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/key_fn.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/py.typed +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/__init__.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/base.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/connector.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/database.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/decorator.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/in_memory.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/interface.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/repository/mongo.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/server.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/synchronization.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/testing/__init__.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/testing/database.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/testing/fake.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/testing/rest.py +0 -0
- {apexdevkit-1.17.5 → apexdevkit-1.17.7}/apexdevkit/value.py +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Generic, Iterable, Iterator, TypeVar
|
|
5
|
+
|
|
6
|
+
from pymssql.exceptions import DatabaseError
|
|
7
|
+
|
|
8
|
+
from apexdevkit.error import DoesNotExistError, ExistsError
|
|
9
|
+
from apexdevkit.formatter import Formatter
|
|
10
|
+
from apexdevkit.repository import Database, DatabaseCommand, RepositoryBase
|
|
11
|
+
|
|
12
|
+
ItemT = TypeVar("ItemT")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class MsSqlRepository(RepositoryBase[ItemT]):
|
|
17
|
+
db: Database
|
|
18
|
+
table: SqlTable[ItemT]
|
|
19
|
+
|
|
20
|
+
def __iter__(self) -> Iterator[ItemT]:
|
|
21
|
+
for raw in self.db.execute(self.table.select_all()).fetch_all():
|
|
22
|
+
yield self.table.load(raw)
|
|
23
|
+
|
|
24
|
+
def __len__(self) -> int:
|
|
25
|
+
raw = self.db.execute(self.table.count_all()).fetch_one()
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
return int(raw["n_items"])
|
|
29
|
+
except KeyError:
|
|
30
|
+
raise UnknownError(raw)
|
|
31
|
+
|
|
32
|
+
def delete(self, item_id: str) -> None:
|
|
33
|
+
self.db.execute(self.table.delete(item_id)).fetch_none()
|
|
34
|
+
|
|
35
|
+
def delete_all(self) -> None:
|
|
36
|
+
self.db.execute(self.table.delete_all()).fetch_none()
|
|
37
|
+
|
|
38
|
+
def create(self, item: ItemT) -> ItemT:
|
|
39
|
+
try:
|
|
40
|
+
return self.table.load(self.db.execute(self.table.insert(item)).fetch_one())
|
|
41
|
+
except DatabaseError as e:
|
|
42
|
+
e = MssqlException(e)
|
|
43
|
+
|
|
44
|
+
if e.is_duplication():
|
|
45
|
+
raise self.table.exists(item)
|
|
46
|
+
|
|
47
|
+
raise UnknownError(e.message)
|
|
48
|
+
|
|
49
|
+
def read(self, item_id: str) -> ItemT:
|
|
50
|
+
raw = self.db.execute(self.table.select(item_id)).fetch_one()
|
|
51
|
+
|
|
52
|
+
if not raw:
|
|
53
|
+
raise DoesNotExistError(item_id)
|
|
54
|
+
|
|
55
|
+
return self.table.load(raw)
|
|
56
|
+
|
|
57
|
+
def update(self, item: ItemT) -> None:
|
|
58
|
+
self.db.execute(self.table.update(item)).fetch_none()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SqlTable(Generic[ItemT]): # pragma: no cover
|
|
62
|
+
def count_all(self) -> DatabaseCommand:
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
def insert(self, item: ItemT) -> DatabaseCommand:
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
def select(self, item_id: str) -> DatabaseCommand:
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
|
|
71
|
+
def select_all(self) -> DatabaseCommand:
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
|
|
74
|
+
def delete(self, item_id: str) -> DatabaseCommand:
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
|
|
77
|
+
def delete_all(self) -> DatabaseCommand:
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
def update(self, item: ItemT) -> DatabaseCommand:
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
def load(self, data: dict[str, Any]) -> ItemT:
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
def exists(self, duplicate: ItemT) -> ExistsError:
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class SqlTableDecorator(Generic[ItemT]):
|
|
92
|
+
table: DefaultSqlTable[ItemT]
|
|
93
|
+
|
|
94
|
+
def count_all(self) -> DatabaseCommand:
|
|
95
|
+
return self.table.count_all()
|
|
96
|
+
|
|
97
|
+
def insert(self, item: ItemT) -> DatabaseCommand:
|
|
98
|
+
return self.table.insert(item)
|
|
99
|
+
|
|
100
|
+
def select(self, item_id: str) -> DatabaseCommand:
|
|
101
|
+
return self.table.select(item_id)
|
|
102
|
+
|
|
103
|
+
def select_all(self) -> DatabaseCommand:
|
|
104
|
+
return self.table.select_all()
|
|
105
|
+
|
|
106
|
+
def delete(self, item_id: str) -> DatabaseCommand:
|
|
107
|
+
return self.table.delete(item_id)
|
|
108
|
+
|
|
109
|
+
def delete_all(self) -> DatabaseCommand:
|
|
110
|
+
return self.table.delete_all()
|
|
111
|
+
|
|
112
|
+
def update(self, item: ItemT) -> DatabaseCommand:
|
|
113
|
+
return self.table.update(item)
|
|
114
|
+
|
|
115
|
+
def load(self, data: dict[str, Any]) -> ItemT:
|
|
116
|
+
return self.table.load(data)
|
|
117
|
+
|
|
118
|
+
def exists(self, duplicate: ItemT) -> ExistsError:
|
|
119
|
+
return self.table.exists(duplicate)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class MssqlException:
|
|
124
|
+
code: int
|
|
125
|
+
message: str
|
|
126
|
+
|
|
127
|
+
def __init__(self, e: DatabaseError):
|
|
128
|
+
self.code = e.args[0]
|
|
129
|
+
self.message = e.args[1].decode()
|
|
130
|
+
|
|
131
|
+
def is_duplication(self) -> bool:
|
|
132
|
+
return self.code in [2627, 70003]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class UnknownError(Exception):
|
|
137
|
+
raw: Any
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class MsSqlTableBuilder(Generic[ItemT]):
|
|
142
|
+
username: str | None = None
|
|
143
|
+
schema: str | None = None
|
|
144
|
+
table: str | None = None
|
|
145
|
+
formatter: Formatter[dict[str, Any], ItemT] | None = None
|
|
146
|
+
fields: list[MsSqlField] | None = None
|
|
147
|
+
ordering: list[str] | None = None
|
|
148
|
+
parent_field: str | None = None
|
|
149
|
+
parent_value: Any | None = None
|
|
150
|
+
|
|
151
|
+
def with_username(self, value: str) -> MsSqlTableBuilder[ItemT]:
|
|
152
|
+
return MsSqlTableBuilder[ItemT](
|
|
153
|
+
value,
|
|
154
|
+
self.schema,
|
|
155
|
+
self.table,
|
|
156
|
+
self.formatter,
|
|
157
|
+
self.fields,
|
|
158
|
+
self.ordering,
|
|
159
|
+
self.parent_field,
|
|
160
|
+
self.parent_value,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def with_schema(self, value: str) -> MsSqlTableBuilder[ItemT]:
|
|
164
|
+
return MsSqlTableBuilder[ItemT](
|
|
165
|
+
self.username,
|
|
166
|
+
value,
|
|
167
|
+
self.table,
|
|
168
|
+
self.formatter,
|
|
169
|
+
self.fields,
|
|
170
|
+
self.ordering,
|
|
171
|
+
self.parent_field,
|
|
172
|
+
self.parent_value,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def with_table(self, value: str) -> MsSqlTableBuilder[ItemT]:
|
|
176
|
+
return MsSqlTableBuilder[ItemT](
|
|
177
|
+
self.username,
|
|
178
|
+
self.schema,
|
|
179
|
+
value,
|
|
180
|
+
self.formatter,
|
|
181
|
+
self.fields,
|
|
182
|
+
self.ordering,
|
|
183
|
+
self.parent_field,
|
|
184
|
+
self.parent_value,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def with_formatter(
|
|
188
|
+
self, value: Formatter[dict[str, Any], ItemT]
|
|
189
|
+
) -> MsSqlTableBuilder[ItemT]:
|
|
190
|
+
return MsSqlTableBuilder[ItemT](
|
|
191
|
+
self.username,
|
|
192
|
+
self.schema,
|
|
193
|
+
self.table,
|
|
194
|
+
value,
|
|
195
|
+
self.fields,
|
|
196
|
+
self.ordering,
|
|
197
|
+
self.parent_field,
|
|
198
|
+
self.parent_value,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def with_fields(self, fields: Iterable[str]) -> MsSqlTableBuilder[ItemT]:
|
|
202
|
+
return MsSqlTableBuilder[ItemT](
|
|
203
|
+
self.username,
|
|
204
|
+
self.schema,
|
|
205
|
+
self.table,
|
|
206
|
+
self.formatter,
|
|
207
|
+
[MsSqlField(field, field == "id") for field in list(fields)],
|
|
208
|
+
self.ordering,
|
|
209
|
+
self.parent_field,
|
|
210
|
+
self.parent_value,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def with_order_fields(self, ordering: Iterable[str]) -> MsSqlTableBuilder[ItemT]:
|
|
214
|
+
ordered = list(ordering)
|
|
215
|
+
assert self.fields is not None, "Set fields first."
|
|
216
|
+
|
|
217
|
+
for key in ordered:
|
|
218
|
+
if key not in [field.name for field in self.fields]:
|
|
219
|
+
raise ValueError("Missing fields in the table.")
|
|
220
|
+
return MsSqlTableBuilder[ItemT](
|
|
221
|
+
self.username,
|
|
222
|
+
self.schema,
|
|
223
|
+
self.table,
|
|
224
|
+
self.formatter,
|
|
225
|
+
self.fields,
|
|
226
|
+
ordered,
|
|
227
|
+
self.parent_field,
|
|
228
|
+
self.parent_value,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def with_id(self, identifier: str) -> MsSqlTableBuilder[ItemT]:
|
|
232
|
+
assert self.fields is not None, "Set fields first."
|
|
233
|
+
if identifier not in [field.name for field in self.fields]:
|
|
234
|
+
raise ValueError("Missing fields in the table.")
|
|
235
|
+
|
|
236
|
+
return MsSqlTableBuilder[ItemT](
|
|
237
|
+
self.username,
|
|
238
|
+
self.schema,
|
|
239
|
+
self.table,
|
|
240
|
+
self.formatter,
|
|
241
|
+
[MsSqlField(field.name, field.name == identifier) for field in self.fields],
|
|
242
|
+
self.ordering,
|
|
243
|
+
self.parent_field,
|
|
244
|
+
self.parent_value,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def with_parent(
|
|
248
|
+
self, parent_field: str, parent_value: Any
|
|
249
|
+
) -> MsSqlTableBuilder[ItemT]:
|
|
250
|
+
assert self.fields is not None, "Set fields first."
|
|
251
|
+
if parent_field not in [field.name for field in self.fields]:
|
|
252
|
+
raise ValueError("Missing fields in the table.")
|
|
253
|
+
|
|
254
|
+
return MsSqlTableBuilder[ItemT](
|
|
255
|
+
self.username,
|
|
256
|
+
self.schema,
|
|
257
|
+
self.table,
|
|
258
|
+
self.formatter,
|
|
259
|
+
self.fields,
|
|
260
|
+
self.ordering,
|
|
261
|
+
parent_field,
|
|
262
|
+
parent_value,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def build(self) -> SqlTable[ItemT]:
|
|
266
|
+
if not self.schema or not self.table or not self.formatter or not self.fields:
|
|
267
|
+
raise ValueError("Cannot build sql table.")
|
|
268
|
+
|
|
269
|
+
return DefaultSqlTable(
|
|
270
|
+
self.username,
|
|
271
|
+
self.schema,
|
|
272
|
+
self.table,
|
|
273
|
+
self.formatter,
|
|
274
|
+
self.fields,
|
|
275
|
+
self.ordering,
|
|
276
|
+
self.parent_field,
|
|
277
|
+
self.parent_value,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass(frozen=True)
|
|
282
|
+
class DefaultSqlTable(SqlTable[ItemT]):
|
|
283
|
+
username: str | None
|
|
284
|
+
schema: str
|
|
285
|
+
table: str
|
|
286
|
+
formatter: Formatter[dict[str, Any], ItemT]
|
|
287
|
+
fields: list[MsSqlField]
|
|
288
|
+
ordering: list[str] | None
|
|
289
|
+
parent_key: str | None
|
|
290
|
+
parent_value: Any | None
|
|
291
|
+
|
|
292
|
+
def count_all(self) -> DatabaseCommand:
|
|
293
|
+
where_statement = ""
|
|
294
|
+
if self.parent_key is not None:
|
|
295
|
+
where_statement += (
|
|
296
|
+
"WHERE [" + self.parent_key + "] = " + self._parent_value_sql
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return DatabaseCommand(f"""
|
|
300
|
+
{self._user_check}
|
|
301
|
+
SELECT count(*) AS n_items
|
|
302
|
+
FROM [{self.schema}].[{self.table}]
|
|
303
|
+
{where_statement}
|
|
304
|
+
REVERT
|
|
305
|
+
""")
|
|
306
|
+
|
|
307
|
+
def insert(self, item: ItemT) -> DatabaseCommand:
|
|
308
|
+
dumped = self.formatter.dump(item)
|
|
309
|
+
if self.parent_key is not None:
|
|
310
|
+
dumped[self.parent_key] = self.parent_value
|
|
311
|
+
|
|
312
|
+
columns = ", ".join(["[" + field.name + "]" for field in self.fields])
|
|
313
|
+
placeholders = ", ".join([f"%({key.name})s" for key in self.fields])
|
|
314
|
+
output = ", ".join(["INSERTED." + field.name for field in self.fields])
|
|
315
|
+
|
|
316
|
+
return DatabaseCommand(f"""
|
|
317
|
+
{self._user_check}
|
|
318
|
+
INSERT INTO [{self.schema}].[{self.table}] (
|
|
319
|
+
{columns}
|
|
320
|
+
) OUTPUT
|
|
321
|
+
{output}
|
|
322
|
+
VALUES (
|
|
323
|
+
{placeholders}
|
|
324
|
+
)
|
|
325
|
+
REVERT
|
|
326
|
+
""").with_data(dumped)
|
|
327
|
+
|
|
328
|
+
def select(self, item_id: str) -> DatabaseCommand:
|
|
329
|
+
raw: dict[str, Any] = {self._id: item_id}
|
|
330
|
+
where_statement = f"WHERE [{self._id}] = %({self._id})s"
|
|
331
|
+
if self.parent_key is not None:
|
|
332
|
+
raw[self.parent_key] = self.parent_value
|
|
333
|
+
where_statement += (
|
|
334
|
+
" AND [" + self.parent_key + "] = %(" + self.parent_key + ")s"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
columns = ", ".join(["[" + field.name + "]" for field in self.fields])
|
|
338
|
+
|
|
339
|
+
return DatabaseCommand(f"""
|
|
340
|
+
{self._user_check}
|
|
341
|
+
SELECT
|
|
342
|
+
{columns}
|
|
343
|
+
FROM [{self.schema}].[{self.table}]
|
|
344
|
+
{where_statement}
|
|
345
|
+
REVERT
|
|
346
|
+
""").with_data(raw)
|
|
347
|
+
|
|
348
|
+
def select_all(self) -> DatabaseCommand:
|
|
349
|
+
columns = ", ".join(["[" + field.name + "]" for field in self.fields])
|
|
350
|
+
where_statement = ""
|
|
351
|
+
if self.parent_key is not None:
|
|
352
|
+
where_statement += (
|
|
353
|
+
"WHERE [" + self.parent_key + "] = " + self._parent_value_sql
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return DatabaseCommand(f"""
|
|
357
|
+
{self._user_check}
|
|
358
|
+
SELECT
|
|
359
|
+
{columns}
|
|
360
|
+
FROM [{self.schema}].[{self.table}]
|
|
361
|
+
{where_statement}
|
|
362
|
+
{self._order}
|
|
363
|
+
REVERT
|
|
364
|
+
""")
|
|
365
|
+
|
|
366
|
+
def update(self, item: ItemT) -> DatabaseCommand:
|
|
367
|
+
dumped = self.formatter.dump(item)
|
|
368
|
+
if self.parent_key is not None:
|
|
369
|
+
dumped[self.parent_key] = self.parent_value
|
|
370
|
+
|
|
371
|
+
where_statement = f"WHERE [{self._id}] = %({self._id})s"
|
|
372
|
+
if self.parent_key is not None:
|
|
373
|
+
where_statement += (
|
|
374
|
+
" AND [" + self.parent_key + "] = %(" + self.parent_key + ")s"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
updates = ", ".join(
|
|
378
|
+
[
|
|
379
|
+
f"{field.name} = %({field.name})s"
|
|
380
|
+
for field in self.fields
|
|
381
|
+
if not field.is_id and field.name != self.parent_key
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return DatabaseCommand(f"""
|
|
386
|
+
{self._user_check}
|
|
387
|
+
UPDATE [{self.schema}].[{self.table}]
|
|
388
|
+
SET
|
|
389
|
+
{updates}
|
|
390
|
+
{where_statement}
|
|
391
|
+
REVERT
|
|
392
|
+
""").with_data(dumped)
|
|
393
|
+
|
|
394
|
+
def delete(self, item_id: str) -> DatabaseCommand:
|
|
395
|
+
raw: dict[str, Any] = {self._id: item_id}
|
|
396
|
+
where_statement = f"WHERE [{self._id}] = %({self._id})s"
|
|
397
|
+
if self.parent_key is not None:
|
|
398
|
+
raw[self.parent_key] = self.parent_value
|
|
399
|
+
where_statement += (
|
|
400
|
+
" AND [" + self.parent_key + "] = %(" + self.parent_key + ")s"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return DatabaseCommand(f"""
|
|
404
|
+
{self._user_check}
|
|
405
|
+
DELETE
|
|
406
|
+
FROM [{self.schema}].[{self.table}]
|
|
407
|
+
{where_statement}
|
|
408
|
+
REVERT
|
|
409
|
+
""").with_data(raw)
|
|
410
|
+
|
|
411
|
+
def delete_all(self) -> DatabaseCommand:
|
|
412
|
+
where_statement = ""
|
|
413
|
+
if self.parent_key is not None:
|
|
414
|
+
where_statement += (
|
|
415
|
+
"WHERE [" + self.parent_key + "] = " + self._parent_value_sql
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return DatabaseCommand(f"""
|
|
419
|
+
{self._user_check}
|
|
420
|
+
DELETE
|
|
421
|
+
FROM [{self.schema}].[{self.table}]
|
|
422
|
+
{where_statement}
|
|
423
|
+
REVERT
|
|
424
|
+
""")
|
|
425
|
+
|
|
426
|
+
def load(self, data: dict[str, Any]) -> ItemT:
|
|
427
|
+
return self.formatter.load(data)
|
|
428
|
+
|
|
429
|
+
def exists(self, duplicate: ItemT) -> ExistsError:
|
|
430
|
+
raw = self.formatter.dump(duplicate)
|
|
431
|
+
return ExistsError(duplicate).with_duplicate(
|
|
432
|
+
lambda i: f"{self._id}<{raw[self._id]}>"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def _id(self) -> str:
|
|
437
|
+
result = next((field for field in self.fields if field.is_id), None)
|
|
438
|
+
if result is None:
|
|
439
|
+
raise ValueError("Id field is required.")
|
|
440
|
+
return result.name
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def _user_check(self) -> str:
|
|
444
|
+
if self.username is not None:
|
|
445
|
+
return f"EXECUTE AS USER = '{self.username}'"
|
|
446
|
+
else:
|
|
447
|
+
return ""
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def _order(self) -> str:
|
|
451
|
+
if self.ordering is not None and len(self.ordering) > 0:
|
|
452
|
+
return "ORDER BY " + ", ".join(self.ordering)
|
|
453
|
+
else:
|
|
454
|
+
return ""
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def _parent_value_sql(self) -> str:
|
|
458
|
+
if isinstance(self.parent_value, str):
|
|
459
|
+
return "'" + self.parent_value + "'"
|
|
460
|
+
else:
|
|
461
|
+
return str(self.parent_value)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@dataclass(frozen=True)
|
|
465
|
+
class MsSqlField:
|
|
466
|
+
name: str
|
|
467
|
+
is_id: bool
|
|
@@ -103,20 +103,28 @@ class SqliteTableBuilder(Generic[ItemT]):
|
|
|
103
103
|
table_name: str | None = None
|
|
104
104
|
formatter: Formatter[dict[str, Any], ItemT] | None = None
|
|
105
105
|
fields: list[SqliteField] | None = None
|
|
106
|
+
parent_field: str | None = None
|
|
107
|
+
parent_value: Any | None = None
|
|
106
108
|
|
|
107
109
|
def with_name(self, value: str) -> SqliteTableBuilder[ItemT]:
|
|
108
|
-
return SqliteTableBuilder[ItemT](
|
|
110
|
+
return SqliteTableBuilder[ItemT](
|
|
111
|
+
value, self.formatter, self.fields, self.parent_field, self.parent_value
|
|
112
|
+
)
|
|
109
113
|
|
|
110
114
|
def with_formatter(
|
|
111
115
|
self, value: Formatter[dict[str, Any], ItemT]
|
|
112
116
|
) -> SqliteTableBuilder[ItemT]:
|
|
113
|
-
return SqliteTableBuilder[ItemT](
|
|
117
|
+
return SqliteTableBuilder[ItemT](
|
|
118
|
+
self.table_name, value, self.fields, self.parent_field, self.parent_value
|
|
119
|
+
)
|
|
114
120
|
|
|
115
121
|
def with_fields(self, fields: Iterable[str]) -> SqliteTableBuilder[ItemT]:
|
|
116
122
|
return SqliteTableBuilder[ItemT](
|
|
117
123
|
self.table_name,
|
|
118
124
|
self.formatter,
|
|
119
125
|
[SqliteField(field, field == "id", False) for field in list(fields)],
|
|
126
|
+
self.parent_field,
|
|
127
|
+
self.parent_value,
|
|
120
128
|
)
|
|
121
129
|
|
|
122
130
|
def with_id(self, identifier: str) -> SqliteTableBuilder[ItemT]:
|
|
@@ -131,6 +139,8 @@ class SqliteTableBuilder(Generic[ItemT]):
|
|
|
131
139
|
SqliteField(field.name, field.name == identifier, field.is_composite)
|
|
132
140
|
for field in self.fields
|
|
133
141
|
],
|
|
142
|
+
self.parent_field,
|
|
143
|
+
self.parent_value,
|
|
134
144
|
)
|
|
135
145
|
|
|
136
146
|
def with_composite_key(
|
|
@@ -149,13 +159,36 @@ class SqliteTableBuilder(Generic[ItemT]):
|
|
|
149
159
|
SqliteField(field.name, field.is_id, field.name in list(composites))
|
|
150
160
|
for field in self.fields
|
|
151
161
|
],
|
|
162
|
+
self.parent_field,
|
|
163
|
+
self.parent_value,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def with_parent(
|
|
167
|
+
self, parent_field: str, parent_value: Any
|
|
168
|
+
) -> SqliteTableBuilder[ItemT]:
|
|
169
|
+
assert self.fields is not None, "Set fields first."
|
|
170
|
+
if parent_field not in [field.name for field in self.fields]:
|
|
171
|
+
raise ValueError("Missing fields in the table.")
|
|
172
|
+
|
|
173
|
+
return SqliteTableBuilder[ItemT](
|
|
174
|
+
self.table_name,
|
|
175
|
+
self.formatter,
|
|
176
|
+
self.fields,
|
|
177
|
+
parent_field,
|
|
178
|
+
parent_value,
|
|
152
179
|
)
|
|
153
180
|
|
|
154
181
|
def build(self) -> SqlTable[ItemT]:
|
|
155
182
|
if not self.table_name or not self.formatter or not self.fields:
|
|
156
183
|
raise ValueError("Cannot build sql table.")
|
|
157
184
|
|
|
158
|
-
return _DefaultSqlTable(
|
|
185
|
+
return _DefaultSqlTable(
|
|
186
|
+
self.table_name,
|
|
187
|
+
self.formatter,
|
|
188
|
+
self.fields,
|
|
189
|
+
self.parent_field,
|
|
190
|
+
self.parent_value,
|
|
191
|
+
)
|
|
159
192
|
|
|
160
193
|
|
|
161
194
|
@dataclass(frozen=True)
|
|
@@ -163,14 +196,27 @@ class _DefaultSqlTable(SqlTable[ItemT]):
|
|
|
163
196
|
table_name: str
|
|
164
197
|
formatter: Formatter[dict[str, Any], ItemT]
|
|
165
198
|
fields: list[SqliteField]
|
|
199
|
+
parent_key: str | None
|
|
200
|
+
parent_value: Any | None
|
|
166
201
|
|
|
167
202
|
def count_all(self) -> DatabaseCommand:
|
|
203
|
+
where_statement = ""
|
|
204
|
+
if self.parent_key is not None:
|
|
205
|
+
where_statement += (
|
|
206
|
+
"WHERE " + self.parent_key + " = " + self._parent_value_sql
|
|
207
|
+
)
|
|
208
|
+
|
|
168
209
|
return DatabaseCommand(f"""
|
|
169
210
|
SELECT count(*) as n_items
|
|
170
|
-
FROM {self.table_name.capitalize()}
|
|
211
|
+
FROM {self.table_name.capitalize()}
|
|
212
|
+
{where_statement};
|
|
171
213
|
""")
|
|
172
214
|
|
|
173
215
|
def insert(self, item: ItemT) -> DatabaseCommand:
|
|
216
|
+
dumped = self.formatter.dump(item)
|
|
217
|
+
if self.parent_key is not None:
|
|
218
|
+
dumped[self.parent_key] = self.parent_value
|
|
219
|
+
|
|
174
220
|
columns = ", ".join([field.name for field in self.fields])
|
|
175
221
|
placeholders = ", ".join([f":{key.name}" for key in self.fields])
|
|
176
222
|
|
|
@@ -181,17 +227,23 @@ class _DefaultSqlTable(SqlTable[ItemT]):
|
|
|
181
227
|
{placeholders}
|
|
182
228
|
)
|
|
183
229
|
RETURNING {columns};
|
|
184
|
-
""").with_data(
|
|
230
|
+
""").with_data(dumped)
|
|
185
231
|
|
|
186
232
|
def select(self, item_id: str) -> DatabaseCommand:
|
|
233
|
+
raw: dict[str, Any] = {self._id: item_id}
|
|
234
|
+
where_statement = f"WHERE {self._id} = :{self._id}"
|
|
235
|
+
if self.parent_key is not None:
|
|
236
|
+
raw[self.parent_key] = self.parent_value
|
|
237
|
+
where_statement += " AND " + self.parent_key + " = :" + self.parent_key
|
|
238
|
+
|
|
187
239
|
columns = ", ".join([field.name for field in self.fields])
|
|
188
240
|
|
|
189
241
|
return DatabaseCommand(f"""
|
|
190
242
|
SELECT
|
|
191
243
|
{columns}
|
|
192
244
|
FROM {self.table_name.capitalize()}
|
|
193
|
-
|
|
194
|
-
""").with_data(
|
|
245
|
+
{where_statement};
|
|
246
|
+
""").with_data(raw)
|
|
195
247
|
|
|
196
248
|
def select_duplicate(self, item: ItemT) -> DatabaseCommand:
|
|
197
249
|
raw = self.formatter.dump(item)
|
|
@@ -207,20 +259,35 @@ class _DefaultSqlTable(SqlTable[ItemT]):
|
|
|
207
259
|
""").with_data({key: raw[key] for key in raw if key in self._composite})
|
|
208
260
|
|
|
209
261
|
def select_all(self) -> DatabaseCommand:
|
|
262
|
+
where_statement = ""
|
|
263
|
+
if self.parent_key is not None:
|
|
264
|
+
where_statement += (
|
|
265
|
+
"WHERE " + self.parent_key + " = " + self._parent_value_sql
|
|
266
|
+
)
|
|
267
|
+
|
|
210
268
|
columns = ", ".join([field.name for field in self.fields])
|
|
211
269
|
|
|
212
270
|
return DatabaseCommand(f"""
|
|
213
271
|
SELECT
|
|
214
272
|
{columns}
|
|
215
|
-
FROM {self.table_name.capitalize()}
|
|
273
|
+
FROM {self.table_name.capitalize()}
|
|
274
|
+
{where_statement};
|
|
216
275
|
""")
|
|
217
276
|
|
|
218
277
|
def update(self, item: ItemT) -> DatabaseCommand:
|
|
278
|
+
dumped = self.formatter.dump(item)
|
|
279
|
+
if self.parent_key is not None:
|
|
280
|
+
dumped[self.parent_key] = self.parent_value
|
|
281
|
+
|
|
282
|
+
where_statement = f"WHERE {self._id} = :{self._id}"
|
|
283
|
+
if self.parent_key is not None:
|
|
284
|
+
where_statement += " AND " + self.parent_key + " = :" + self.parent_key
|
|
285
|
+
|
|
219
286
|
updates = ", ".join(
|
|
220
287
|
[
|
|
221
288
|
f"{field.name} = :{field.name}"
|
|
222
289
|
for field in self.fields
|
|
223
|
-
if not field.is_id
|
|
290
|
+
if not field.is_id and field.name != self.parent_key
|
|
224
291
|
]
|
|
225
292
|
)
|
|
226
293
|
|
|
@@ -228,22 +295,33 @@ class _DefaultSqlTable(SqlTable[ItemT]):
|
|
|
228
295
|
UPDATE {self.table_name.capitalize()}
|
|
229
296
|
SET
|
|
230
297
|
{updates}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
""").with_data(self.formatter.dump(item))
|
|
298
|
+
{where_statement};
|
|
299
|
+
""").with_data(dumped)
|
|
234
300
|
|
|
235
301
|
def delete(self, item_id: str) -> DatabaseCommand:
|
|
302
|
+
raw: dict[str, Any] = {self._id: item_id}
|
|
303
|
+
where_statement = f"WHERE {self._id} = :{self._id}"
|
|
304
|
+
if self.parent_key is not None:
|
|
305
|
+
raw[self.parent_key] = self.parent_value
|
|
306
|
+
where_statement += " AND " + self.parent_key + " = :" + self.parent_key
|
|
307
|
+
|
|
236
308
|
return DatabaseCommand(f"""
|
|
237
309
|
DELETE
|
|
238
310
|
FROM {self.table_name.capitalize()}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
""").with_data({self._id: item_id})
|
|
311
|
+
{where_statement};
|
|
312
|
+
""").with_data(raw)
|
|
242
313
|
|
|
243
314
|
def delete_all(self) -> DatabaseCommand:
|
|
315
|
+
where_statement = ""
|
|
316
|
+
if self.parent_key is not None:
|
|
317
|
+
where_statement += (
|
|
318
|
+
"WHERE " + self.parent_key + " = " + self._parent_value_sql
|
|
319
|
+
)
|
|
320
|
+
|
|
244
321
|
return DatabaseCommand(f"""
|
|
245
322
|
DELETE
|
|
246
|
-
FROM {self.table_name.capitalize()}
|
|
323
|
+
FROM {self.table_name.capitalize()}
|
|
324
|
+
{where_statement};
|
|
247
325
|
""")
|
|
248
326
|
|
|
249
327
|
def load(self, data: dict[str, Any]) -> ItemT:
|
|
@@ -269,6 +347,13 @@ class _DefaultSqlTable(SqlTable[ItemT]):
|
|
|
269
347
|
names = [field.name for field in self.fields if field.is_composite]
|
|
270
348
|
return [self._id] if len(names) == 0 else names
|
|
271
349
|
|
|
350
|
+
@property
|
|
351
|
+
def _parent_value_sql(self) -> str:
|
|
352
|
+
if isinstance(self.parent_value, str):
|
|
353
|
+
return "'" + self.parent_value + "'"
|
|
354
|
+
else:
|
|
355
|
+
return str(self.parent_value)
|
|
356
|
+
|
|
272
357
|
|
|
273
358
|
@dataclass(frozen=True)
|
|
274
359
|
class SqliteField:
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, Generic, Iterator, TypeVar
|
|
5
|
-
|
|
6
|
-
from pymssql.exceptions import DatabaseError
|
|
7
|
-
|
|
8
|
-
from apexdevkit.error import DoesNotExistError, ExistsError
|
|
9
|
-
from apexdevkit.repository import Database, DatabaseCommand, RepositoryBase
|
|
10
|
-
|
|
11
|
-
ItemT = TypeVar("ItemT")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class MsSqlRepository(RepositoryBase[ItemT]):
|
|
16
|
-
db: Database
|
|
17
|
-
table: SqlTable[ItemT]
|
|
18
|
-
|
|
19
|
-
def __iter__(self) -> Iterator[ItemT]:
|
|
20
|
-
for raw in self.db.execute(self.table.select_all()).fetch_all():
|
|
21
|
-
yield self.table.load(raw)
|
|
22
|
-
|
|
23
|
-
def __len__(self) -> int:
|
|
24
|
-
raw = self.db.execute(self.table.count_all()).fetch_one()
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
return int(raw["n_items"])
|
|
28
|
-
except KeyError:
|
|
29
|
-
raise UnknownError(raw)
|
|
30
|
-
|
|
31
|
-
def delete(self, item_id: str) -> None:
|
|
32
|
-
self.db.execute(self.table.delete(item_id)).fetch_none()
|
|
33
|
-
|
|
34
|
-
def delete_all(self) -> None:
|
|
35
|
-
self.db.execute(self.table.delete_all()).fetch_none()
|
|
36
|
-
|
|
37
|
-
def create(self, item: ItemT) -> ItemT:
|
|
38
|
-
try:
|
|
39
|
-
return self.table.load(self.db.execute(self.table.insert(item)).fetch_one())
|
|
40
|
-
except DatabaseError as e:
|
|
41
|
-
e = MssqlException(e)
|
|
42
|
-
|
|
43
|
-
if e.is_duplication():
|
|
44
|
-
raise self.table.exists(item)
|
|
45
|
-
|
|
46
|
-
raise UnknownError(e.message)
|
|
47
|
-
|
|
48
|
-
def read(self, item_id: str) -> ItemT:
|
|
49
|
-
raw = self.db.execute(self.table.select(item_id)).fetch_one()
|
|
50
|
-
|
|
51
|
-
if not raw:
|
|
52
|
-
raise DoesNotExistError(item_id)
|
|
53
|
-
|
|
54
|
-
return self.table.load(raw)
|
|
55
|
-
|
|
56
|
-
def update(self, item: ItemT) -> None:
|
|
57
|
-
self.db.execute(self.table.update(item)).fetch_none()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class SqlTable(Generic[ItemT]): # pragma: no cover
|
|
61
|
-
def count_all(self) -> DatabaseCommand:
|
|
62
|
-
raise NotImplementedError
|
|
63
|
-
|
|
64
|
-
def insert(self, item: ItemT) -> DatabaseCommand:
|
|
65
|
-
raise NotImplementedError
|
|
66
|
-
|
|
67
|
-
def select(self, item_id: str) -> DatabaseCommand:
|
|
68
|
-
raise NotImplementedError
|
|
69
|
-
|
|
70
|
-
def select_all(self) -> DatabaseCommand:
|
|
71
|
-
raise NotImplementedError
|
|
72
|
-
|
|
73
|
-
def delete(self, item_id: str) -> DatabaseCommand:
|
|
74
|
-
raise NotImplementedError
|
|
75
|
-
|
|
76
|
-
def delete_all(self) -> DatabaseCommand:
|
|
77
|
-
raise NotImplementedError
|
|
78
|
-
|
|
79
|
-
def update(self, item: ItemT) -> DatabaseCommand:
|
|
80
|
-
raise NotImplementedError
|
|
81
|
-
|
|
82
|
-
def load(self, data: dict[str, Any]) -> ItemT:
|
|
83
|
-
raise NotImplementedError
|
|
84
|
-
|
|
85
|
-
def exists(self, duplicate: ItemT) -> ExistsError:
|
|
86
|
-
raise NotImplementedError
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@dataclass
|
|
90
|
-
class MssqlException:
|
|
91
|
-
code: int
|
|
92
|
-
message: str
|
|
93
|
-
|
|
94
|
-
def __init__(self, e: DatabaseError):
|
|
95
|
-
self.code = e.args[0]
|
|
96
|
-
self.message = e.args[1].decode()
|
|
97
|
-
|
|
98
|
-
def is_duplication(self) -> bool:
|
|
99
|
-
return self.code in [2627, 70003]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@dataclass
|
|
103
|
-
class UnknownError(Exception):
|
|
104
|
-
raw: Any
|
|
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
|