sql-athame 0.4.0a10__py3-none-any.whl → 0.4.0a12__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/dataclasses.py CHANGED
@@ -157,7 +157,7 @@ class ModelBase:
157
157
  _cache: dict[tuple, Any]
158
158
  table_name: str
159
159
  primary_key_names: tuple[str, ...]
160
- array_safe_insert: bool
160
+ insert_multiple_mode: str
161
161
 
162
162
  def __init_subclass__(
163
163
  cls,
@@ -169,12 +169,9 @@ class ModelBase:
169
169
  ):
170
170
  cls._cache = {}
171
171
  cls.table_name = table_name
172
- if insert_multiple_mode == "array_safe":
173
- cls.array_safe_insert = True
174
- elif insert_multiple_mode == "unnest":
175
- cls.array_safe_insert = False
176
- else:
172
+ if insert_multiple_mode not in ("array_safe", "unnest", "executemany"):
177
173
  raise ValueError("Unknown `insert_multiple_mode`")
174
+ cls.insert_multiple_mode = insert_multiple_mode
178
175
  if isinstance(primary_key, str):
179
176
  cls.primary_key_names = (primary_key,)
180
177
  else:
@@ -233,8 +230,21 @@ class ModelBase:
233
230
 
234
231
  @classmethod
235
232
  def field_names_sql(
236
- cls, *, prefix: Optional[str] = None, exclude: FieldNamesSet = ()
233
+ cls,
234
+ *,
235
+ prefix: Optional[str] = None,
236
+ exclude: FieldNamesSet = (),
237
+ as_prepended: Optional[str] = None,
237
238
  ) -> list[Fragment]:
239
+ if as_prepended:
240
+ return [
241
+ sql(
242
+ "{} AS {}",
243
+ sql.identifier(f, prefix=prefix),
244
+ sql.identifier(f"{as_prepended}{f}"),
245
+ )
246
+ for f in cls.field_names(exclude=exclude)
247
+ ]
238
248
  return [
239
249
  sql.identifier(f, prefix=prefix) for f in cls.field_names(exclude=exclude)
240
250
  ]
@@ -303,6 +313,16 @@ class ModelBase:
303
313
  cls.from_mapping = from_mapping_fn # type: ignore
304
314
  return from_mapping_fn(mapping)
305
315
 
316
+ @classmethod
317
+ def from_prepended_mapping(
318
+ cls: type[T], mapping: Mapping[str, Any], prepend: str
319
+ ) -> T:
320
+ filtered_dict: dict[str, Any] = {}
321
+ for k, v in mapping.items():
322
+ if k.startswith(prepend):
323
+ filtered_dict[k[len(prepend) :]] = v
324
+ return cls.from_mapping(filtered_dict)
325
+
306
326
  @classmethod
307
327
  def ensure_model(cls: type[T], row: Union[T, Mapping[str, Any]]) -> T:
308
328
  if isinstance(row, cls):
@@ -357,7 +377,17 @@ class ModelBase:
357
377
  return query
358
378
 
359
379
  @classmethod
360
- async def select_cursor(
380
+ async def cursor_from(
381
+ cls: type[T],
382
+ connection: Connection,
383
+ query: Fragment,
384
+ prefetch: int = 1000,
385
+ ) -> AsyncGenerator[T, None]:
386
+ async for row in connection.cursor(*query, prefetch=prefetch):
387
+ yield cls.from_mapping(row)
388
+
389
+ @classmethod
390
+ def select_cursor(
361
391
  cls: type[T],
362
392
  connection: Connection,
363
393
  order_by: Union[FieldNames, str] = (),
@@ -365,11 +395,19 @@ class ModelBase:
365
395
  where: Where = (),
366
396
  prefetch: int = 1000,
367
397
  ) -> AsyncGenerator[T, None]:
368
- async for row in connection.cursor(
369
- *cls.select_sql(order_by=order_by, for_update=for_update, where=where),
398
+ return cls.cursor_from(
399
+ connection,
400
+ cls.select_sql(order_by=order_by, for_update=for_update, where=where),
370
401
  prefetch=prefetch,
371
- ):
372
- yield cls.from_mapping(row)
402
+ )
403
+
404
+ @classmethod
405
+ async def fetch_from(
406
+ cls: type[T],
407
+ connection_or_pool: Union[Connection, Pool],
408
+ query: Fragment,
409
+ ) -> list[T]:
410
+ return [cls.from_mapping(row) for row in await connection_or_pool.fetch(*query)]
373
411
 
374
412
  @classmethod
375
413
  async def select(
@@ -379,12 +417,10 @@ class ModelBase:
379
417
  for_update: bool = False,
380
418
  where: Where = (),
381
419
  ) -> list[T]:
382
- return [
383
- cls.from_mapping(row)
384
- for row in await connection_or_pool.fetch(
385
- *cls.select_sql(order_by=order_by, for_update=for_update, where=where)
386
- )
387
- ]
420
+ return await cls.fetch_from(
421
+ connection_or_pool,
422
+ cls.select_sql(order_by=order_by, for_update=for_update, where=where),
423
+ )
388
424
 
389
425
  @classmethod
390
426
  def create_sql(cls: type[T], **kwargs: Any) -> Fragment:
@@ -506,6 +542,37 @@ class ModelBase:
506
542
  ),
507
543
  )
508
544
 
545
+ @classmethod
546
+ def insert_multiple_executemany_chunk_sql(
547
+ cls: type[T], chunk_size: int
548
+ ) -> Fragment:
549
+ def generate() -> Fragment:
550
+ columns = len(cls.column_info())
551
+ values = ", ".join(
552
+ f"({', '.join(f'${i}' for i in chunk)})"
553
+ for chunk in chunked(range(1, columns * chunk_size + 1), columns)
554
+ )
555
+ return sql(
556
+ "INSERT INTO {table} ({fields}) VALUES {values}",
557
+ table=cls.table_name_sql(),
558
+ fields=sql.list(cls.field_names_sql()),
559
+ values=sql.literal(values),
560
+ ).flatten()
561
+
562
+ return cls._cached(
563
+ ("insert_multiple_executemany_chunk", chunk_size),
564
+ generate,
565
+ )
566
+
567
+ @classmethod
568
+ async def insert_multiple_executemany(
569
+ cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
570
+ ) -> None:
571
+ args = [r.field_values() for r in rows]
572
+ query = cls.insert_multiple_executemany_chunk_sql(1).query()[0]
573
+ if args:
574
+ await connection_or_pool.executemany(query, args)
575
+
509
576
  @classmethod
510
577
  async def insert_multiple_unnest(
511
578
  cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
@@ -527,11 +594,28 @@ class ModelBase:
527
594
  async def insert_multiple(
528
595
  cls: type[T], connection_or_pool: Union[Connection, Pool], rows: Iterable[T]
529
596
  ) -> str:
530
- if cls.array_safe_insert:
597
+ if cls.insert_multiple_mode == "executemany":
598
+ await cls.insert_multiple_executemany(connection_or_pool, rows)
599
+ return "INSERT"
600
+ elif cls.insert_multiple_mode == "array_safe":
531
601
  return await cls.insert_multiple_array_safe(connection_or_pool, rows)
532
602
  else:
533
603
  return await cls.insert_multiple_unnest(connection_or_pool, rows)
534
604
 
605
+ @classmethod
606
+ async def upsert_multiple_executemany(
607
+ cls: type[T],
608
+ connection_or_pool: Union[Connection, Pool],
609
+ rows: Iterable[T],
610
+ insert_only: FieldNamesSet = (),
611
+ ) -> None:
612
+ args = [r.field_values() for r in rows]
613
+ query = cls.upsert_sql(
614
+ cls.insert_multiple_executemany_chunk_sql(1), exclude=insert_only
615
+ ).query()[0]
616
+ if args:
617
+ await connection_or_pool.executemany(query, args)
618
+
535
619
  @classmethod
536
620
  async def upsert_multiple_unnest(
537
621
  cls: type[T],
@@ -566,7 +650,12 @@ class ModelBase:
566
650
  rows: Iterable[T],
567
651
  insert_only: FieldNamesSet = (),
568
652
  ) -> str:
569
- if cls.array_safe_insert:
653
+ if cls.insert_multiple_mode == "executemany":
654
+ await cls.upsert_multiple_executemany(
655
+ connection_or_pool, rows, insert_only=insert_only
656
+ )
657
+ return "INSERT"
658
+ elif cls.insert_multiple_mode == "array_safe":
570
659
  return await cls.upsert_multiple_array_safe(
571
660
  connection_or_pool, rows, insert_only=insert_only
572
661
  )
@@ -1,22 +1,16 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: sql-athame
3
- Version: 0.4.0a10
3
+ Version: 0.4.0a12
4
4
  Summary: Python tool for slicing and dicing SQL
5
- Home-page: https://github.com/bdowning/sql-athame
5
+ Project-URL: homepage, https://github.com/bdowning/sql-athame
6
+ Project-URL: repository, https://github.com/bdowning/sql-athame
7
+ Author-email: Brian Downing <bdowning@lavos.net>
6
8
  License: MIT
7
- Author: Brian Downing
8
- Author-email: bdowning@lavos.net
9
- Requires-Python: >=3.9,<4.0
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
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
16
- Provides-Extra: asyncpg
17
- Requires-Dist: asyncpg ; extra == "asyncpg"
9
+ License-File: LICENSE
10
+ Requires-Python: <4.0,>=3.9
18
11
  Requires-Dist: typing-extensions
19
- Project-URL: Repository, https://github.com/bdowning/sql-athame
12
+ Provides-Extra: asyncpg
13
+ Requires-Dist: asyncpg; extra == 'asyncpg'
20
14
  Description-Content-Type: text/markdown
21
15
 
22
16
  # sql-athame
@@ -404,5 +398,4 @@ TODO, for now read the [source](sql_athame/dataclasses.py).
404
398
  MIT.
405
399
 
406
400
  ---
407
- Copyright (c) 2019, 2020 Brian Downing
408
-
401
+ Copyright (c) 2019–2025 Brian Downing
@@ -1,11 +1,11 @@
1
1
  sql_athame/__init__.py,sha256=0GA9-xfnZ_gw7Vysx6t4iInQ2kGXos1EYwBI6Eb7auU,100
2
2
  sql_athame/base.py,sha256=FR7EmC0VkX1VRgvAutSEfYSWhlEYpoqS1Kqxp1jHp6Y,10293
3
- sql_athame/dataclasses.py,sha256=7qjAYP_dAD3hY_3txchBJfI5gURGmunOha6dDJdQjjw,25018
3
+ sql_athame/dataclasses.py,sha256=rqobdKMHEohVJknEj6Vv2UzihgOttaROEhnwuO6G4LQ,28155
4
4
  sql_athame/escape.py,sha256=kK101xXeFitlvuG-L_hvhdpgGJCtmRTprsn1yEfZKws,758
5
5
  sql_athame/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  sql_athame/sqlalchemy.py,sha256=aWopfPh3j71XwKmcN_VcHRNlhscI0Sckd4AiyGf8Tpw,1293
7
7
  sql_athame/types.py,sha256=FQ06l9Uc-vo57UrAarvnukILdV2gN1IaYUnHJ_bNYic,475
8
- sql_athame-0.4.0a10.dist-info/LICENSE,sha256=xqV29vPFqITcKifYrGPgVIBjq4fdmLSwY3gRUtDKafg,1076
9
- sql_athame-0.4.0a10.dist-info/METADATA,sha256=NU7Rhvp11MdZYD3YWtHJpauGOq8Nal8-cBa2EsMR0vE,12846
10
- sql_athame-0.4.0a10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
11
- sql_athame-0.4.0a10.dist-info/RECORD,,
8
+ sql_athame-0.4.0a12.dist-info/METADATA,sha256=kBYRXE0MC2W8jGbX3NqZpDvOeBKxqnu-vlAKCx5ka5o,12571
9
+ sql_athame-0.4.0a12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ sql_athame-0.4.0a12.dist-info/licenses/LICENSE,sha256=kPtZhuBL-A3tMrge4oz5bm_Di2M8KcA-sJY7abTEfZ4,1075
11
+ sql_athame-0.4.0a12.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019, 2020 Brian Downing
3
+ Copyright (c) 2019-2025 Brian Downing
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal