SQLDbWrpr 5.1.1__tar.gz → 5.2.0__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.
- {sqldbwrpr-5.1.1 → sqldbwrpr-5.2.0}/PKG-INFO +1 -1
- {sqldbwrpr-5.1.1 → sqldbwrpr-5.2.0}/pyproject.toml +1 -1
- {sqldbwrpr-5.1.1 → sqldbwrpr-5.2.0}/src/sqldbwrpr/sqldbwrpr.py +78 -8
- {sqldbwrpr-5.1.1 → sqldbwrpr-5.2.0}/LICENSE.txt +0 -0
- {sqldbwrpr-5.1.1 → sqldbwrpr-5.2.0}/README.md +0 -0
- {sqldbwrpr-5.1.1 → sqldbwrpr-5.2.0}/src/sqldbwrpr/__init__.py +0 -0
|
@@ -95,6 +95,7 @@ class SQLDbWrpr:
|
|
|
95
95
|
self.inline_indexes = True
|
|
96
96
|
self.msg_width = p_msg_width
|
|
97
97
|
self.non_char_fields = {}
|
|
98
|
+
self.owns_connection = True
|
|
98
99
|
self._password = p_password
|
|
99
100
|
self.re_create_db = p_recreate_db
|
|
100
101
|
self.silent = p_verbose
|
|
@@ -104,10 +105,16 @@ class SQLDbWrpr:
|
|
|
104
105
|
self.get_db_field_types()
|
|
105
106
|
self.db_port = p_db_port
|
|
106
107
|
|
|
108
|
+
def _execute_import_rows(self, p_table_name, p_header, p_rows, p_replace):
|
|
109
|
+
"""Execute one prepared import batch without managing its transaction."""
|
|
110
|
+
self.cur.executemany(self.build_insert_sql(p_table_name, p_header, p_replace), p_rows)
|
|
111
|
+
|
|
107
112
|
def close(self):
|
|
108
|
-
"""Close
|
|
109
|
-
if self.conn:
|
|
113
|
+
"""Close owned connections, or just this wrapper's cursor on a borrowed connection."""
|
|
114
|
+
if self.conn and self.owns_connection:
|
|
110
115
|
self.conn.close()
|
|
116
|
+
elif self.cur:
|
|
117
|
+
self.cur.close()
|
|
111
118
|
|
|
112
119
|
def build_column_sql(self, p_field_name, p_field_type, p_field_params, p_field_comment):
|
|
113
120
|
"""Build SQL for one column in a CREATE TABLE statement."""
|
|
@@ -762,6 +769,7 @@ class SQLDbWrpr:
|
|
|
762
769
|
p_vol_type="Multi",
|
|
763
770
|
p_verbose=False,
|
|
764
771
|
p_replace=False,
|
|
772
|
+
p_manage_transaction=True,
|
|
765
773
|
):
|
|
766
774
|
"""Import CSV rows into a database table.
|
|
767
775
|
|
|
@@ -790,7 +798,11 @@ class SQLDbWrpr:
|
|
|
790
798
|
- p_replace = False
|
|
791
799
|
- False - INSERT into database
|
|
792
800
|
- True - REPLACE into database
|
|
801
|
+
- p_manage_transaction = True
|
|
802
|
+
- False leaves commits and rollbacks to the caller and propagates database errors.
|
|
793
803
|
"""
|
|
804
|
+
if not p_manage_transaction and self.conn.autocommit:
|
|
805
|
+
raise ValueError("Caller-managed imports require autocommit=False")
|
|
794
806
|
|
|
795
807
|
def import_volume(p_csv_db, p_header, p_verbose):
|
|
796
808
|
"""Prepare and write one in-memory CSV volume to the active table."""
|
|
@@ -905,6 +917,12 @@ class SQLDbWrpr:
|
|
|
905
917
|
|
|
906
918
|
def write_to_table(p_csv_db):
|
|
907
919
|
"""Write prepared rows to the destination table."""
|
|
920
|
+
if not p_manage_transaction:
|
|
921
|
+
for start in range(1, len(p_csv_db), self.batch_size):
|
|
922
|
+
self._execute_import_rows(
|
|
923
|
+
p_table_name, header, p_csv_db[start : start + self.batch_size], p_replace
|
|
924
|
+
)
|
|
925
|
+
return
|
|
908
926
|
i = 1
|
|
909
927
|
j = 0 # In case batch size is more than all records
|
|
910
928
|
list_len = len(p_csv_db)
|
|
@@ -1032,6 +1050,7 @@ class SQLDbWrpr:
|
|
|
1032
1050
|
p_insert_header=False,
|
|
1033
1051
|
p_verbose=False,
|
|
1034
1052
|
p_debug=False,
|
|
1053
|
+
p_manage_transaction=True,
|
|
1035
1054
|
):
|
|
1036
1055
|
"""Split CSV rows into one or more destination table imports.
|
|
1037
1056
|
|
|
@@ -1194,6 +1213,7 @@ class SQLDbWrpr:
|
|
|
1194
1213
|
p_header=new_header,
|
|
1195
1214
|
p_verbose=p_verbose,
|
|
1196
1215
|
p_replace=p_split_struct[seq]["Replace"],
|
|
1216
|
+
p_manage_transaction=p_manage_transaction,
|
|
1197
1217
|
)
|
|
1198
1218
|
|
|
1199
1219
|
@classmethod
|
|
@@ -1570,9 +1590,19 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1570
1590
|
p_verbose=False,
|
|
1571
1591
|
p_db_port="5432",
|
|
1572
1592
|
p_maintenance_db="postgres",
|
|
1593
|
+
p_connection=None,
|
|
1573
1594
|
**kwargs,
|
|
1574
1595
|
):
|
|
1575
|
-
"""Create a PostgreSQL wrapper
|
|
1596
|
+
"""Create a PostgreSQL wrapper, optionally borrowing a caller-owned DB-API connection.
|
|
1597
|
+
|
|
1598
|
+
A borrowed connection is never closed or reconfigured. Use
|
|
1599
|
+
p_manage_transaction=False on imports to keep its transaction caller-owned.
|
|
1600
|
+
Database recreation is unavailable on borrowed connections.
|
|
1601
|
+
"""
|
|
1602
|
+
if p_connection is not None and p_recreate_db:
|
|
1603
|
+
raise ValueError("Cannot recreate a database on a borrowed connection")
|
|
1604
|
+
if p_batch_size < 1:
|
|
1605
|
+
raise ValueError("Batch size must be positive")
|
|
1576
1606
|
super().__init__(
|
|
1577
1607
|
p_host_name=p_host_name,
|
|
1578
1608
|
p_user_name=p_user_name,
|
|
@@ -1594,14 +1624,27 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1594
1624
|
self.maintenance_db = p_maintenance_db
|
|
1595
1625
|
self._connection_kwargs = kwargs
|
|
1596
1626
|
connect_db = self.maintenance_db if self.re_create_db else self.db_name
|
|
1597
|
-
self.
|
|
1598
|
-
self.conn
|
|
1627
|
+
self.owns_connection = p_connection is None
|
|
1628
|
+
self.conn = self._connect(connect_db) if self.owns_connection else p_connection
|
|
1629
|
+
if self.owns_connection:
|
|
1630
|
+
self.conn.autocommit = True
|
|
1599
1631
|
self.cur = self.conn.cursor()
|
|
1600
1632
|
if self.re_create_db:
|
|
1601
1633
|
if self.create_db():
|
|
1602
1634
|
self.create_tables()
|
|
1603
1635
|
self.success = True
|
|
1604
1636
|
|
|
1637
|
+
def _execute_import_rows(self, p_table_name, p_header, p_rows, p_replace):
|
|
1638
|
+
"""Use batched VALUES for psycopg2; retain native executemany for other drivers."""
|
|
1639
|
+
if type(self.conn).__module__.split(".")[0] == "psycopg2":
|
|
1640
|
+
# Borrowing this driver implies the host already has psycopg2 installed.
|
|
1641
|
+
from psycopg2.extras import execute_values
|
|
1642
|
+
|
|
1643
|
+
statement = self.build_insert_sql(p_table_name, p_header, p_replace, p_multi_row=True)
|
|
1644
|
+
execute_values(self.cur, statement, p_rows, page_size=self.batch_size)
|
|
1645
|
+
else:
|
|
1646
|
+
super()._execute_import_rows(p_table_name, p_header, p_rows, p_replace)
|
|
1647
|
+
|
|
1605
1648
|
def build_column_sql(self, p_field_name, p_field_type, p_field_params, p_field_comment):
|
|
1606
1649
|
"""Build PostgreSQL SQL for one column in a CREATE TABLE statement."""
|
|
1607
1650
|
sql_str = f"{self.quote_identifier(p_field_name)} {self.render_field_type(p_field_type, p_field_params)}"
|
|
@@ -1624,12 +1667,13 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1624
1667
|
",".join(index_fields),
|
|
1625
1668
|
)
|
|
1626
1669
|
|
|
1627
|
-
def build_insert_sql(self, p_table_name, p_header, p_replace=False):
|
|
1670
|
+
def build_insert_sql(self, p_table_name, p_header, p_replace=False, p_multi_row=False):
|
|
1628
1671
|
"""Build a PostgreSQL INSERT or upsert statement."""
|
|
1629
|
-
|
|
1672
|
+
values_sql = "%s" if p_multi_row else f"({','.join(self.param_placeholder() for _ in p_header)})"
|
|
1673
|
+
insert_sql = "INSERT INTO {} ({}) VALUES {}".format(
|
|
1630
1674
|
self.quote_identifier(p_table_name),
|
|
1631
1675
|
",".join([self.quote_identifier(str(x)) for x in p_header]),
|
|
1632
|
-
|
|
1676
|
+
values_sql,
|
|
1633
1677
|
)
|
|
1634
1678
|
if not p_replace:
|
|
1635
1679
|
return insert_sql
|
|
@@ -1703,6 +1747,32 @@ class PostgreSQL(SQLDbWrpr):
|
|
|
1703
1747
|
return f"DECIMAL({str(p_field_type[1])}, {str(p_field_type[2])})"
|
|
1704
1748
|
return type_map.get(field_type, field_type.upper())
|
|
1705
1749
|
|
|
1750
|
+
def update_rows(self, p_table_name, p_header, p_rows, p_key="id"):
|
|
1751
|
+
"""Update supplied columns in bounded batches; the caller owns commit and rollback.
|
|
1752
|
+
|
|
1753
|
+
p_header must contain p_key and at least one update column. Unlike an
|
|
1754
|
+
upsert, this never creates missing records or supplies defaults.
|
|
1755
|
+
"""
|
|
1756
|
+
if p_key not in p_header or len(set(p_header)) != len(p_header) or len(p_header) < 2:
|
|
1757
|
+
raise ValueError("Updates require a unique header containing the key and update columns")
|
|
1758
|
+
if self.conn.autocommit:
|
|
1759
|
+
raise ValueError("Caller-managed updates require autocommit=False")
|
|
1760
|
+
fields = [field for field in p_header if field != p_key]
|
|
1761
|
+
indexes = [p_header.index(field) for field in [*fields, p_key]]
|
|
1762
|
+
assignments = ",".join(f"{self.quote_identifier(field)} = %s" for field in fields)
|
|
1763
|
+
statement = (
|
|
1764
|
+
f"UPDATE {self.quote_identifier(p_table_name)} SET {assignments} "
|
|
1765
|
+
f"WHERE {self.quote_identifier(p_key)} = %s"
|
|
1766
|
+
)
|
|
1767
|
+
for start in range(0, len(p_rows), self.batch_size):
|
|
1768
|
+
rows = [tuple(row[index] for index in indexes) for row in p_rows[start : start + self.batch_size]]
|
|
1769
|
+
if type(self.conn).__module__.split(".")[0] == "psycopg2":
|
|
1770
|
+
from psycopg2.extras import execute_batch
|
|
1771
|
+
|
|
1772
|
+
execute_batch(self.cur, statement, rows, page_size=self.batch_size)
|
|
1773
|
+
else:
|
|
1774
|
+
self.cur.executemany(statement, rows)
|
|
1775
|
+
|
|
1706
1776
|
def _connect(self, p_db_name):
|
|
1707
1777
|
"""Open a PostgreSQL connection."""
|
|
1708
1778
|
return psycopg.connect(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|