sql-athame 0.3.16__py3-none-any.whl → 0.4.0a2__py3-none-any.whl

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.
sql_athame/base.py CHANGED
@@ -106,14 +106,12 @@ class Fragment:
106
106
  @overload
107
107
  def prep_query(
108
108
  self, allow_slots: Literal[True]
109
- ) -> Tuple[str, List[Union[Placeholder, Slot]]]:
110
- ... # pragma: no cover
109
+ ) -> Tuple[str, List[Union[Placeholder, Slot]]]: ... # pragma: no cover
111
110
 
112
111
  @overload
113
112
  def prep_query(
114
113
  self, allow_slots: Literal[False] = False
115
- ) -> Tuple[str, List[Placeholder]]:
116
- ... # pragma: no cover
114
+ ) -> Tuple[str, List[Placeholder]]: ... # pragma: no cover
117
115
 
118
116
  def prep_query(self, allow_slots: bool = False) -> Tuple[str, List[Any]]:
119
117
  parts: List[FlatPart] = []
@@ -228,12 +226,10 @@ class SQLFormatter:
228
226
  return lit(f"{quote_identifier(name)}")
229
227
 
230
228
  @overload
231
- def all(self, parts: Iterable[Fragment]) -> Fragment:
232
- ... # pragma: no cover
229
+ def all(self, parts: Iterable[Fragment]) -> Fragment: ... # pragma: no cover
233
230
 
234
231
  @overload
235
- def all(self, *parts: Fragment) -> Fragment:
236
- ... # pragma: no cover
232
+ def all(self, *parts: Fragment) -> Fragment: ... # pragma: no cover
237
233
 
238
234
  def all(self, *parts) -> Fragment: # type: ignore
239
235
  if parts and not isinstance(parts[0], Fragment):
@@ -241,12 +237,10 @@ class SQLFormatter:
241
237
  return any_all(list(parts), "AND", "TRUE")
242
238
 
243
239
  @overload
244
- def any(self, parts: Iterable[Fragment]) -> Fragment:
245
- ... # pragma: no cover
240
+ def any(self, parts: Iterable[Fragment]) -> Fragment: ... # pragma: no cover
246
241
 
247
242
  @overload
248
- def any(self, *parts: Fragment) -> Fragment:
249
- ... # pragma: no cover
243
+ def any(self, *parts: Fragment) -> Fragment: ... # pragma: no cover
250
244
 
251
245
  def any(self, *parts) -> Fragment: # type: ignore
252
246
  if parts and not isinstance(parts[0], Fragment):
@@ -254,12 +248,10 @@ class SQLFormatter:
254
248
  return any_all(list(parts), "OR", "FALSE")
255
249
 
256
250
  @overload
257
- def list(self, parts: Iterable[Fragment]) -> Fragment:
258
- ... # pragma: no cover
251
+ def list(self, parts: Iterable[Fragment]) -> Fragment: ... # pragma: no cover
259
252
 
260
253
  @overload
261
- def list(self, *parts: Fragment) -> Fragment:
262
- ... # pragma: no cover
254
+ def list(self, *parts: Fragment) -> Fragment: ... # pragma: no cover
263
255
 
264
256
  def list(self, *parts) -> Fragment: # type: ignore
265
257
  if parts and not isinstance(parts[0], Fragment):
sql_athame/dataclasses.py CHANGED
@@ -54,7 +54,7 @@ def model_field_metadata(
54
54
  def model_field(
55
55
  *, type: str, constraints: Union[str, Iterable[str]] = (), **kwargs: Any
56
56
  ) -> Any:
57
- return field(**kwargs, metadata=model_field_metadata(type, constraints)) # type: ignore
57
+ return field(**kwargs, metadata=model_field_metadata(type, constraints))
58
58
 
59
59
 
60
60
  sql_create_type_map = {
@@ -100,17 +100,36 @@ class ModelBase(Mapping[str, Any]):
100
100
  _cache: Dict[tuple, Any]
101
101
  table_name: str
102
102
  primary_key_names: Tuple[str, ...]
103
+ array_safe_insert: bool
103
104
 
104
105
  def __init_subclass__(
105
- cls, *, table_name: str, primary_key: Union[FieldNames, str] = (), **kwargs: Any
106
+ cls,
107
+ *,
108
+ table_name: str,
109
+ primary_key: Union[FieldNames, str] = (),
110
+ insert_multiple_mode: str = "unnest",
111
+ **kwargs: Any,
106
112
  ):
107
113
  cls._cache = {}
108
114
  cls.table_name = table_name
115
+ if insert_multiple_mode == "array_safe":
116
+ cls.array_safe_insert = True
117
+ elif insert_multiple_mode == "unnest":
118
+ cls.array_safe_insert = False
119
+ else:
120
+ raise ValueError("Unknown `insert_multiple_mode`")
109
121
  if isinstance(primary_key, str):
110
122
  cls.primary_key_names = (primary_key,)
111
123
  else:
112
124
  cls.primary_key_names = tuple(primary_key)
113
125
 
126
+ @classmethod
127
+ def _fields(cls):
128
+ # wrapper to ignore typing weirdness: 'Argument 1 to "fields"
129
+ # has incompatible type "..."; expected "DataclassInstance |
130
+ # type[DataclassInstance]"'
131
+ return fields(cls) # type: ignore
132
+
114
133
  @classmethod
115
134
  def _cached(cls, key: tuple, thunk: Callable[[], U]) -> U:
116
135
  try:
@@ -139,7 +158,7 @@ class ModelBase(Mapping[str, Any]):
139
158
  try:
140
159
  return cls._column_info[column] # type: ignore
141
160
  except AttributeError:
142
- cls._column_info = {f.name: column_info_for_field(f) for f in fields(cls)}
161
+ cls._column_info = {f.name: column_info_for_field(f) for f in cls._fields()}
143
162
  return cls._column_info[column]
144
163
 
145
164
  @classmethod
@@ -152,7 +171,7 @@ class ModelBase(Mapping[str, Any]):
152
171
 
153
172
  @classmethod
154
173
  def field_names(cls, *, exclude: FieldNamesSet = ()) -> List[str]:
155
- return [f.name for f in fields(cls) if f.name not in exclude]
174
+ return [f.name for f in cls._fields() if f.name not in exclude]
156
175
 
157
176
  @classmethod
158
177
  def field_names_sql(
@@ -171,7 +190,7 @@ class ModelBase(Mapping[str, Any]):
171
190
  ) -> Callable[[T], List[Any]]:
172
191
  env: Dict[str, Any] = dict()
173
192
  func = ["def get_field_values(self): return ["]
174
- for f in fields(cls):
193
+ for f in cls._fields():
175
194
  if f.name not in exclude:
176
195
  func.append(f"self.{f.name},")
177
196
  func += ["]"]
@@ -200,23 +219,23 @@ class ModelBase(Mapping[str, Any]):
200
219
  def from_tuple(
201
220
  cls: Type[T], tup: tuple, *, offset: int = 0, exclude: FieldNamesSet = ()
202
221
  ) -> T:
203
- names = (f.name for f in fields(cls) if f.name not in exclude)
222
+ names = (f.name for f in cls._fields() if f.name not in exclude)
204
223
  kwargs = {name: tup[offset] for offset, name in enumerate(names, start=offset)}
205
- return cls(**kwargs) # type: ignore
224
+ return cls(**kwargs)
206
225
 
207
226
  @classmethod
208
227
  def from_dict(
209
228
  cls: Type[T], dct: Dict[str, Any], *, exclude: FieldNamesSet = ()
210
229
  ) -> T:
211
- names = {f.name for f in fields(cls) if f.name not in exclude}
230
+ names = {f.name for f in cls._fields() if f.name not in exclude}
212
231
  kwargs = {k: v for k, v in dct.items() if k in names}
213
- return cls(**kwargs) # type: ignore
232
+ return cls(**kwargs)
214
233
 
215
234
  @classmethod
216
235
  def ensure_model(cls: Type[T], row: Union[T, Mapping[str, Any]]) -> T:
217
236
  if isinstance(row, cls):
218
237
  return row
219
- return cls(**row) # type: ignore
238
+ return cls(**row)
220
239
 
221
240
  @classmethod
222
241
  def create_table_sql(cls) -> Fragment:
@@ -226,7 +245,7 @@ class ModelBase(Mapping[str, Any]):
226
245
  sql.identifier(f.name),
227
246
  sql.literal(cls.column_info(f.name).create_table_string()),
228
247
  )
229
- for f in fields(cls)
248
+ for f in cls._fields()
230
249
  ]
231
250
  if cls.primary_key_names:
232
251
  entries += [sql("PRIMARY KEY ({})", sql.list(cls.primary_key_names_sql()))]
@@ -278,7 +297,7 @@ class ModelBase(Mapping[str, Any]):
278
297
  *cls.select_sql(order_by=order_by, for_update=for_update, where=where),
279
298
  prefetch=prefetch,
280
299
  ):
281
- yield cls(**row) # type: ignore
300
+ yield cls(**row)
282
301
 
283
302
  @classmethod
284
303
  async def select(
@@ -289,7 +308,7 @@ class ModelBase(Mapping[str, Any]):
289
308
  where: Where = (),
290
309
  ) -> List[T]:
291
310
  return [
292
- cls(**row) # type: ignore
311
+ cls(**row)
293
312
  for row in await connection_or_pool.fetch(
294
313
  *cls.select_sql(order_by=order_by, for_update=for_update, where=where)
295
314
  )
@@ -310,7 +329,7 @@ class ModelBase(Mapping[str, Any]):
310
329
  cls: Type[T], connection_or_pool: Union[Connection, Pool], **kwargs: Any
311
330
  ) -> T:
312
331
  row = await connection_or_pool.fetchrow(*cls.create_sql(**kwargs))
313
- return cls(**row) # type: ignore
332
+ return cls(**row)
314
333
 
315
334
  def insert_sql(self, exclude: FieldNamesSet = ()) -> Fragment:
316
335
  cached = self._cached(
@@ -400,26 +419,76 @@ class ModelBase(Mapping[str, Any]):
400
419
  )
401
420
 
402
421
  @classmethod
403
- async def insert_multiple(
422
+ def insert_multiple_array_safe_sql(cls: Type[T], rows: Iterable[T]) -> Fragment:
423
+ return sql(
424
+ "INSERT INTO {table} ({fields}) VALUES {values}",
425
+ table=cls.table_name_sql(),
426
+ fields=sql.list(cls.field_names_sql()),
427
+ values=sql.list(
428
+ sql("({})", sql.list(row.field_values_sql(default_none=True)))
429
+ for row in rows
430
+ ),
431
+ )
432
+
433
+ @classmethod
434
+ async def insert_multiple_unnest(
404
435
  cls: Type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
405
436
  ) -> str:
406
437
  return await connection_or_pool.execute(*cls.insert_multiple_sql(rows))
407
438
 
408
439
  @classmethod
409
- async def upsert_multiple(
440
+ async def insert_multiple_array_safe(
441
+ cls: Type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
442
+ ) -> str:
443
+ for chunk in chunked(rows, 100):
444
+ last = await connection_or_pool.execute(
445
+ *cls.insert_multiple_array_safe_sql(chunk)
446
+ )
447
+ return last
448
+
449
+ @classmethod
450
+ async def insert_multiple(
451
+ cls: Type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
452
+ ) -> str:
453
+ if cls.array_safe_insert:
454
+ return await cls.insert_multiple_array_safe(connection_or_pool, rows)
455
+ else:
456
+ return await cls.insert_multiple_unnest(connection_or_pool, rows)
457
+
458
+ @classmethod
459
+ async def upsert_multiple_unnest(
410
460
  cls: Type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
411
461
  ) -> str:
412
462
  return await connection_or_pool.execute(
413
463
  *cls.upsert_sql(cls.insert_multiple_sql(rows))
414
464
  )
415
465
 
466
+ @classmethod
467
+ async def upsert_multiple_array_safe(
468
+ cls: Type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
469
+ ) -> str:
470
+ for chunk in chunked(rows, 100):
471
+ last = await connection_or_pool.execute(
472
+ *cls.upsert_sql(cls.insert_multiple_array_safe_sql(chunk))
473
+ )
474
+ return last
475
+
476
+ @classmethod
477
+ async def upsert_multiple(
478
+ cls: Type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
479
+ ) -> str:
480
+ if cls.array_safe_insert:
481
+ return await cls.upsert_multiple_array_safe(connection_or_pool, rows)
482
+ else:
483
+ return await cls.upsert_multiple_unnest(connection_or_pool, rows)
484
+
416
485
  @classmethod
417
486
  def _get_equal_ignoring_fn(
418
487
  cls: Type[T], ignore: FieldNamesSet = ()
419
488
  ) -> Callable[[T, T], bool]:
420
489
  env: Dict[str, Any] = dict()
421
490
  func = ["def equal_ignoring(a, b):"]
422
- for f in fields(cls):
491
+ for f in cls._fields():
423
492
  if f.name not in ignore:
424
493
  func.append(f" if a.{f.name} != b.{f.name}: return False")
425
494
  func += [" return True"]
@@ -473,7 +542,7 @@ class ModelBase(Mapping[str, Any]):
473
542
  "def differences_ignoring(a, b):",
474
543
  " diffs = []",
475
544
  ]
476
- for f in fields(cls):
545
+ for f in cls._fields():
477
546
  if f.name not in ignore:
478
547
  func.append(
479
548
  f" if a.{f.name} != b.{f.name}: diffs.append({repr(f.name)})"
@@ -523,3 +592,8 @@ class ModelBase(Mapping[str, Any]):
523
592
  await cls.delete_multiple(connection, deleted)
524
593
 
525
594
  return created, updated_triples, deleted
595
+
596
+
597
+ def chunked(lst, n):
598
+ for i in range(0, len(lst), n):
599
+ yield lst[i : i + n]
sql_athame/sqlalchemy.py CHANGED
@@ -27,7 +27,6 @@ try:
27
27
  query = "".join(out_parts).strip()
28
28
  return text(query).bindparams(*bindparams.values())
29
29
 
30
-
31
30
  except ImportError:
32
31
 
33
32
  def sqlalchemy_text_from_fragment(self: "Fragment") -> Any:
@@ -1,19 +1,20 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sql-athame
3
- Version: 0.3.16
3
+ Version: 0.4.0a2
4
4
  Summary: Python tool for slicing and dicing SQL
5
5
  Home-page: https://github.com/bdowning/sql-athame
6
6
  License: MIT
7
7
  Author: Brian Downing
8
8
  Author-email: bdowning@lavos.net
9
- Requires-Python: >=3.7,<4.0
9
+ Requires-Python: >=3.9,<4.0
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.7
13
- Classifier: Programming Language :: Python :: 3.8
14
12
  Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
15
16
  Provides-Extra: asyncpg
16
- Requires-Dist: asyncpg; extra == "asyncpg"
17
+ Requires-Dist: asyncpg ; extra == "asyncpg"
17
18
  Requires-Dist: typing-extensions
18
19
  Project-URL: Repository, https://github.com/bdowning/sql-athame
19
20
  Description-Content-Type: text/markdown
@@ -0,0 +1,11 @@
1
+ sql_athame/__init__.py,sha256=rzUQcbzmj3qkPZpL9jI_ALTRv-e1pAV4jSCryWkutlk,130
2
+ sql_athame/base.py,sha256=fSnHQhh5ULeJ5q32RVUAvpWtF0qoY61B2gEEP59Nrpo,10350
3
+ sql_athame/dataclasses.py,sha256=oo1NnUrd-THKVRtTnAK08P4UnFIUBsVJHrOO5tgfi_I,19697
4
+ sql_athame/escape.py,sha256=LXExbiYtc407yDU4vPieyY2Pq5nypsJFfBc_2-gsbUg,743
5
+ sql_athame/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ sql_athame/sqlalchemy.py,sha256=c-pCLE11hTh5I19rY1Vp5E7P7lAaj9i-i7ko2L8rlF4,1305
7
+ sql_athame/types.py,sha256=7P4OyY0ezRlb2UDD9lpdXiLChnhQcBvHWaG_PKy3jmE,412
8
+ sql_athame-0.4.0a2.dist-info/LICENSE,sha256=xqV29vPFqITcKifYrGPgVIBjq4fdmLSwY3gRUtDKafg,1076
9
+ sql_athame-0.4.0a2.dist-info/METADATA,sha256=_2r7ZYOb5St8muq7EnfoiNEQJA1eskEibMjn7JHkcxI,12845
10
+ sql_athame-0.4.0a2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
11
+ sql_athame-0.4.0a2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry 1.0.3
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,11 +0,0 @@
1
- sql_athame/__init__.py,sha256=rzUQcbzmj3qkPZpL9jI_ALTRv-e1pAV4jSCryWkutlk,130
2
- sql_athame/base.py,sha256=JIe2oUtADAuF1wAsrK2-VoA8ZbcXIECMNEamdd7f8-Y,10414
3
- sql_athame/dataclasses.py,sha256=d_QRwLulhxtRKtOlUkjuBcMYv_Zd2ioWLr8y-yP2GJ4,17208
4
- sql_athame/escape.py,sha256=LXExbiYtc407yDU4vPieyY2Pq5nypsJFfBc_2-gsbUg,743
5
- sql_athame/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- sql_athame/sqlalchemy.py,sha256=3ivXs3GqoGebJig20XIaCZQcdccB-dOrLVq2uztIpb8,1306
7
- sql_athame/types.py,sha256=7P4OyY0ezRlb2UDD9lpdXiLChnhQcBvHWaG_PKy3jmE,412
8
- sql_athame-0.3.16.dist-info/LICENSE,sha256=xqV29vPFqITcKifYrGPgVIBjq4fdmLSwY3gRUtDKafg,1076
9
- sql_athame-0.3.16.dist-info/WHEEL,sha256=V7iVckP-GYreevsTDnv1eAinQt_aArwnAxmnP0gygBY,83
10
- sql_athame-0.3.16.dist-info/METADATA,sha256=9gdIZYfTND8xXnu0-5LaEYE5KdlvtNrO5Fu7TELIbjo,12790
11
- sql_athame-0.3.16.dist-info/RECORD,,