sqlalchemy-upsert-kit 0.1.1__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.
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from ._version import __version__
4
+ from ._version import __short_description__
5
+ from ._version import __license__
6
+ from ._version import __author__
7
+ from ._version import __author_email__
8
+ from ._version import __maintainer__
9
+ from ._version import __maintainer_email__
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python
2
+ # This file is automatically generated by pywf script
3
+ # based on the content in pyproject.toml file
4
+
5
+ __version__ = "0.1.1"
6
+ __short_description__ = "High-performance SQLAlchemy bulk upsert operations using temporary table staging - 20x faster than row-by-row approaches."
7
+ __license__ = "MIT"
8
+ __author__ = "Sanhe Hu"
9
+ __author_email__ = "husanhe@email.com"
10
+ __maintainer__ = "Sanhe Hu"
11
+ __maintainer_email__ = "husanhe@email.com"
12
+
13
+ # run this script to print out the version number
14
+ if __name__ == "__main__": # pragma: no cover
15
+ print(__version__)
@@ -0,0 +1,3 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .sqlite import api as sqlite
@@ -0,0 +1,13 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+
4
+ class UpsertTestError(Exception):
5
+ """
6
+ Custom exception raised during testing to simulate failures.
7
+
8
+ This exception is used exclusively for testing error handling and cleanup
9
+ behavior in upsert operations. It allows tests to inject failures at specific
10
+ points in the operation flow to verify proper rollback and cleanup.
11
+
12
+ :param message: Descriptive error message indicating where the failure occurred
13
+ """
@@ -0,0 +1,33 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from pathlib import Path
4
+
5
+ dir_here = Path(__file__).absolute().parent
6
+ dir_package = dir_here
7
+ PACKAGE_NAME = dir_package.name
8
+
9
+ dir_project_root = dir_package.parent
10
+
11
+ # ------------------------------------------------------------------------------
12
+ # Virtual Environment Related
13
+ # ------------------------------------------------------------------------------
14
+ dir_venv = dir_project_root / ".venv"
15
+ dir_venv_bin = dir_venv / "bin"
16
+
17
+ # virtualenv executable paths
18
+ bin_pytest = dir_venv_bin / "pytest"
19
+
20
+ # ------------------------------------------------------------------------------
21
+ # Test Related
22
+ # ------------------------------------------------------------------------------
23
+ dir_htmlcov = dir_project_root / "htmlcov"
24
+ path_cov_index_html = dir_htmlcov / "index.html"
25
+ dir_unit_test = dir_project_root / "tests"
26
+ dir_int_test = dir_project_root / "tests_int"
27
+ dir_load_test = dir_project_root / "tests_load"
28
+
29
+ # ------------------------------------------------------------------------------
30
+ # Doc Related
31
+ # ------------------------------------------------------------------------------
32
+ dir_docs_source = dir_project_root / "docs" / "source"
33
+ dir_docs_build_html = dir_project_root / "docs" / "build" / "html"
@@ -0,0 +1,34 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ High-Performance SQLAlchemy Bulk Operations Module for Sqlite
5
+
6
+ This module provides optimized bulk operations for SQLAlchemy using temporary tables.
7
+ These methods are specifically designed for large datasets and offer
8
+ superior performance compared to traditional row-by-row operations.
9
+
10
+
11
+ Implementation Notes
12
+ ------------------------------------------------------------------------------
13
+ **Temporary Table Strategy**:
14
+ All bulk operations use temporary tables as staging areas to achieve optimal
15
+ performance. Temporary tables are created with unique names to avoid conflicts
16
+ in concurrent environments. Comprehensive cleanup ensures no temporary tables
17
+ are left behind, even when errors occur.
18
+
19
+ **SQLite DDL Behavior**:
20
+ SQLite DDL operations (CREATE/DROP TABLE) are not transactional and commit
21
+ immediately. The cleanup logic accounts for this by using fresh connections
22
+ when necessary to avoid database locks during error scenarios.
23
+
24
+ **Testing Infrastructure**:
25
+ Functions include boolean parameters prefixed with ``_raise_on_`` that are
26
+ exclusively for testing purposes. These parameters inject controlled failures
27
+ at specific points in the operation flow to verify error handling and cleanup
28
+ behavior. These parameters should never be used in production code.
29
+ """
30
+
31
+ from .insert_or_ignore import insert_or_ignore
32
+ from .insert_or_replace import insert_or_replace
33
+
34
+ __all__ = ["insert_or_ignore", "insert_or_replace"]
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .insert_or_ignore import insert_or_ignore
4
+ from .insert_or_replace import insert_or_replace
5
+ from .insert_or_merge import insert_or_merge
@@ -0,0 +1,535 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ Abstract Base Class for High-Performance SQLAlchemy Bulk Upsert Operations
5
+
6
+ This module provides a Template Method pattern implementation for bulk upsert operations
7
+ using temporary table staging. The abstraction eliminates code duplication while allowing
8
+ different upsert strategies to implement their specific logic.
9
+
10
+ **Abstraction Strategy**:
11
+
12
+ The ``UpsertExecutor`` class abstracts the common workflow shared by all bulk upsert
13
+ operations while requiring subclasses to implement only their strategy-specific logic.
14
+ This design provides:
15
+
16
+ 1. **Code Reuse**: ~80% of upsert logic is common (transaction management, temp table
17
+ lifecycle, error handling, cleanup)
18
+ 2. **Strategy Isolation**: Each upsert strategy (INSERT OR IGNORE, INSERT OR REPLACE,
19
+ UPSERT/MERGE) implements only its core database operations
20
+ 3. **Consistent Behavior**: All strategies share the same transaction modes, error
21
+ handling, and cleanup patterns
22
+ 4. **Easy Extension**: Adding new upsert strategies requires minimal code
23
+
24
+ **Template Method Pattern**:
25
+
26
+ The execution flow follows this pattern::
27
+
28
+ UpsertExecutor.run()
29
+ ├── Transaction Management (auto vs user-managed)
30
+ ├── UpsertExecutor.execute_operation()
31
+ │ ├── Create temporary staging table
32
+ │ ├── Bulk load candidate data into temp table
33
+ │ ├── Subclass.apply_strategy() ← **Strategy-specific logic**
34
+ │ └── Cleanup temporary table
35
+ └── Error handling and rollback cleanup
36
+
37
+ **Supported Strategies**:
38
+
39
+ - **INSERT OR IGNORE**: Inserts only new records, ignores conflicts
40
+ - **INSERT OR REPLACE**: Replaces entire conflicting records with new data
41
+ - **UPSERT/MERGE**: Updates specific fields of existing records, inserts new ones
42
+
43
+ **Transaction Management**:
44
+
45
+ All strategies support dual-mode operation:
46
+
47
+ - **Auto-managed**: Function creates and manages its own transaction
48
+ - **User-managed**: Function operates within caller's existing transaction
49
+
50
+ **Database Compatibility**:
51
+
52
+ This module is designed for SQLite but the abstraction pattern can be extended
53
+ to other database systems by implementing database-specific executors.
54
+
55
+ **Implementation Notes**:
56
+
57
+ - Uses dataclasses for clean state management (eliminates nonlocal variables)
58
+ - Cached properties for efficient mode detection
59
+ - Comprehensive error handling with proper cleanup in all scenarios
60
+ - Testing infrastructure with controlled failure injection
61
+ """
62
+
63
+ import typing as T
64
+ import abc
65
+
66
+ import sqlalchemy as sa
67
+
68
+ from ..exc import UpsertTestError
69
+ from ..utils import get_pk_name, get_temp_table_name, clone_temp_table
70
+
71
+ import dataclasses
72
+ from functools import cached_property
73
+
74
+
75
+ @dataclasses.dataclass
76
+ class UpsertExecutor(abc.ABC):
77
+ """
78
+ Abstract base class for high-performance bulk upsert operations using temporary tables.
79
+
80
+ This class implements the Template Method pattern, providing a common framework
81
+ for all upsert strategies while requiring subclasses to implement only their
82
+ strategy-specific database operations.
83
+
84
+ **Template Method Flow**:
85
+
86
+ 1. **Setup**: Create temporary table and validate parameters
87
+ 2. **Data Loading**: Bulk insert candidate records into staging table
88
+ 3. **Strategy Execution**: Subclass implements specific upsert logic
89
+ 4. **Cleanup**: Remove temporary resources and handle errors
90
+
91
+ **Subclass Requirements**:
92
+
93
+ Subclasses must implement :meth:`apply_strategy` to define their specific
94
+ upsert behavior (INSERT OR IGNORE, INSERT OR REPLACE, UPSERT/MERGE, etc.).
95
+
96
+ **State Management**:
97
+
98
+ All operation state is managed through dataclass fields, eliminating the need
99
+ for nonlocal variables and providing clean, testable state tracking.
100
+
101
+ **Transaction Modes**:
102
+
103
+ - **Auto-managed**: ``conn=None, trans=None`` - Creates own transaction
104
+ - **User-managed**: ``conn=Connection, trans=Transaction`` - Uses caller's transaction
105
+
106
+ **Example Usage**::
107
+
108
+ @dataclasses.dataclass
109
+ class MyUpsertExecutor(UpsertExecutor):
110
+ def apply_strategy(self, conn, trans):
111
+ # Implement specific upsert logic here
112
+ pass
113
+
114
+ executor = MyUpsertExecutor.new(engine, table, data)
115
+ ignored, inserted = executor.run()
116
+
117
+ :param engine: SQLAlchemy engine for database connection
118
+ :param table: Target table for upsert operation
119
+ :param values: Records to insert or replace.
120
+ Must include primary key values for conflict detection.
121
+ :param metadata: Optional metadata instance for temporary table isolation.
122
+ If None, a new MetaData instance is created for clean separation.
123
+ :param temp_table_name: Optional custom name for temporary table.
124
+ If None, generates unique name with timestamp to avoid conflicts.
125
+ :param conn: Optional database connection for user-managed transaction mode.
126
+ Must be provided together with ``trans`` parameter.
127
+ :param trans: Optional transaction for user-managed transaction mode.
128
+ Must be provided together with ``conn`` parameter.
129
+ :param _raise_on_temp_table_create: **Testing only** - Simulate temp table creation failure
130
+ :param _raise_on_temp_data_insert: **Testing only** - Simulate temp data insertion failure
131
+ :param _raise_on_target_delete: **Testing only** - Simulate target deletion failure
132
+ :param _raise_on_target_insert: **Testing only** - Simulate target insertion failure
133
+ :param _raise_on_temp_table_drop: **Testing only** - Simulate temp table cleanup failure
134
+ :param _ignored_rows: Number of records ignored during operation (INSERT OR IGNORE).
135
+ :param _replaced_rows: Number of records replaced during operation (INSERT OR REPLACE).
136
+ :param _updated_rows: Number of records updated during operation (UPSERT/MERGE).
137
+ :param _inserted_rows: Number of new records inserted during operation
138
+ :param _temp_table: Temporary staging table created during operation.
139
+ :param _temp_table_created: Flag tracking whether temporary table was successfully created.
140
+ """
141
+
142
+ # --- Input Parameters ---
143
+ engine: sa.Engine = dataclasses.field()
144
+ table: sa.Table = dataclasses.field()
145
+ values: list[dict[str, T.Any]] = dataclasses.field()
146
+ metadata: T.Optional[sa.MetaData] = dataclasses.field()
147
+ temp_table_name: T.Optional[str] = dataclasses.field()
148
+ conn: T.Optional[sa.Connection] = dataclasses.field()
149
+ trans: T.Optional[sa.Transaction] = dataclasses.field()
150
+ columns: T.Optional[list[str]] = dataclasses.field()
151
+ # --- Testing Parameters (Testing Only) ---
152
+ _raise_on_temp_table_create: bool = dataclasses.field()
153
+ _raise_on_temp_data_insert: bool = dataclasses.field()
154
+ _raise_on_target_delete: bool = dataclasses.field()
155
+ _raise_on_target_insert: bool = dataclasses.field()
156
+ _raise_on_temp_table_drop: bool = dataclasses.field()
157
+ _raise_on_merge_update: bool = dataclasses.field()
158
+ # --- Operation Results ---
159
+ _ignored_rows: int = dataclasses.field(default=0)
160
+ _replaced_rows: int = dataclasses.field(default=0)
161
+ _updated_rows: int = dataclasses.field(default=0)
162
+ _inserted_rows: int = dataclasses.field(default=0)
163
+ # --- Internal State ---
164
+ _temp_table: sa.Table = dataclasses.field(init=False)
165
+ _temp_table_created: bool = dataclasses.field(default=False)
166
+
167
+ @classmethod
168
+ def new(
169
+ cls,
170
+ engine: sa.Engine,
171
+ table: sa.Table,
172
+ values: list[dict[str, T.Any]],
173
+ metadata: T.Optional[sa.MetaData] = None,
174
+ temp_table_name: T.Optional[str] = None,
175
+ conn: T.Optional[sa.Connection] = None,
176
+ trans: T.Optional[sa.Transaction] = None,
177
+ columns: T.Optional[list[str]] = None,
178
+ _raise_on_temp_table_create: bool = False,
179
+ _raise_on_temp_data_insert: bool = False,
180
+ _raise_on_target_delete: bool = False,
181
+ _raise_on_target_insert: bool = False,
182
+ _raise_on_temp_table_drop: bool = False,
183
+ _raise_on_merge_update: bool = False,
184
+ ):
185
+ """
186
+ Factory method to create UpsertExecutor instances with sensible defaults.
187
+
188
+ This method provides default values for optional parameters and creates
189
+ a properly configured executor instance ready for operation.
190
+ """
191
+ # Validate required parameters
192
+ if metadata is None:
193
+ metadata = sa.MetaData()
194
+ if temp_table_name is None:
195
+ temp_table_name = get_temp_table_name(original_table_name=table.name)
196
+ return cls(
197
+ engine=engine,
198
+ table=table,
199
+ values=values,
200
+ metadata=metadata,
201
+ temp_table_name=temp_table_name,
202
+ conn=conn,
203
+ trans=trans,
204
+ columns=columns,
205
+ _raise_on_temp_table_create=_raise_on_temp_table_create,
206
+ _raise_on_temp_data_insert=_raise_on_temp_data_insert,
207
+ _raise_on_target_delete=_raise_on_target_delete,
208
+ _raise_on_target_insert=_raise_on_target_insert,
209
+ _raise_on_temp_table_drop=_raise_on_temp_table_drop,
210
+ _raise_on_merge_update=_raise_on_merge_update,
211
+ )
212
+
213
+ @cached_property
214
+ def user_managed(self) -> bool:
215
+ """
216
+ Check if executor is operating in user-managed transaction mode.
217
+ """
218
+ return (self.conn is not None) and (self.trans is not None)
219
+
220
+ @cached_property
221
+ def auto_managed(self) -> bool:
222
+ """
223
+ Check if executor is operating in auto-managed transaction mode.
224
+ """
225
+ return (self.conn is None) and (self.trans is None)
226
+
227
+ def __post_init__(self):
228
+ """
229
+ Validate transaction mode parameters after dataclass initialization.
230
+ """
231
+ if not (self.user_managed or self.auto_managed): # pragma: no cover
232
+ raise ValueError(
233
+ "Either both conn and trans must be provided (user-managed mode), "
234
+ "or both must be None (auto-managed mode)"
235
+ )
236
+
237
+ @cached_property
238
+ def pk_name(self) -> str:
239
+ """
240
+ Get the primary key column name from the target table.
241
+ """
242
+ return get_pk_name(self.table)
243
+
244
+ def clone_temp_table(self):
245
+ """
246
+ Create a temporary table with the same structure as the target table.
247
+
248
+ This method clones the target table's structure into a temporary table
249
+ that will be used for staging data during the upsert operation.
250
+ """
251
+ self._temp_table = clone_temp_table(
252
+ original_table=self.table,
253
+ metadata=self.metadata,
254
+ temp_table_name=self.temp_table_name,
255
+ )
256
+
257
+ def create_temp_table(self, conn: sa.Connection):
258
+ """
259
+ Create the temporary staging table for bulk data operations.
260
+
261
+ :param conn: Database connection to use for table creation
262
+ """
263
+ if self._raise_on_temp_table_create: # Testing flag
264
+ raise UpsertTestError("error on temp table creation")
265
+ self._temp_table.create(conn)
266
+ self._temp_table_created = True
267
+
268
+ def insert_temp_data(self, conn: sa.Connection):
269
+ """
270
+ Bulk insert candidate records into temporary staging table.
271
+
272
+ This operation loads all candidate records into the staging area,
273
+ enabling efficient bulk processing through JOIN operations.
274
+
275
+ :param conn: Database connection to use for data insertion
276
+ """
277
+ if self._raise_on_temp_data_insert: # Testing flag
278
+ raise UpsertTestError("error on temp data insertion")
279
+ conn.execute(self._temp_table.insert(), self.values)
280
+
281
+ def cleanup_temp_table_on_success(self, conn: sa.Connection):
282
+ """
283
+ Clean up temporary table after successful operation.
284
+
285
+ This method removes the temporary table and cleans up metadata
286
+ within the same connection context, ensuring proper cleanup
287
+ in the success path.
288
+
289
+ :param conn: Database connection to use for cleanup
290
+ """
291
+ if self._temp_table_created:
292
+ if self._raise_on_temp_table_drop: # Testing flag
293
+ raise UpsertTestError("error on temp table cleanup")
294
+ # Normal cleanup - drop temp table within the same connection
295
+ self._temp_table.drop(conn)
296
+ self.metadata.remove(self._temp_table)
297
+ self._temp_table_created = False
298
+
299
+ def cleanup_temp_table_on_failure(self):
300
+ """
301
+ Clean up temporary table using a fresh connection after transaction failure.
302
+
303
+ This method is called when cleanup needs to happen outside the main
304
+ transaction context, typically in error scenarios. It uses a fresh
305
+ connection to avoid SQLite database lock issues that can occur when
306
+ the original transaction has been rolled back.
307
+
308
+ **Why Fresh Connection**:
309
+
310
+ SQLite DDL operations (CREATE/DROP TABLE) are not transactional and commit
311
+ immediately. When the main transaction rolls back, the temporary table may
312
+ still exist but the original connection may be locked. A fresh connection
313
+ ensures we can clean up properly.
314
+
315
+ **Error Handling**:
316
+
317
+ Cleanup failures are suppressed to avoid masking the original exception
318
+ that caused the operation to fail. This prevents cleanup issues from
319
+ hiding the root cause of problems.
320
+ """
321
+ if self._temp_table_created:
322
+ try:
323
+ # Use fresh connection to avoid database locks from rolled-back transactions
324
+ with self.engine.connect() as cleanup_conn:
325
+ self._temp_table.drop(cleanup_conn)
326
+ cleanup_conn.commit()
327
+ self.metadata.remove(self._temp_table)
328
+ except Exception:
329
+ # Cleanup failures should not mask the original exception
330
+ # This can happen if temp table was already dropped or doesn't exist
331
+ try:
332
+ # Try to remove from metadata anyway to prevent resource leaks
333
+ self.metadata.remove(self._temp_table)
334
+ except Exception: # pragma: no cover
335
+ pass
336
+
337
+ @abc.abstractmethod
338
+ def apply_strategy(
339
+ self,
340
+ conn: sa.Connection,
341
+ trans: sa.Transaction,
342
+ ):
343
+ """
344
+ Apply the upsert strategy-specific logic.
345
+
346
+ This abstract method must be implemented by subclasses to define their
347
+ specific upsert behavior. The method is called after the temporary table
348
+ has been created and populated with candidate data.
349
+
350
+ **Implementation Requirements**:
351
+
352
+ Subclasses should implement the core database operations that define
353
+ their upsert strategy:
354
+
355
+ - **INSERT OR IGNORE**: Use LEFT JOIN to insert only non-conflicting records
356
+ - **INSERT OR REPLACE**: Delete conflicting records, then insert all records
357
+ - **UPSERT/MERGE**: Update existing records, insert new ones
358
+
359
+ **State Management**:
360
+
361
+ Implementations should update the appropriate result counters:
362
+
363
+ - ``self._ignored_rows`` - Records ignored (INSERT OR IGNORE)
364
+ - ``self._replaced_rows`` - Records replaced (INSERT OR REPLACE)
365
+ - ``self._updated_rows`` - Records updated (UPSERT/MERGE)
366
+ - ``self._inserted_rows`` - New records inserted
367
+
368
+ **Error Handling**:
369
+
370
+ Implementations can use testing flags for controlled failure simulation:
371
+
372
+ - ``self._raise_on_target_delete`` - Simulate deletion failures
373
+ - ``self._raise_on_target_insert`` - Simulate insertion failures
374
+
375
+ :param conn: Database connection within active transaction
376
+ :param trans: Active transaction context
377
+
378
+ :raises UpsertTestError: When testing flags are enabled
379
+ :raises NotImplementedError: If subclass doesn't implement this method
380
+
381
+ **Example Implementation**::
382
+
383
+ def apply_strategy(self, conn, trans):
384
+ # INSERT OR IGNORE strategy
385
+ stmt = self.table.insert().from_select(
386
+ list(self._temp_table.columns.keys()),
387
+ sa.select(self._temp_table).select_from(
388
+ self._temp_table.outerjoin(self.table, ...)
389
+ ).where(self.table.c[self.pk_name].is_(None))
390
+ )
391
+ result = conn.execute(stmt)
392
+ self._inserted_rows = result.rowcount or 0
393
+ self._ignored_rows = len(self.values) - self._inserted_rows
394
+ """
395
+ raise NotImplementedError
396
+
397
+ def execute_operation(
398
+ self,
399
+ conn: sa.Connection,
400
+ trans: sa.Transaction,
401
+ ):
402
+ """
403
+ Execute the complete upsert operation within the provided transaction context.
404
+
405
+ This method implements the Template Method pattern, orchestrating the
406
+ common workflow while delegating strategy-specific logic to subclasses.
407
+ It operates within either a user-managed or auto-managed transaction
408
+ depending on how the executor was configured.
409
+
410
+ **Execution Flow**:
411
+
412
+ 1. **Create Staging**: Create temporary table for bulk data processing
413
+ 2. **Load Data**: Bulk insert all candidate records into staging area
414
+ 3. **Apply Strategy**: Execute subclass-specific upsert logic
415
+ 4. **Cleanup**: Remove temporary resources
416
+
417
+ **Error Handling**:
418
+
419
+ If any step fails, the method re-raises the original exception.
420
+ Cleanup of temporary tables is handled by the caller based on
421
+ transaction mode (auto vs user-managed).
422
+
423
+ **Performance Notes**:
424
+
425
+ The temporary table approach provides significant performance benefits
426
+ over row-by-row operations by enabling efficient bulk SQL operations
427
+ and leveraging database JOIN optimizations.
428
+
429
+ :param conn: Database connection within active transaction
430
+ :param trans: Active transaction context
431
+
432
+ :returns: Tuple of operation results (varies by strategy)
433
+
434
+ :raises UpsertTestError: When testing flags are enabled
435
+ :raises Exception: Any database or application errors during operation
436
+ """
437
+ try:
438
+ # Step 1: Create temporary staging table for bulk data processing
439
+ self.create_temp_table(conn)
440
+
441
+ # Step 2: Bulk load all candidate records into staging area
442
+ # This is much faster than individual row processing
443
+ self.insert_temp_data(conn)
444
+
445
+ # Step 3: Execute strategy-specific upsert logic
446
+ # Subclasses implement their specific database operations here
447
+ self.apply_strategy(conn, trans)
448
+
449
+ # Step 4: Clean up temporary table in normal success path
450
+ self.cleanup_temp_table_on_success(conn)
451
+
452
+ return self._ignored_rows, self._inserted_rows
453
+
454
+ except Exception as e:
455
+ # Handle testing flag for temp table cleanup errors
456
+ if self._temp_table_created and self._raise_on_temp_table_drop:
457
+ raise UpsertTestError("error on temp table cleanup")
458
+ # Re-raise original exception - cleanup handled by caller based on transaction mode
459
+ raise e
460
+
461
+ def run(self):
462
+ """
463
+ Execute the complete upsert operation with appropriate transaction management.
464
+
465
+ This is the main entry point for upsert operations. It handles both
466
+ auto-managed and user-managed transaction modes, ensuring proper
467
+ error handling and cleanup in all scenarios.
468
+
469
+ **Transaction Modes**:
470
+
471
+ - **Auto-managed** (``conn=None, trans=None``): Creates and manages
472
+ its own database transaction. Automatically commits on success
473
+ and rolls back on failure.
474
+
475
+ - **User-managed** (``conn=Connection, trans=Transaction``): Operates
476
+ within the caller's existing transaction. The caller is responsible
477
+ for committing or rolling back the transaction.
478
+
479
+ **Error Handling**:
480
+
481
+ In both modes, temporary table cleanup is handled appropriately:
482
+ - Auto-managed: Uses fresh connection after transaction rollback
483
+ - User-managed: Cleans up but preserves caller's transaction state
484
+
485
+ **Usage Examples**::
486
+
487
+ # Auto-managed transaction
488
+ executor = MyUpsertExecutor.new(engine, table, data)
489
+ result = executor.run()
490
+
491
+ # User-managed transaction
492
+ with engine.connect() as conn:
493
+ with conn.begin() as trans:
494
+ executor = MyUpsertExecutor.new(
495
+ engine, table, data, conn=conn, trans=trans
496
+ )
497
+ result = executor.run()
498
+ # Additional operations...
499
+ # trans.commit() handled by context manager
500
+
501
+ :returns: Tuple of operation results (varies by strategy):
502
+ - INSERT OR IGNORE: (_ignored_rows, _inserted_rows)
503
+ - INSERT OR REPLACE: (_replaced_rows, _inserted_rows)
504
+ - UPSERT/MERGE: (_updated_rows, _inserted_rows)
505
+
506
+ :raises ValueError: When transaction mode parameters are inconsistent
507
+ :raises UpsertTestError: When testing flags are enabled
508
+ :raises Exception: Any database or application errors during operation
509
+ """
510
+ self.clone_temp_table()
511
+
512
+ if self.user_managed:
513
+ # User-managed transaction mode: operate within caller's transaction context
514
+ try:
515
+ return self.execute_operation(self.conn, self.trans)
516
+ except Exception:
517
+ # Clean up temp table but don't manage transaction - caller is responsible
518
+ self.cleanup_temp_table_on_failure()
519
+ raise
520
+ elif self.auto_managed:
521
+ # Auto-managed transaction mode: create and manage our own transaction
522
+ try:
523
+ with self.engine.connect() as conn:
524
+ with conn.begin() as trans:
525
+ result = self.execute_operation(conn, trans)
526
+ # Transaction automatically committed on successful exit
527
+ return result
528
+ except Exception as e:
529
+ # Transaction automatically rolled back by context manager
530
+ # Clean up temp tables after all connections are properly closed
531
+ self.cleanup_temp_table_on_failure()
532
+ raise e
533
+ # should never reach here
534
+ else: # pragma: no cover
535
+ raise NotImplementedError