SQLDbWrpr 5.1.1__py3-none-any.whl → 5.2.0__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.
sqldbwrpr/sqldbwrpr.py CHANGED
@@ -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 the active database connection."""
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 and optionally recreate the target database."""
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.conn = self._connect(connect_db)
1598
- self.conn.autocommit = True
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
- insert_sql = "INSERT INTO {} ({}) VALUES ({})".format(
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
- ",".join([self.param_placeholder() for x in range(len(p_header))]),
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SQLDbWrpr
3
- Version: 5.1.1
3
+ Version: 5.2.0
4
4
  Summary: Wrapper for importing CSV and Text files into MySQL and Postgress
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE.txt
@@ -0,0 +1,6 @@
1
+ sqldbwrpr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sqldbwrpr/sqldbwrpr.py,sha256=HHAY03xzBkXgO-OpY44-eyWMLTPzFOnOy07M3quTT1E,82330
3
+ sqldbwrpr-5.2.0.dist-info/METADATA,sha256=xYVRFe187se6z_jVXfCW8UvRuK_rZS3xPeXK3YlV39U,19594
4
+ sqldbwrpr-5.2.0.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
5
+ sqldbwrpr-5.2.0.dist-info/licenses/LICENSE.txt,sha256=IA9VQhUiFRCmo0yWTjYyjM37Ldm-rjBno-D0AW12UBs,1149
6
+ sqldbwrpr-5.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.4.0
2
+ Generator: poetry-core 2.5.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,6 +0,0 @@
1
- sqldbwrpr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sqldbwrpr/sqldbwrpr.py,sha256=9jdMaA04m4SOkkB3A7D5J4NuKGCQh4Pd4I1wTB-wIPQ,78344
3
- sqldbwrpr-5.1.1.dist-info/METADATA,sha256=u2CdXkWXep1J9A4Pch1gAabkN7aXAfbm5fe9UztK6LM,19594
4
- sqldbwrpr-5.1.1.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
5
- sqldbwrpr-5.1.1.dist-info/licenses/LICENSE.txt,sha256=IA9VQhUiFRCmo0yWTjYyjM37Ldm-rjBno-D0AW12UBs,1149
6
- sqldbwrpr-5.1.1.dist-info/RECORD,,