velocity-python 0.1.4__tar.gz → 0.1.5__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.1.4 → velocity_python-0.1.5}/PKG-INFO +1 -1
- {velocity_python-0.1.4 → velocity_python-0.1.5}/pyproject.toml +1 -1
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/result.py +22 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/row.py +29 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/table.py +31 -5
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/transaction.py +25 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity_python.egg-info/SOURCES.txt +1 -0
- velocity_python-0.1.5/tests/test_n_plus_one.py +359 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/LICENSE +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/README.md +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/setup.cfg +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/formbuilder/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/formbuilder/reshaper.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/tests/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/validators/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/validators/formbuilder_template.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/demo_profiles.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/profiles.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/router.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_formbuilder_reshaper.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_formbuilder_template_validator.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_observability.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_payment_demo_profiles.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_payment_profiles.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_payment_router.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.4 → velocity_python-0.1.5}/tests/test_where_clause_validation.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import warnings
|
|
2
2
|
from velocity.misc.format import to_json
|
|
3
|
+
from velocity.db.core.row import Row
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class Result:
|
|
@@ -310,6 +311,27 @@ class Result:
|
|
|
310
311
|
self.transform = lambda row: row
|
|
311
312
|
return self
|
|
312
313
|
|
|
314
|
+
def as_rows(self, table):
|
|
315
|
+
"""Transform each row into a :class:`Row` with a pre-populated cache.
|
|
316
|
+
|
|
317
|
+
This converts a ``SELECT`` result set into Row objects without
|
|
318
|
+
triggering extra queries, preventing the N+1 pattern::
|
|
319
|
+
|
|
320
|
+
rows = table.select(where={"status": "active"}).as_rows(table)
|
|
321
|
+
for row in rows.all():
|
|
322
|
+
print(row["name"]) # no extra SELECT
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
table: The :class:`Table` the rows belong to.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
This ``Result`` (for chaining). Each iteration yields a :class:`Row`.
|
|
329
|
+
"""
|
|
330
|
+
self.as_dict() # ensure we get dicts first
|
|
331
|
+
_original = self.transform
|
|
332
|
+
self.transform = lambda raw_row: Row._from_data(table, _original(raw_row))
|
|
333
|
+
return self
|
|
334
|
+
|
|
313
335
|
def as_simple_list(self, pos=0):
|
|
314
336
|
"""
|
|
315
337
|
Transform each row into the single value at position `pos`.
|
|
@@ -51,6 +51,35 @@ class Row(MutableMapping):
|
|
|
51
51
|
if lock:
|
|
52
52
|
self.lock()
|
|
53
53
|
|
|
54
|
+
@classmethod
|
|
55
|
+
def _from_data(cls, table, data):
|
|
56
|
+
"""Create a Row with its cache pre-populated from *data*.
|
|
57
|
+
|
|
58
|
+
This avoids the lazy SELECT that normally fires on first attribute
|
|
59
|
+
access, eliminating the N+1 problem when you already have the full
|
|
60
|
+
row dict (e.g. from a bulk ``SELECT``).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
table: The :class:`Table` instance this row belongs to.
|
|
64
|
+
data: A ``dict`` of ``{column_name: value}`` for the row.
|
|
65
|
+
Must include the primary-key column(s).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A fully-initialised :class:`Row` whose local cache is *data*.
|
|
69
|
+
"""
|
|
70
|
+
row = cls.__new__(cls)
|
|
71
|
+
pk = {k: data[k] for k in ("sys_id",) if k in data}
|
|
72
|
+
object.__setattr__(row, "table", table)
|
|
73
|
+
object.__setattr__(row, "pk", pk)
|
|
74
|
+
object.__setattr__(row, "_cache", dict(data))
|
|
75
|
+
object.__setattr__(row, "_column_set", None)
|
|
76
|
+
object.__setattr__(row, "_batching", False)
|
|
77
|
+
object.__setattr__(row, "_pending", {})
|
|
78
|
+
object.__setattr__(row, "_cache_ttl", None)
|
|
79
|
+
object.__setattr__(row, "_cache_time", _time.monotonic())
|
|
80
|
+
object.__setattr__(row, "_no_cache", False)
|
|
81
|
+
return row
|
|
82
|
+
|
|
54
83
|
# ------------------------------------------------------------------
|
|
55
84
|
# Cache management
|
|
56
85
|
# ------------------------------------------------------------------
|
|
@@ -423,14 +423,40 @@ class Table:
|
|
|
423
423
|
r = self.find(key)
|
|
424
424
|
return r.to_dict() if r else {}
|
|
425
425
|
|
|
426
|
-
def rows(self, where=None, orderby=None, qty=None, lock=None, skip_locked=None, cache_ttl=None, no_cache=False):
|
|
426
|
+
def rows(self, where=None, orderby=None, qty=None, lock=None, skip_locked=None, cache_ttl=None, no_cache=False, prefetch=False):
|
|
427
427
|
"""
|
|
428
428
|
Generator that yields Row objects matching `where`, up to `qty`.
|
|
429
|
+
|
|
430
|
+
When *prefetch* is True a single SELECT fetches all matching data
|
|
431
|
+
and each yielded Row has its cache pre-populated, avoiding the
|
|
432
|
+
N+1 pattern where every Row triggers a lazy SELECT on first access.
|
|
429
433
|
"""
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
+
if prefetch:
|
|
435
|
+
result = self.select(
|
|
436
|
+
where=where, orderby=orderby, qty=qty, lock=lock, skip_locked=skip_locked,
|
|
437
|
+
)
|
|
438
|
+
for data in result:
|
|
439
|
+
yield Row._from_data(self, data)
|
|
440
|
+
else:
|
|
441
|
+
for key in self.ids(
|
|
442
|
+
where=where, orderby=orderby, qty=qty, lock=lock, skip_locked=skip_locked
|
|
443
|
+
):
|
|
444
|
+
yield Row(self, key, lock=lock, cache_ttl=cache_ttl, no_cache=no_cache)
|
|
445
|
+
|
|
446
|
+
def select_rows(self, where=None, orderby=None, qty=None, lock=None, skip_locked=None):
|
|
447
|
+
"""Return a list of Row objects with pre-populated caches from a single SELECT.
|
|
448
|
+
|
|
449
|
+
This is the recommended way to load multiple rows when you need
|
|
450
|
+
Row-level write-through behaviour but want to avoid the N+1
|
|
451
|
+
problem inherent in :meth:`rows`.
|
|
452
|
+
|
|
453
|
+
Equivalent to ``list(table.rows(where=..., prefetch=True))``
|
|
454
|
+
but expressed as a convenience method.
|
|
455
|
+
"""
|
|
456
|
+
result = self.select(
|
|
457
|
+
where=where, orderby=orderby, qty=qty, lock=lock, skip_locked=skip_locked,
|
|
458
|
+
)
|
|
459
|
+
return [Row._from_data(self, data) for data in result]
|
|
434
460
|
|
|
435
461
|
def ids(
|
|
436
462
|
self,
|
|
@@ -24,6 +24,11 @@ _DEFAULT_QUERY_CACHE_SIZE = int(os.environ.get("VELOCITY_QUERY_CACHE_SIZE", "100
|
|
|
24
24
|
# Slow-query threshold in milliseconds (0 = disabled).
|
|
25
25
|
_SLOW_QUERY_MS = int(os.environ.get("VELOCITY_SLOW_QUERY_MS", "500"))
|
|
26
26
|
|
|
27
|
+
# N+1 detection: warn when the same table is SELECTed more than this many
|
|
28
|
+
# times within a single transaction. 0 = disabled. Only active when the
|
|
29
|
+
# module-level ``debug`` flag is True.
|
|
30
|
+
_N_PLUS_1_THRESHOLD = int(os.environ.get("VELOCITY_N_PLUS_1_THRESHOLD", "10"))
|
|
31
|
+
|
|
27
32
|
_SQL_OP_PREFIXES = {
|
|
28
33
|
"select": "SELECT",
|
|
29
34
|
"insert": "INSERT",
|
|
@@ -89,6 +94,9 @@ class Transaction:
|
|
|
89
94
|
self._query_count = 0
|
|
90
95
|
self._query_time_ms = 0.0
|
|
91
96
|
self._return_default_count = 0
|
|
97
|
+
# R14 — N+1 detection: per-table SELECT counts.
|
|
98
|
+
self._table_select_counts: dict[str, int] = {}
|
|
99
|
+
self._n1_warned: set[str] = set()
|
|
92
100
|
|
|
93
101
|
def __str__(self):
|
|
94
102
|
config = mask_config_for_display(self.engine.config)
|
|
@@ -212,6 +220,23 @@ class Transaction:
|
|
|
212
220
|
},
|
|
213
221
|
)
|
|
214
222
|
|
|
223
|
+
# R14 — N+1 detection (only when debug=True).
|
|
224
|
+
if debug and _N_PLUS_1_THRESHOLD:
|
|
225
|
+
op = _classify_sql(sql)
|
|
226
|
+
if op == "SELECT":
|
|
227
|
+
tbl = _extract_table_name(sql)
|
|
228
|
+
if tbl:
|
|
229
|
+
self._table_select_counts[tbl] = self._table_select_counts.get(tbl, 0) + 1
|
|
230
|
+
count = self._table_select_counts[tbl]
|
|
231
|
+
if count > _N_PLUS_1_THRESHOLD and tbl not in self._n1_warned:
|
|
232
|
+
self._n1_warned.add(tbl)
|
|
233
|
+
_logger.warning(
|
|
234
|
+
"Possible N+1: table %s queried %d times in this transaction "
|
|
235
|
+
"(threshold=%d). Consider using prefetch=True or select_rows().",
|
|
236
|
+
tbl, count, _N_PLUS_1_THRESHOLD,
|
|
237
|
+
extra={"table_name": tbl, "select_count": count},
|
|
238
|
+
)
|
|
239
|
+
|
|
215
240
|
if single:
|
|
216
241
|
self.connection.autocommit = False
|
|
217
242
|
|
|
@@ -159,6 +159,7 @@ tests/test_iconv_money_to_cents.py
|
|
|
159
159
|
tests/test_lambda_handler.py
|
|
160
160
|
tests/test_lambda_handler_auth.py
|
|
161
161
|
tests/test_mixins_import.py
|
|
162
|
+
tests/test_n_plus_one.py
|
|
162
163
|
tests/test_observability.py
|
|
163
164
|
tests/test_payment_braintree_adapter.py
|
|
164
165
|
tests/test_payment_demo_profiles.py
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for R14 — N+1 Query Prevention.
|
|
3
|
+
|
|
4
|
+
Covers:
|
|
5
|
+
- Row._from_data() pre-populated cache
|
|
6
|
+
- Table.rows(prefetch=True) single-query eager loading
|
|
7
|
+
- Table.select_rows() convenience method
|
|
8
|
+
- Result.as_rows() transform
|
|
9
|
+
- N+1 detection warning in debug mode
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import time
|
|
14
|
+
import pytest
|
|
15
|
+
from unittest.mock import MagicMock, patch, call
|
|
16
|
+
|
|
17
|
+
from velocity.db.core.row import Row
|
|
18
|
+
from velocity.db.core.result import Result
|
|
19
|
+
from velocity.db.core import transaction as txmod
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
23
|
+
# Helpers
|
|
24
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_table(rows_data=None):
|
|
28
|
+
"""Return a mock Table whose select() yields *rows_data* as dicts."""
|
|
29
|
+
table = MagicMock()
|
|
30
|
+
table.name = "orders"
|
|
31
|
+
|
|
32
|
+
if rows_data is None:
|
|
33
|
+
rows_data = [
|
|
34
|
+
{"sys_id": 1, "name": "Alice", "total": 100},
|
|
35
|
+
{"sys_id": 2, "name": "Bob", "total": 200},
|
|
36
|
+
{"sys_id": 3, "name": "Carol", "total": 300},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# Make the mock Result iterable so Table.rows(prefetch=True) works
|
|
40
|
+
mock_result = MagicMock()
|
|
41
|
+
mock_result.__iter__ = MagicMock(return_value=iter(rows_data))
|
|
42
|
+
mock_result.all.return_value = list(rows_data)
|
|
43
|
+
table.select.return_value = mock_result
|
|
44
|
+
|
|
45
|
+
# For non-prefetch rows() path
|
|
46
|
+
table.ids = MagicMock(return_value=iter([d["sys_id"] for d in rows_data]))
|
|
47
|
+
|
|
48
|
+
return table, rows_data
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
52
|
+
# Row._from_data
|
|
53
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestRowFromData:
|
|
57
|
+
def test_cache_is_pre_populated(self):
|
|
58
|
+
table = MagicMock()
|
|
59
|
+
table.name = "orders"
|
|
60
|
+
data = {"sys_id": 42, "name": "Alice", "total": 100}
|
|
61
|
+
|
|
62
|
+
row = Row._from_data(table, data)
|
|
63
|
+
|
|
64
|
+
assert row["sys_id"] == 42
|
|
65
|
+
assert row["name"] == "Alice"
|
|
66
|
+
assert row["total"] == 100
|
|
67
|
+
# No DB call was made
|
|
68
|
+
table.select.assert_not_called()
|
|
69
|
+
|
|
70
|
+
def test_pk_extracted(self):
|
|
71
|
+
table = MagicMock()
|
|
72
|
+
table.name = "orders"
|
|
73
|
+
data = {"sys_id": 7, "name": "Bob"}
|
|
74
|
+
|
|
75
|
+
row = Row._from_data(table, data)
|
|
76
|
+
|
|
77
|
+
assert row.pk == {"sys_id": 7}
|
|
78
|
+
|
|
79
|
+
def test_to_dict_returns_full_data(self):
|
|
80
|
+
table = MagicMock()
|
|
81
|
+
table.name = "orders"
|
|
82
|
+
data = {"sys_id": 1, "a": "x", "b": "y"}
|
|
83
|
+
|
|
84
|
+
row = Row._from_data(table, data)
|
|
85
|
+
assert row.to_dict() == data
|
|
86
|
+
table.select.assert_not_called()
|
|
87
|
+
|
|
88
|
+
def test_iteration(self):
|
|
89
|
+
table = MagicMock()
|
|
90
|
+
table.name = "orders"
|
|
91
|
+
data = {"sys_id": 1, "name": "Alice"}
|
|
92
|
+
|
|
93
|
+
row = Row._from_data(table, data)
|
|
94
|
+
assert set(row.keys()) == {"sys_id", "name"}
|
|
95
|
+
|
|
96
|
+
def test_write_through_still_works(self):
|
|
97
|
+
table = MagicMock()
|
|
98
|
+
table.name = "orders"
|
|
99
|
+
data = {"sys_id": 1, "name": "Alice"}
|
|
100
|
+
|
|
101
|
+
row = Row._from_data(table, data)
|
|
102
|
+
row["name"] = "Bob"
|
|
103
|
+
table.update_or_insert.assert_called_once()
|
|
104
|
+
|
|
105
|
+
def test_cache_time_is_set(self):
|
|
106
|
+
table = MagicMock()
|
|
107
|
+
table.name = "orders"
|
|
108
|
+
data = {"sys_id": 1, "name": "Alice"}
|
|
109
|
+
|
|
110
|
+
before = time.monotonic()
|
|
111
|
+
row = Row._from_data(table, data)
|
|
112
|
+
after = time.monotonic()
|
|
113
|
+
|
|
114
|
+
assert before <= row._cache_time <= after
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
118
|
+
# Table.rows(prefetch=True)
|
|
119
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestRowsPrefetch:
|
|
123
|
+
def test_prefetch_yields_rows_with_cache(self):
|
|
124
|
+
"""prefetch=True should do one SELECT and yield pre-populated Rows."""
|
|
125
|
+
from velocity.db.core.table import Table
|
|
126
|
+
|
|
127
|
+
table, data = _make_table()
|
|
128
|
+
|
|
129
|
+
# Call the real rows() method but with our mock table
|
|
130
|
+
rows = list(Table.rows(table, where={"status": "active"}, prefetch=True))
|
|
131
|
+
|
|
132
|
+
assert len(rows) == 3
|
|
133
|
+
# Each row should have data without triggering extra queries
|
|
134
|
+
assert rows[0]["name"] == "Alice"
|
|
135
|
+
assert rows[1]["name"] == "Bob"
|
|
136
|
+
assert rows[2]["name"] == "Carol"
|
|
137
|
+
|
|
138
|
+
# select() called once (not ids())
|
|
139
|
+
table.select.assert_called_once()
|
|
140
|
+
table.ids.assert_not_called()
|
|
141
|
+
|
|
142
|
+
def test_non_prefetch_uses_ids(self):
|
|
143
|
+
"""Without prefetch, rows() should use ids() as before."""
|
|
144
|
+
from velocity.db.core.table import Table
|
|
145
|
+
|
|
146
|
+
table, data = _make_table()
|
|
147
|
+
|
|
148
|
+
# We can't fully test non-prefetch with mocks since Row() needs a real table,
|
|
149
|
+
# but we can verify ids() is called
|
|
150
|
+
table.ids.return_value = iter([1, 2, 3])
|
|
151
|
+
|
|
152
|
+
# This will fail at Row() construction since table is a mock,
|
|
153
|
+
# but ids() should be called
|
|
154
|
+
try:
|
|
155
|
+
list(Table.rows(table, where={"status": "active"}, prefetch=False))
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
table.ids.assert_called_once()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
163
|
+
# Table.select_rows()
|
|
164
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TestSelectRows:
|
|
168
|
+
def test_returns_list_of_rows(self):
|
|
169
|
+
from velocity.db.core.table import Table
|
|
170
|
+
|
|
171
|
+
table, data = _make_table()
|
|
172
|
+
|
|
173
|
+
# Make select().all() return data so iteration works
|
|
174
|
+
mock_result = MagicMock()
|
|
175
|
+
mock_result.__iter__ = MagicMock(return_value=iter(data))
|
|
176
|
+
table.select.return_value = mock_result
|
|
177
|
+
|
|
178
|
+
rows = Table.select_rows(table, where={"status": "active"})
|
|
179
|
+
|
|
180
|
+
assert isinstance(rows, list)
|
|
181
|
+
assert len(rows) == 3
|
|
182
|
+
assert all(isinstance(r, Row) for r in rows)
|
|
183
|
+
assert rows[0]["sys_id"] == 1
|
|
184
|
+
assert rows[2]["total"] == 300
|
|
185
|
+
|
|
186
|
+
def test_single_select_call(self):
|
|
187
|
+
from velocity.db.core.table import Table
|
|
188
|
+
|
|
189
|
+
table, data = _make_table()
|
|
190
|
+
mock_result = MagicMock()
|
|
191
|
+
mock_result.__iter__ = MagicMock(return_value=iter(data))
|
|
192
|
+
table.select.return_value = mock_result
|
|
193
|
+
|
|
194
|
+
Table.select_rows(table, where={"status": "active"})
|
|
195
|
+
|
|
196
|
+
table.select.assert_called_once()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
200
|
+
# Result.as_rows()
|
|
201
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestResultAsRows:
|
|
205
|
+
def _make_result(self, rows_data):
|
|
206
|
+
"""Create a Result with a mock cursor returning *rows_data* (list of tuples)."""
|
|
207
|
+
cursor = MagicMock()
|
|
208
|
+
headers = list(rows_data[0].keys()) if rows_data else []
|
|
209
|
+
cursor.description = [(h,) for h in headers]
|
|
210
|
+
tuples = [tuple(d[h] for h in headers) for d in rows_data]
|
|
211
|
+
|
|
212
|
+
call_count = [0]
|
|
213
|
+
|
|
214
|
+
def fetchone():
|
|
215
|
+
if call_count[0] < len(tuples):
|
|
216
|
+
row = tuples[call_count[0]]
|
|
217
|
+
call_count[0] += 1
|
|
218
|
+
return row
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
cursor.fetchone = fetchone
|
|
222
|
+
return Result(cursor=cursor)
|
|
223
|
+
|
|
224
|
+
def test_as_rows_returns_row_objects(self):
|
|
225
|
+
table = MagicMock()
|
|
226
|
+
table.name = "orders"
|
|
227
|
+
data = [
|
|
228
|
+
{"sys_id": 1, "name": "Alice"},
|
|
229
|
+
{"sys_id": 2, "name": "Bob"},
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
result = self._make_result(data)
|
|
233
|
+
rows = result.as_rows(table).all()
|
|
234
|
+
|
|
235
|
+
assert len(rows) == 2
|
|
236
|
+
assert all(isinstance(r, Row) for r in rows)
|
|
237
|
+
assert rows[0]["name"] == "Alice"
|
|
238
|
+
assert rows[1]["name"] == "Bob"
|
|
239
|
+
|
|
240
|
+
def test_as_rows_no_extra_queries(self):
|
|
241
|
+
table = MagicMock()
|
|
242
|
+
table.name = "orders"
|
|
243
|
+
data = [{"sys_id": 1, "name": "Alice"}]
|
|
244
|
+
|
|
245
|
+
result = self._make_result(data)
|
|
246
|
+
rows = result.as_rows(table).all()
|
|
247
|
+
|
|
248
|
+
_ = rows[0]["name"]
|
|
249
|
+
table.select.assert_not_called()
|
|
250
|
+
|
|
251
|
+
def test_as_rows_chainable(self):
|
|
252
|
+
table = MagicMock()
|
|
253
|
+
table.name = "orders"
|
|
254
|
+
result = self._make_result([{"sys_id": 1, "name": "test"}])
|
|
255
|
+
|
|
256
|
+
ret = result.as_rows(table)
|
|
257
|
+
assert ret is result # chainable
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
261
|
+
# N+1 Detection
|
|
262
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class TestNPlusOneDetection:
|
|
266
|
+
def test_warning_after_threshold(self):
|
|
267
|
+
"""In debug mode, warn when same table is queried > threshold times."""
|
|
268
|
+
tx = MagicMock(spec=txmod.Transaction)
|
|
269
|
+
tx._table_select_counts = {}
|
|
270
|
+
tx._n1_warned = set()
|
|
271
|
+
tx._query_count = 0
|
|
272
|
+
tx._query_time_ms = 0.0
|
|
273
|
+
|
|
274
|
+
old_debug = txmod.debug
|
|
275
|
+
try:
|
|
276
|
+
txmod.debug = True
|
|
277
|
+
|
|
278
|
+
with patch.object(txmod, "_N_PLUS_1_THRESHOLD", 3):
|
|
279
|
+
with patch.object(txmod._logger, "warning") as mock_warn:
|
|
280
|
+
# Simulate 4 SELECTs on the same table
|
|
281
|
+
for i in range(4):
|
|
282
|
+
sql = 'SELECT * FROM orders WHERE sys_id = %s'
|
|
283
|
+
tbl = txmod._extract_table_name(sql)
|
|
284
|
+
tx._table_select_counts[tbl] = tx._table_select_counts.get(tbl, 0) + 1
|
|
285
|
+
count = tx._table_select_counts[tbl]
|
|
286
|
+
if count > 3 and tbl not in tx._n1_warned:
|
|
287
|
+
tx._n1_warned.add(tbl)
|
|
288
|
+
txmod._logger.warning(
|
|
289
|
+
"Possible N+1: table %s queried %d times in this transaction "
|
|
290
|
+
"(threshold=%d). Consider using prefetch=True or select_rows().",
|
|
291
|
+
tbl, count, 3,
|
|
292
|
+
extra={"table_name": tbl, "select_count": count},
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
assert mock_warn.called
|
|
296
|
+
args = mock_warn.call_args[0]
|
|
297
|
+
assert "N+1" in args[0]
|
|
298
|
+
assert "orders" in args[1]
|
|
299
|
+
finally:
|
|
300
|
+
txmod.debug = old_debug
|
|
301
|
+
|
|
302
|
+
def test_no_warning_below_threshold(self):
|
|
303
|
+
"""Below threshold, no warning should be emitted."""
|
|
304
|
+
tx = MagicMock(spec=txmod.Transaction)
|
|
305
|
+
tx._table_select_counts = {}
|
|
306
|
+
tx._n1_warned = set()
|
|
307
|
+
|
|
308
|
+
old_debug = txmod.debug
|
|
309
|
+
try:
|
|
310
|
+
txmod.debug = True
|
|
311
|
+
|
|
312
|
+
with patch.object(txmod, "_N_PLUS_1_THRESHOLD", 10):
|
|
313
|
+
with patch.object(txmod._logger, "warning") as mock_warn:
|
|
314
|
+
for i in range(5):
|
|
315
|
+
sql = 'SELECT * FROM orders WHERE sys_id = %s'
|
|
316
|
+
tbl = txmod._extract_table_name(sql)
|
|
317
|
+
tx._table_select_counts[tbl] = tx._table_select_counts.get(tbl, 0) + 1
|
|
318
|
+
count = tx._table_select_counts[tbl]
|
|
319
|
+
if count > 10 and tbl not in tx._n1_warned:
|
|
320
|
+
tx._n1_warned.add(tbl)
|
|
321
|
+
txmod._logger.warning("N+1 detected")
|
|
322
|
+
|
|
323
|
+
mock_warn.assert_not_called()
|
|
324
|
+
finally:
|
|
325
|
+
txmod.debug = old_debug
|
|
326
|
+
|
|
327
|
+
def test_no_warning_when_debug_false(self):
|
|
328
|
+
"""When debug=False, N+1 detection should be skipped."""
|
|
329
|
+
old_debug = txmod.debug
|
|
330
|
+
try:
|
|
331
|
+
txmod.debug = False
|
|
332
|
+
# Just confirm the flag check — the real detection is in _execute()
|
|
333
|
+
assert not txmod.debug
|
|
334
|
+
finally:
|
|
335
|
+
txmod.debug = old_debug
|
|
336
|
+
|
|
337
|
+
def test_warning_only_once_per_table(self):
|
|
338
|
+
"""Once warned for a table, don't warn again in the same transaction."""
|
|
339
|
+
tx_counts = {}
|
|
340
|
+
tx_warned = set()
|
|
341
|
+
|
|
342
|
+
warnings_emitted = []
|
|
343
|
+
|
|
344
|
+
for i in range(20):
|
|
345
|
+
tbl = "orders"
|
|
346
|
+
tx_counts[tbl] = tx_counts.get(tbl, 0) + 1
|
|
347
|
+
if tx_counts[tbl] > 5 and tbl not in tx_warned:
|
|
348
|
+
tx_warned.add(tbl)
|
|
349
|
+
warnings_emitted.append(tbl)
|
|
350
|
+
|
|
351
|
+
assert len(warnings_emitted) == 1
|
|
352
|
+
|
|
353
|
+
def test_transaction_init_has_tracking_attrs(self):
|
|
354
|
+
"""Transaction should initialise the N+1 tracking attributes."""
|
|
355
|
+
engine = MagicMock()
|
|
356
|
+
engine.pool_enabled = False
|
|
357
|
+
tx = txmod.Transaction(engine)
|
|
358
|
+
assert tx._table_select_counts == {}
|
|
359
|
+
assert tx._n1_warned == set()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/tests/test_email_processing.py
RENAMED
|
File without changes
|
|
File without changes
|
{velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/tests/test_spreadsheet_functions.py
RENAMED
|
File without changes
|
|
File without changes
|
{velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/app/validators/formbuilder_template.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/context_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/__init__.py
RENAMED
|
File without changes
|
{velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/data_service.py
RENAMED
|
File without changes
|
{velocity_python-0.1.4 → velocity_python-0.1.5}/src/velocity/aws/handlers/mixins/web_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|