velocity-python 0.0.218__tar.gz → 0.0.220__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.
- {velocity_python-0.0.218/src/velocity_python.egg-info → velocity_python-0.0.220}/PKG-INFO +1 -1
- {velocity_python-0.0.218 → velocity_python-0.0.220}/pyproject.toml +1 -1
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/__init__.py +1 -1
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/decorators.py +12 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/result.py +11 -1
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/row.py +140 -64
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/sequence.py +6 -5
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/table.py +40 -87
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/sql.py +48 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/sql.py +91 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_row_comprehensive.py +6 -9
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_row_get_missing_column.py +16 -4
- {velocity_python-0.0.218 → velocity_python-0.0.220/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity_python.egg-info/SOURCES.txt +2 -0
- velocity_python-0.0.220/tests/test_payment_braintree_adapter.py +77 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_payment_profiles.py +30 -1
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_payment_router.py +47 -1
- velocity_python-0.0.220/tests/test_payment_stripe_adapter.py +186 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/LICENSE +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/README.md +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/setup.cfg +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/tests/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/logging.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/profiles.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/router.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_decorators.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_table_alter.py +0 -0
- {velocity_python-0.0.218 → velocity_python-0.0.220}/tests/test_where_clause_validation.py +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import time
|
|
2
3
|
import random
|
|
3
4
|
from functools import wraps
|
|
4
5
|
from velocity.db import exceptions
|
|
5
6
|
|
|
7
|
+
logger = logging.getLogger("velocity.db")
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
_PRIMARY_KEY_PATTERNS = (
|
|
8
11
|
"primary key",
|
|
@@ -108,6 +111,15 @@ def return_default(
|
|
|
108
111
|
except func.exceptions as e:
|
|
109
112
|
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
110
113
|
|
|
114
|
+
# Log the swallowed exception so silent failures are visible.
|
|
115
|
+
logger.warning(
|
|
116
|
+
"@return_default swallowed %s in %s.%s: %s",
|
|
117
|
+
e.__class__.__name__,
|
|
118
|
+
func.__module__,
|
|
119
|
+
getattr(func, "__qualname__", func.__name__),
|
|
120
|
+
e,
|
|
121
|
+
)
|
|
122
|
+
|
|
111
123
|
# Capture swallowed exceptions for upstream diagnostics.
|
|
112
124
|
# This decorator intentionally returns a default value instead of
|
|
113
125
|
# raising, but consumers (e.g. API handlers) may still want to
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from velocity.misc.format import to_json
|
|
2
3
|
|
|
3
4
|
|
|
@@ -279,13 +280,22 @@ class Result:
|
|
|
279
280
|
self.transform = lambda row: to_json(dict(zip(self.headers, row)))
|
|
280
281
|
return self
|
|
281
282
|
|
|
282
|
-
def
|
|
283
|
+
def as_pairs(self):
|
|
283
284
|
"""
|
|
284
285
|
Transform each row into a list of (column_name, value) pairs.
|
|
285
286
|
"""
|
|
286
287
|
self.transform = lambda row: list(zip(self.headers, row))
|
|
287
288
|
return self
|
|
288
289
|
|
|
290
|
+
def as_named_tuple(self):
|
|
291
|
+
"""Deprecated: use as_pairs() instead."""
|
|
292
|
+
warnings.warn(
|
|
293
|
+
"Result.as_named_tuple() is deprecated, use Result.as_pairs() instead.",
|
|
294
|
+
DeprecationWarning,
|
|
295
|
+
stacklevel=2,
|
|
296
|
+
)
|
|
297
|
+
return self.as_pairs()
|
|
298
|
+
|
|
289
299
|
def as_list(self):
|
|
290
300
|
"""
|
|
291
301
|
Transform each row into a list of values.
|
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import pprint
|
|
2
|
+
import warnings
|
|
3
|
+
from collections.abc import MutableMapping
|
|
2
4
|
from velocity.db.exceptions import DbColumnMissingError
|
|
3
5
|
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
# Attributes that live on the Row instance itself and must never be
|
|
8
|
+
# intercepted by __getattr__ / __setattr__.
|
|
9
|
+
_INTERNAL_ATTRS = frozenset({
|
|
10
|
+
"table", "pk", "_cache", "_column_set",
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Row(MutableMapping):
|
|
6
15
|
"""
|
|
7
|
-
Represents a single row in a given table, identified by a primary key
|
|
16
|
+
Represents a single row in a given table, identified by a primary key.
|
|
17
|
+
|
|
18
|
+
Acts as a ``MutableMapping`` so that standard dict idioms work:
|
|
19
|
+
``dict(row)``, ``{**row}``, ``for k in row:``, ``json.dumps(row)``.
|
|
20
|
+
|
|
21
|
+
Data is fetched from the database **once** on first access and cached
|
|
22
|
+
locally. Call ``row.refresh()`` to re-fetch. Writes are still
|
|
23
|
+
write-through (immediate UPDATE) and also update the local cache.
|
|
8
24
|
"""
|
|
9
25
|
|
|
10
26
|
def __init__(self, table, key, lock=None):
|
|
11
27
|
if isinstance(table, str):
|
|
12
28
|
raise Exception("Table parameter must be a `table` instance.")
|
|
13
|
-
self
|
|
29
|
+
object.__setattr__(self, "table", table)
|
|
14
30
|
|
|
15
31
|
if isinstance(key, (dict, Row)):
|
|
16
32
|
pk = {}
|
|
@@ -18,38 +34,68 @@ class Row:
|
|
|
18
34
|
for k in self.key_cols:
|
|
19
35
|
pk[k] = key[k]
|
|
20
36
|
except KeyError:
|
|
21
|
-
pk = key
|
|
37
|
+
pk = dict(key) if isinstance(key, Row) else key
|
|
22
38
|
else:
|
|
23
39
|
pk = {self.key_cols[0]: key}
|
|
24
40
|
|
|
25
|
-
self
|
|
26
|
-
self
|
|
41
|
+
object.__setattr__(self, "pk", pk)
|
|
42
|
+
object.__setattr__(self, "_cache", None)
|
|
43
|
+
object.__setattr__(self, "_column_set", None)
|
|
27
44
|
if lock:
|
|
28
45
|
self.lock()
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Cache management
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def _ensure_cache(self):
|
|
52
|
+
"""Populate the local cache from the database if not yet loaded."""
|
|
53
|
+
if self._cache is None:
|
|
54
|
+
data = self.table.select(where=self.pk).as_dict().one()
|
|
55
|
+
object.__setattr__(self, "_cache", data or {})
|
|
56
|
+
return self._cache
|
|
57
|
+
|
|
58
|
+
def _column_names_lower(self):
|
|
59
|
+
"""Return a frozenset of lowered column names (cached)."""
|
|
60
|
+
if self._column_set is None:
|
|
61
|
+
cols = frozenset(k.lower() for k in self._ensure_cache())
|
|
62
|
+
object.__setattr__(self, "_column_set", cols)
|
|
63
|
+
return self._column_set
|
|
64
|
+
|
|
65
|
+
def refresh(self):
|
|
66
|
+
"""Re-fetch the row data from the database, clearing the local cache."""
|
|
67
|
+
object.__setattr__(self, "_cache", None)
|
|
68
|
+
object.__setattr__(self, "_column_set", None)
|
|
69
|
+
self._ensure_cache()
|
|
70
|
+
return self
|
|
32
71
|
|
|
33
|
-
def
|
|
34
|
-
|
|
72
|
+
def invalidate(self):
|
|
73
|
+
"""Clear the local cache so the next access re-fetches from the database."""
|
|
74
|
+
object.__setattr__(self, "_cache", None)
|
|
75
|
+
object.__setattr__(self, "_column_set", None)
|
|
76
|
+
return self
|
|
35
77
|
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
# MutableMapping protocol
|
|
80
|
+
# ------------------------------------------------------------------
|
|
38
81
|
|
|
39
82
|
def __getitem__(self, key):
|
|
40
83
|
if key in self.pk:
|
|
41
84
|
return self.pk[key]
|
|
85
|
+
cache = self._ensure_cache()
|
|
86
|
+
if key in cache:
|
|
87
|
+
return cache[key]
|
|
88
|
+
# Fall back to a direct DB fetch for columns not in the initial SELECT
|
|
42
89
|
return self.table.get_value(key, self.pk)
|
|
43
90
|
|
|
44
91
|
def __setitem__(self, key, val):
|
|
45
92
|
if key in self.pk:
|
|
46
93
|
raise Exception("Cannot update a primary key.")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
self.table.update({key: val}, pk=self.pk)
|
|
94
|
+
self.table.update_or_insert({key: val}, pk=self.pk)
|
|
95
|
+
# Update local cache
|
|
96
|
+
if self._cache is not None:
|
|
97
|
+
self._cache[key] = val
|
|
98
|
+
object.__setattr__(self, "_column_set", None)
|
|
53
99
|
|
|
54
100
|
def __delitem__(self, key):
|
|
55
101
|
if key in self.pk:
|
|
@@ -58,37 +104,91 @@ class Row:
|
|
|
58
104
|
return
|
|
59
105
|
self[key] = None
|
|
60
106
|
|
|
107
|
+
def __iter__(self):
|
|
108
|
+
return iter(self._ensure_cache())
|
|
109
|
+
|
|
110
|
+
def __len__(self):
|
|
111
|
+
return len(self._ensure_cache())
|
|
112
|
+
|
|
61
113
|
def __contains__(self, key):
|
|
62
|
-
return key.lower() in
|
|
114
|
+
return key.lower() in self._column_names_lower()
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
# Attribute-style access (row.name instead of row['name'])
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def __getattr__(self, key):
|
|
121
|
+
if key.startswith("_") or key in _INTERNAL_ATTRS:
|
|
122
|
+
raise AttributeError(key)
|
|
123
|
+
try:
|
|
124
|
+
return self[key]
|
|
125
|
+
except (KeyError, DbColumnMissingError):
|
|
126
|
+
raise AttributeError(
|
|
127
|
+
f"'{type(self).__name__}' object has no attribute '{key}'"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def __setattr__(self, key, val):
|
|
131
|
+
if key in _INTERNAL_ATTRS or key.startswith("_"):
|
|
132
|
+
object.__setattr__(self, key, val)
|
|
133
|
+
else:
|
|
134
|
+
self[key] = val
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
# Equality and hashing — based on (table name, pk)
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def __eq__(self, other):
|
|
141
|
+
if isinstance(other, Row):
|
|
142
|
+
return self.table.name == other.table.name and self.pk == other.pk
|
|
143
|
+
if isinstance(other, dict):
|
|
144
|
+
return self.to_dict() == other
|
|
145
|
+
return NotImplemented
|
|
146
|
+
|
|
147
|
+
def __hash__(self):
|
|
148
|
+
return hash((self.table.name, tuple(sorted(self.pk.items()))))
|
|
149
|
+
|
|
150
|
+
def __ne__(self, other):
|
|
151
|
+
result = self.__eq__(other)
|
|
152
|
+
if result is NotImplemented:
|
|
153
|
+
return result
|
|
154
|
+
return not result
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
# Representations
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def __repr__(self):
|
|
161
|
+
return repr(self.to_dict())
|
|
162
|
+
|
|
163
|
+
def __str__(self):
|
|
164
|
+
return pprint.pformat(self.to_dict())
|
|
165
|
+
|
|
166
|
+
def __bool__(self):
|
|
167
|
+
return bool(self._ensure_cache())
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# dict-like helpers
|
|
171
|
+
# ------------------------------------------------------------------
|
|
63
172
|
|
|
64
173
|
def clear(self):
|
|
65
174
|
"""
|
|
66
175
|
Deletes this row from the database.
|
|
67
176
|
"""
|
|
68
177
|
self.table.delete(where=self.pk)
|
|
178
|
+
self.invalidate()
|
|
69
179
|
return self
|
|
70
180
|
|
|
71
181
|
def keys(self):
|
|
72
|
-
|
|
73
|
-
Returns the column names in the table (including sys_ columns).
|
|
74
|
-
"""
|
|
75
|
-
return self.table.sys_columns()
|
|
182
|
+
return self._ensure_cache().keys()
|
|
76
183
|
|
|
77
184
|
def values(self, *args):
|
|
78
|
-
|
|
79
|
-
Returns values from this row, optionally restricted to columns in `args`.
|
|
80
|
-
"""
|
|
81
|
-
d = self.table.select(where=self.pk).as_dict().one()
|
|
185
|
+
d = self._ensure_cache()
|
|
82
186
|
if args:
|
|
83
187
|
return [d[arg] for arg in args]
|
|
84
188
|
return list(d.values())
|
|
85
189
|
|
|
86
190
|
def items(self):
|
|
87
|
-
|
|
88
|
-
Returns (key, value) pairs for all columns.
|
|
89
|
-
"""
|
|
90
|
-
d = self.table.select(where=self.pk).as_dict().one()
|
|
91
|
-
return list(d.items())
|
|
191
|
+
return self._ensure_cache().items()
|
|
92
192
|
|
|
93
193
|
def get(self, key, failobj=None):
|
|
94
194
|
try:
|
|
@@ -97,16 +197,13 @@ class Row:
|
|
|
97
197
|
return failobj
|
|
98
198
|
return data
|
|
99
199
|
except DbColumnMissingError:
|
|
100
|
-
# Column doesn't exist in the table, return the default value
|
|
101
200
|
return failobj
|
|
102
201
|
except Exception as e:
|
|
103
|
-
# Check if the error message indicates a missing column
|
|
104
202
|
error_msg = str(e).lower()
|
|
105
203
|
if "column" in error_msg and (
|
|
106
204
|
"does not exist" in error_msg or "not found" in error_msg
|
|
107
205
|
):
|
|
108
206
|
return failobj
|
|
109
|
-
# Re-raise other exceptions
|
|
110
207
|
raise
|
|
111
208
|
|
|
112
209
|
def setdefault(self, key, default=None):
|
|
@@ -118,7 +215,7 @@ class Row:
|
|
|
118
215
|
|
|
119
216
|
def update(self, dict_=None, **kwds):
|
|
120
217
|
"""
|
|
121
|
-
Updates columns in this row.
|
|
218
|
+
Updates columns in this row (write-through to DB + local cache).
|
|
122
219
|
"""
|
|
123
220
|
data = {}
|
|
124
221
|
if dict_:
|
|
@@ -126,33 +223,12 @@ class Row:
|
|
|
126
223
|
if kwds:
|
|
127
224
|
data.update(kwds)
|
|
128
225
|
if data:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
else:
|
|
134
|
-
self.table.update(data, pk=self.pk)
|
|
226
|
+
self.table.update_or_insert(data, pk=self.pk)
|
|
227
|
+
if self._cache is not None:
|
|
228
|
+
self._cache.update(data)
|
|
229
|
+
object.__setattr__(self, "_column_set", None)
|
|
135
230
|
return self
|
|
136
231
|
|
|
137
|
-
def __cmp__(self, other):
|
|
138
|
-
"""
|
|
139
|
-
Legacy comparison method; returns 0 if self and other share keys/values, else -1.
|
|
140
|
-
"""
|
|
141
|
-
diff = -1
|
|
142
|
-
if hasattr(other, "keys"):
|
|
143
|
-
k1 = list(self.keys())
|
|
144
|
-
k2 = list(other.keys())
|
|
145
|
-
if k1 == k2:
|
|
146
|
-
diff = 0
|
|
147
|
-
for k in k1:
|
|
148
|
-
if self[k] != other[k]:
|
|
149
|
-
diff = -1
|
|
150
|
-
break
|
|
151
|
-
return diff
|
|
152
|
-
|
|
153
|
-
def __bool__(self):
|
|
154
|
-
return bool(len(self))
|
|
155
|
-
|
|
156
232
|
def copy(self, lock=None):
|
|
157
233
|
"""
|
|
158
234
|
Makes a copy of this row with a new sys_id, dropping sys_-prefixed columns from the new dict.
|
|
@@ -163,7 +239,7 @@ class Row:
|
|
|
163
239
|
old.pop(k)
|
|
164
240
|
return self.table.new(old, lock=lock)
|
|
165
241
|
|
|
166
|
-
def pop(self):
|
|
242
|
+
def pop(self, key, *args):
|
|
167
243
|
raise NotImplementedError
|
|
168
244
|
|
|
169
245
|
def popitem(self):
|
|
@@ -187,9 +263,9 @@ class Row:
|
|
|
187
263
|
|
|
188
264
|
def to_dict(self):
|
|
189
265
|
"""
|
|
190
|
-
Returns the row as a dictionary via a SELECT on self.pk.
|
|
266
|
+
Returns the row as a dictionary (from cache or via a SELECT on self.pk).
|
|
191
267
|
"""
|
|
192
|
-
return
|
|
268
|
+
return dict(self._ensure_cache())
|
|
193
269
|
|
|
194
270
|
def extract(self, *args):
|
|
195
271
|
"""
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import psycopg2
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
class Sequence:
|
|
5
2
|
"""
|
|
6
3
|
Represents a database sequence in PostgreSQL.
|
|
@@ -48,8 +45,12 @@ class Sequence:
|
|
|
48
45
|
sql = f"SELECT currval('{self.name}');"
|
|
49
46
|
try:
|
|
50
47
|
return self.tx.execute(sql, ()).scalar()
|
|
51
|
-
except
|
|
52
|
-
|
|
48
|
+
except Exception as e:
|
|
49
|
+
# Catch psycopg2.ProgrammingError (or equivalent) when no
|
|
50
|
+
# value has been generated in this session.
|
|
51
|
+
if "currval" in str(e).lower():
|
|
52
|
+
return None
|
|
53
|
+
raise
|
|
53
54
|
|
|
54
55
|
def set_value(self, start=None):
|
|
55
56
|
"""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
+
import warnings
|
|
2
3
|
import sqlparse
|
|
3
4
|
from collections.abc import Iterable, Mapping
|
|
4
5
|
from velocity.db import exceptions
|
|
@@ -597,9 +598,9 @@ class Table:
|
|
|
597
598
|
|
|
598
599
|
def foreign_keys(self):
|
|
599
600
|
"""
|
|
600
|
-
Returns the list of foreign key columns for this table
|
|
601
|
+
Returns the list of foreign key columns for this table.
|
|
601
602
|
"""
|
|
602
|
-
sql, vals = self.sql.
|
|
603
|
+
sql, vals = self.sql.foreign_keys(self.name)
|
|
603
604
|
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
604
605
|
return [x[0] for x in result.as_tuple()]
|
|
605
606
|
|
|
@@ -918,8 +919,8 @@ class Table:
|
|
|
918
919
|
else:
|
|
919
920
|
exists_where = pk
|
|
920
921
|
|
|
921
|
-
ins_builder = getattr(self.sql, "
|
|
922
|
-
self.sql, "
|
|
922
|
+
ins_builder = getattr(self.sql, "insert_if_not_exists", None) or getattr(
|
|
923
|
+
self.sql, "insnx", None
|
|
923
924
|
)
|
|
924
925
|
if ins_builder is None:
|
|
925
926
|
raise NotImplementedError(
|
|
@@ -932,7 +933,14 @@ class Table:
|
|
|
932
933
|
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
933
934
|
return result.cursor.rowcount if result.cursor else 0
|
|
934
935
|
|
|
935
|
-
|
|
936
|
+
@property
|
|
937
|
+
def updins(self):
|
|
938
|
+
warnings.warn(
|
|
939
|
+
"Table.updins is deprecated, use Table.update_or_insert instead.",
|
|
940
|
+
DeprecationWarning,
|
|
941
|
+
stacklevel=2,
|
|
942
|
+
)
|
|
943
|
+
return self.update_or_insert
|
|
936
944
|
|
|
937
945
|
@create_missing
|
|
938
946
|
def insert_if_not_exists(self, data, where=None, **kwds):
|
|
@@ -953,10 +961,25 @@ class Table:
|
|
|
953
961
|
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
954
962
|
return result.cursor.rowcount if result.cursor else 0
|
|
955
963
|
|
|
956
|
-
|
|
964
|
+
@property
|
|
965
|
+
def insnx(self):
|
|
966
|
+
warnings.warn(
|
|
967
|
+
"Table.insnx is deprecated, use Table.insert_if_not_exists instead.",
|
|
968
|
+
DeprecationWarning,
|
|
969
|
+
stacklevel=2,
|
|
970
|
+
)
|
|
971
|
+
return self.insert_if_not_exists
|
|
957
972
|
|
|
958
973
|
upsert = merge
|
|
959
|
-
|
|
974
|
+
|
|
975
|
+
@property
|
|
976
|
+
def indate(self):
|
|
977
|
+
warnings.warn(
|
|
978
|
+
"Table.indate is deprecated, use Table.merge (or Table.upsert) instead.",
|
|
979
|
+
DeprecationWarning,
|
|
980
|
+
stacklevel=2,
|
|
981
|
+
)
|
|
982
|
+
return self.merge
|
|
960
983
|
|
|
961
984
|
@return_default(0)
|
|
962
985
|
def count(self, where=None, **kwds):
|
|
@@ -1167,37 +1190,17 @@ class Table:
|
|
|
1167
1190
|
def duplicate_rows(self, columns=None, where=None, orderby=None, **kwds):
|
|
1168
1191
|
"""
|
|
1169
1192
|
Returns rows that have duplicates in the specified `columns`.
|
|
1170
|
-
TBD: Move code to generate sql to the sql module. it should not be
|
|
1171
|
-
here so different sql engines can use this function.
|
|
1172
1193
|
"""
|
|
1173
1194
|
if not columns:
|
|
1174
1195
|
raise ValueError(
|
|
1175
1196
|
"You must specify at least one column to check for duplicates."
|
|
1176
1197
|
)
|
|
1177
|
-
sql, vals = self.sql.
|
|
1178
|
-
self.tx,
|
|
1179
|
-
columns=columns,
|
|
1180
|
-
table=self.name,
|
|
1181
|
-
where=where,
|
|
1182
|
-
groupby=columns,
|
|
1183
|
-
having={">count(*)": 1},
|
|
1198
|
+
sql, vals = self.sql.duplicate_rows(
|
|
1199
|
+
self.tx, table=self.name, columns=columns, where=where, orderby=orderby
|
|
1184
1200
|
)
|
|
1185
|
-
if orderby:
|
|
1186
|
-
orderby = [orderby] if isinstance(orderby, str) else orderby
|
|
1187
|
-
else:
|
|
1188
|
-
orderby = columns
|
|
1189
|
-
subjoin = " AND ".join([f"t.{col} = dup.{col}" for col in columns])
|
|
1190
|
-
ob = ", ".join(orderby)
|
|
1191
|
-
final_sql = f"""
|
|
1192
|
-
SELECT t.*
|
|
1193
|
-
FROM {self.name} t
|
|
1194
|
-
JOIN ({sql}) dup
|
|
1195
|
-
ON {subjoin}
|
|
1196
|
-
ORDER BY {ob}
|
|
1197
|
-
"""
|
|
1198
1201
|
if kwds.get("sql_only", False):
|
|
1199
|
-
return
|
|
1200
|
-
return self.tx.execute(
|
|
1202
|
+
return sql, vals
|
|
1203
|
+
return self.tx.execute(sql, vals)
|
|
1201
1204
|
|
|
1202
1205
|
def has_duplicates(self, columns=None, where=None, **kwds):
|
|
1203
1206
|
"""
|
|
@@ -1290,12 +1293,10 @@ class Table:
|
|
|
1290
1293
|
def lock(self, mode="ACCESS EXCLUSIVE", wait_for_lock=None, **kwds):
|
|
1291
1294
|
"""
|
|
1292
1295
|
Issues a LOCK TABLE statement for this table.
|
|
1293
|
-
TBD: MOve SQL To sql module so we can use this function with other engines.
|
|
1294
1296
|
"""
|
|
1295
|
-
sql =
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
vals = None
|
|
1297
|
+
sql, vals = self.sql.lock_table(
|
|
1298
|
+
table=self.name, mode=mode, wait_for_lock=wait_for_lock
|
|
1299
|
+
)
|
|
1299
1300
|
if kwds.get("sql_only", False):
|
|
1300
1301
|
return sql, vals
|
|
1301
1302
|
return self.tx.execute(sql, vals)
|
|
@@ -1468,62 +1469,14 @@ class Table:
|
|
|
1468
1469
|
def find_duplicates(self, columns, sql_only=False):
|
|
1469
1470
|
"""
|
|
1470
1471
|
Returns duplicate groups from the table based on the specified columns in a case-insensitive way.
|
|
1471
|
-
|
|
1472
|
-
For each column, the subquery computes:
|
|
1473
|
-
- lower(column) AS normalized_<column>
|
|
1474
|
-
- array_agg(column) AS variations_<column>
|
|
1475
|
-
- array_agg(sys_id) AS sys_ids
|
|
1476
|
-
- COUNT(*) AS total_count
|
|
1477
|
-
|
|
1478
|
-
The subquery groups by lower(column) values and retains only groups with more than one row.
|
|
1479
|
-
|
|
1480
|
-
Example SQL for a single column "email_address":
|
|
1481
|
-
|
|
1482
|
-
SELECT
|
|
1483
|
-
lower(email_address) AS normalized_email_address,
|
|
1484
|
-
array_agg(email_address) AS variations_email_address,
|
|
1485
|
-
array_agg(sys_id) AS sys_ids,
|
|
1486
|
-
COUNT(*) AS total_count
|
|
1487
|
-
FROM donor_users
|
|
1488
|
-
GROUP BY lower(email_address)
|
|
1489
|
-
HAVING COUNT(*) > 1
|
|
1490
|
-
|
|
1491
|
-
Parameters:
|
|
1492
|
-
columns (list or str): Column name or list of column names to check duplicates on.
|
|
1493
|
-
sql_only (bool): If True, returns the SQL string and an empty tuple; otherwise,
|
|
1494
|
-
executes the query using self.tx.execute.
|
|
1495
|
-
|
|
1496
|
-
Returns:
|
|
1497
|
-
A tuple of (SQL string, parameters) if sql_only is True, otherwise the result of executing the query.
|
|
1498
1472
|
"""
|
|
1499
1473
|
if not columns:
|
|
1500
1474
|
raise ValueError(
|
|
1501
1475
|
"You must specify at least one column to check for duplicates."
|
|
1502
1476
|
)
|
|
1503
|
-
|
|
1504
1477
|
if isinstance(columns, str):
|
|
1505
1478
|
columns = [columns]
|
|
1506
|
-
|
|
1507
|
-
# Build subquery SELECT clause parts for normalized values and variations.
|
|
1508
|
-
normalized_cols = [f"lower({col}) AS normalized_{col}" for col in columns]
|
|
1509
|
-
variations_cols = [f"array_agg({col}) AS variations_{col}" for col in columns]
|
|
1510
|
-
|
|
1511
|
-
subquery_select = ",\n ".join(
|
|
1512
|
-
normalized_cols
|
|
1513
|
-
+ variations_cols
|
|
1514
|
-
+ ["array_agg(sys_id) AS sys_ids", "COUNT(*) AS total_count"]
|
|
1515
|
-
)
|
|
1516
|
-
|
|
1517
|
-
groupby_clause = ", ".join([f"lower({col})" for col in columns])
|
|
1518
|
-
|
|
1519
|
-
subquery = f"""
|
|
1520
|
-
SELECT
|
|
1521
|
-
{subquery_select}
|
|
1522
|
-
FROM {self.name}
|
|
1523
|
-
GROUP BY {groupby_clause}
|
|
1524
|
-
HAVING COUNT(*) > 1
|
|
1525
|
-
"""
|
|
1526
|
-
|
|
1479
|
+
sql, vals = self.sql.find_duplicates(table=self.name, columns=columns)
|
|
1527
1480
|
if sql_only:
|
|
1528
|
-
return
|
|
1529
|
-
return self.tx.execute(
|
|
1481
|
+
return sql, vals
|
|
1482
|
+
return self.tx.execute(sql, vals)
|
|
@@ -530,3 +530,51 @@ class BaseSQLDialect(ABC):
|
|
|
530
530
|
Tuple of (sql_string, parameters)
|
|
531
531
|
"""
|
|
532
532
|
pass
|
|
533
|
+
|
|
534
|
+
# ------------------------------------------------------------------
|
|
535
|
+
# Optional methods — dialects may override if they support these
|
|
536
|
+
# ------------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
def foreign_keys(cls, table: str) -> Tuple[str, tuple]:
|
|
540
|
+
"""Return foreign key column names for the given table."""
|
|
541
|
+
raise NotImplementedError(
|
|
542
|
+
f"{cls.server} dialect does not implement foreign_keys()"
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
@classmethod
|
|
546
|
+
def lock_table(
|
|
547
|
+
cls,
|
|
548
|
+
table: str,
|
|
549
|
+
mode: str = "ACCESS EXCLUSIVE",
|
|
550
|
+
wait_for_lock: Optional[bool] = None,
|
|
551
|
+
) -> Tuple[str, tuple]:
|
|
552
|
+
"""Generate a LOCK TABLE statement."""
|
|
553
|
+
raise NotImplementedError(
|
|
554
|
+
f"{cls.server} dialect does not implement lock_table()"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
@classmethod
|
|
558
|
+
def duplicate_rows(
|
|
559
|
+
cls,
|
|
560
|
+
tx: Any,
|
|
561
|
+
table: str,
|
|
562
|
+
columns: List[str],
|
|
563
|
+
where: Optional[Union[str, Dict, List]] = None,
|
|
564
|
+
orderby: Optional[Union[str, List[str]]] = None,
|
|
565
|
+
) -> Tuple[str, tuple]:
|
|
566
|
+
"""Return rows that have duplicates in the specified columns."""
|
|
567
|
+
raise NotImplementedError(
|
|
568
|
+
f"{cls.server} dialect does not implement duplicate_rows()"
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
@classmethod
|
|
572
|
+
def find_duplicates(
|
|
573
|
+
cls,
|
|
574
|
+
table: str,
|
|
575
|
+
columns: List[str],
|
|
576
|
+
) -> Tuple[str, tuple]:
|
|
577
|
+
"""Return duplicate groups with case-insensitive matching."""
|
|
578
|
+
raise NotImplementedError(
|
|
579
|
+
f"{cls.server} dialect does not implement find_duplicates()"
|
|
580
|
+
)
|