dqlite-dbapi 0.2.0__tar.gz → 0.2.1__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.
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/PKG-INFO +16 -8
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/README.md +13 -5
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/pyproject.toml +3 -3
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/__init__.py +67 -13
- dqlite_dbapi-0.2.1/src/dqlitedbapi/_busy_retry.py +297 -0
- dqlite_dbapi-0.2.1/src/dqlitedbapi/_pragma_intercept.py +106 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/aio/__init__.py +58 -5
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/aio/connection.py +57 -5
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/aio/cursor.py +53 -3
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/connection.py +332 -39
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/cursor.py +65 -3
- dqlite_dbapi-0.2.1/tests/integration/test_pragma_deny_list.py +122 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_module_attributes.py +4 -1
- dqlite_dbapi-0.2.1/tests/test_async_resource_warning_sanitize_parity.py +83 -0
- dqlite_dbapi-0.2.1/tests/test_busy_retry_async_wiring.py +159 -0
- dqlite_dbapi-0.2.1/tests/test_busy_retry_classifier.py +97 -0
- dqlite_dbapi-0.2.1/tests/test_busy_retry_curve.py +149 -0
- dqlite_dbapi-0.2.1/tests/test_busy_retry_docstring_pins_cross_thread_caveat.py +61 -0
- dqlite_dbapi-0.2.1/tests/test_busy_retry_sync_wiring.py +140 -0
- dqlite_dbapi-0.2.1/tests/test_check_same_thread_kwarg.py +264 -0
- dqlite_dbapi-0.2.1/tests/test_connect_busy_timeout_kwarg.py +125 -0
- dqlite_dbapi-0.2.1/tests/test_connection_docstring_pins_check_same_thread_contract.py +106 -0
- dqlite_dbapi-0.2.1/tests/test_cursors_weakset_state_lock.py +142 -0
- dqlite_dbapi-0.2.1/tests/test_in_transaction_property_under_check_same_thread.py +141 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_module_attributes.py +14 -2
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_module_level_stubs.py +20 -1
- dqlite_dbapi-0.2.1/tests/test_pragma_busy_timeout_intercept.py +201 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_transaction_owner_token_bookkeeping.py +24 -10
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_thread_safety_enforcement.py +19 -8
- dqlite_dbapi-0.2.1/tests/test_transaction_ctxmgr_state_lock.py +227 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/uv.lock +3 -3
- dqlite_dbapi-0.2.0/tests/integration/test_pragma_deny_list.py +0 -60
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/.gitignore +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/DEVELOPMENT.md +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/LICENSE.md +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/_constants.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/exceptions.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/py.typed +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/src/dqlitedbapi/types.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/__init__.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/aio/test_aio_version_final_annotated.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/aio/test_async_close_underlying_close_failed_logged.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/aio/test_async_connection_fork_guard.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/aio/test_async_iter_protocol_invariants.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/aio/test_async_unclosed_warning_skips_in_fork_child.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/aio/test_close_cancel_between_cursor_cascade_and_lock.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/aio/test_fetchmany_cancel_atomic.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/aio/test_messages_clear_on_closed.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/conftest.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/__init__.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/conftest.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_async_concurrent_executemany_serialized.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_async_cursor_concurrent_execute_rejected.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_async_cursor_edge_branches.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_async_fetchmany_cancel.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_async_returning_zero_rows.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_async_transaction_ctxmgr_rejects_stray_commit.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_close_implicit_rollback_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_concurrent_cursors_visibility.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_constraint_extended_codes.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_context_manager.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_cursor_fetch_after_non_row_execute.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_cursor_misc_invariant_pins.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_cursor_row_index_after_fetchall.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_datetime_conversion.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_description_after_ddl_pragma.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_description_survives_empty_fetchall.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_executemany_cancel_mid_batch.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_executemany_completed_iterations_observability.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_executemany_cte_dml.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_executemany_failure_resets_rowcount.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_executemany_generator.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_executemany_returning.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_integrity_error.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_lastrowid_autoincrement.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_lastrowid_cursor_scoped.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_lastrowid_returning.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_lastrowid_survives_cross_cursor_rollback.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_leader_redirect_live.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_misc_coverage.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_multiline_returning_rows.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_nan_inf_float.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_negative_zero_and_subnormal_float.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_no_transaction_error_wording.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_null_vs_empty_text_blob_three_way.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_query_detection.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_returning.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_savepoint_dbapi.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_savepoint_failure_responses.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_sqlite_version_pin.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/integration/test_sync_transaction_ctxmgr_rejects_stray_commit.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aconn_execute_with_parameters.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aconn_factory_getters.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aconnect_cleanup_close_shielded_against_outer_timeout.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aconnect_cleanup_on_connect_failure.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_adapter_exceptions_wrapped_as_data_error.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aenter_cleanup_preserves_original_under_outer_cancel.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aexit_behavioural_no_close.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aexit_commit_cancel_debug_log.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aexit_rollback_cancel_body_context_pin.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aexit_rollback_debug_log.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_commit_rollback_no_tx.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_commit_rollback_no_tx_code_matrix.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_commit_rollback_wrapping.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_connection_finalizer_detached_on_cancel_close.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_connection_finalizer_detached_on_close.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_cursor_drain_rows.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_cursor_row_factory_typeerror_wrap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_description_tuple_exported.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_executemany_cursor_close_cascade.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_force_close_transport_cursor_cascade.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_init_reexports_diagnostic_prefixes.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_loop_affinity_exc_class.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_register_adapter_export.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_register_converter_stubs.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_row_factory_setter_loop_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_aio_setters_clear_messages_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_arraysize_validation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_backup_not_async_def.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_check_loop_only_pid_before_loop.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_close_cancel_arm_cancels_inner_pending_drain.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_close_ensure_locks_guard.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_close_finally_propagates_cancel.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_close_op_lock_bounded.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_close_outer_cancel_force_closes_transport.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_close_race.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_commit_rollback_force_close_on_outer_cancel.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_commit_rollback_in_transaction_under_lock.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_commit_rollback_loop_binding_first.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_commit_rollback_messages_under_lock.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_commit_rollback_op_lock_bounded.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_commit_rollback_phase_budget.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_commit_rollback_retry_after_invalidate.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_commit_rollback_timeout_phase_aware.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_connect_cross_loop_diagnostic_on_fast_path.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_connection.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_connection_close_during_connect.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_connection_cursor_fork_guard.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_connection_ensure_connection_post_yield_close_shielded.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_connection_race.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_connection_setters_loop_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_connection_transaction.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_aclose_alias.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_add_races_cascade_clear.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_aenter_cross_task_executing_guard.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_aenter_gc_parent_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_aiter_aenter_pep249_pins.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_aiter_cross_loop_fail_fast.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_arraysize_row_factory_loop_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_callproc_nextset_scroll_loop_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_close_clears_executing_task.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_close_is_sync_and_race_safe.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_close_lock_free.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_closed_state_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_execute_executing_task_inside_try_frame.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_execute_messages_clear.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_executemany_validation_clears_executing_task.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_fetch_loop_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_finally_clear_executing_task_unconditional.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_late_add_proxy_swap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_loop_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_rownumber_closed_cross_loop.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_cursor_setinputsizes_loop_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_executemany_atomic.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_executemany_close_race.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_executescript_stub_raises_on_call.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_fetch_does_not_lazy_bind_loop.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_fetchall_row_factory_happy_path.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_force_close_transport.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_in_transaction_closed_foreign_loop.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_in_transaction_loop_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_invalidated_property.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_lock_binding.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_resource_warning_gate.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_secondary_methods_clear_messages_before_guards.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_transaction_owner_set_inside_try_frame.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_async_with_exit_does_not_close.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_audit_2026_05_coverage_gaps.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_audit_2026_05_dbapi_coverage.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_autocommit_doc_no_round_trip.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_autocommit_legacy_transaction_control.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_autocommit_property.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_autocommit_setter_exact_int_gate.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_binary_typeerror_stdlib_parity.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_bind_typeerror_as_dataerror.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_build_and_connect_base_exception_group_wrap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_build_and_connect_resolve_leader_oserror_wrap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_build_and_connect_wire_encode_error_translation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_call_client_base_exception_group_wrap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_call_client_baseexceptiongroup_cancel_split.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_call_client_docstring_mapping_table.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_call_client_mapping.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_call_client_typeerror_message_does_not_blame_bind.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_call_client_wire_encode_error.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_callproc_noreturn_annotation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_callproc_scroll_clear_messages.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cascade_cursors_clears_messages.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_classify_caller_sql.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_classify_caller_sql_hardening.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_classify_caller_sql_structural_reject_precedence.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_classify_operational_code_zero.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cleanup_loop_thread_disarms_inner_and_reaps_drain.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cleanup_loop_thread_finalizer_fork_safe.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_close_bypass_path_releases_latched_op_lock.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_close_resource_leak.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_close_signal_handler_reentry.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_close_surfaces_op_lock_contention.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_close_thread_join_uses_close_timeout.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_close_timeout_floor_enforcement.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_close_timeout_plumbing.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_closed_property.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cluster_policy_rejection_prefix_constant.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_code_to_exception_wire_ssot.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_collated_coverage_gaps.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_commit_after_external_close.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_commit_leader_flip_ambiguous_commit_error.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_commit_leader_flip_propagates.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_commit_no_spurious_connect.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_commit_rollback_shortcircuit_uses_in_transaction.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_conform_adapter_non_primitive_rejected_at_dbapi_layer.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_conftest_adapter_registry_isolation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connect_baseexceptiongroup_cancel_split.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connect_clears_messages.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connect_closed_precedes_thread_affinity.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connect_docstring_parity.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connect_iso_autocommit_round_trip.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connect_isolation_level_autocommit_kwargs_symmetric.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connect_preserves_code.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_after_fork_raises.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_autocommit_isolation_level_closed_state.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_close_cascades_cursors.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_close_cursor_cascade_atomicity.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_close_idempotent_cross_thread.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_closed_checks.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_cursor_class_attribute.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_cursor_closed_state_precedes_unknown_kwarg.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_database_empty_string_rejected.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_database_kwarg_isinstance.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_eager_address_validation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_enter_eager.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_executemany_outer_shape_check.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_exit_commit_ki_breadcrumb.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_exit_short_circuits_on_closed.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_messages_cleared_by_cursor_methods.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_connection_row_factory_setter_closed_check.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_context_manager_exit.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_convert_bind_param_conform_exception_propagates.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cte_prefix_stripper_edges.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_after_external_close.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_close_clears_messages.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_close_cross_thread.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_close_preserves_body_exception_on_messages_strip.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_close_releases_connection_pin.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_closed_after_connection_gc_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_closed_state_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_completed_iterations_resets_on_single_execute.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_connection_partial_init_attributeerror.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_connection_property_after_gc_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_execute_non_str_operation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_execute_state_reset_on_failure.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_executemany_non_str_operation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_executemany_outer_shape_check.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_factory_kwarg_rejected.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_force_close_race.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_iter_clears_messages_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_iterator_reset.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_kwargs_rejection_precedence.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_next_row_factory_raise_replays_row.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_per_row_types.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_post_close_property_reads.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_protocol_check.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_rejects_generator_params.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_repr_with_gced_parent_connection.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_row_factory_rejecting_setter.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_row_factory_subclass_inheritance.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_row_index_reset.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_rowcount_after_ddl_stdlib_parity.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_rownumber_affinity.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_rownumber_post_close.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_scroll_mode_validation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_secondary_methods_positional_only.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_setinputsizes_affinity_precedes_shape.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_setinputsizes_setoutputsize_closed_state_precedes_validation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_setters_clear_messages_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_stop_iteration_contract.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_cursor_uses_run_protocol.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_date_widens_to_datetime.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_datetime_from_iso8601_docstring_two_step_fallback.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_datetime_time_round_trip.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dbapi_commit_deferred_fk.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dbapi_default_timeouts_use_client_constants.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dbapi_exception_message_arg_cap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dbapi_exception_raw_message_cap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dbapi_fork_guard_cluster.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dbapi_hardening.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dbapi_type_hash_consistency.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_description_null_first_row_resolves.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_description_null_type_code_to_none.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_description_strict_type_codes.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_description_tuple_public_alias.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_diagnostic_messages.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dial_attempt_timeout_propagation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dial_func_propagation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dial_func_propagation_to_leader_resolve.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_doc_class_and_description_drift.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_dqlite_namespace_error_classification.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_ensure_loop_fast_path_snapshot_dcl_defence.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_ensure_loop_post_fork_pid_guard.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_error_code_mapping.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_error_pickle_cause_either_or.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_error_pickle_preserves_raw_message.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_exception_coded_mixin.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_exception_mapping.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_exception_repr_code.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_exception_wrapping.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_exceptions.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_execute_caller_param_shape_preserves_prior_state.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_execute_executemany_positional_only.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_execute_multi_statement_rejection.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_execute_reject_scrubs_prior_result_state.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_accumulator_semantics.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_basecaught_state_reset.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_cancel_restores_pre_batch_lastrowid.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_cancellederror_restores_pre_batch_lastrowid.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_classifier_resets_state_first.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_classify_caller_sql_hoisted.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_completed_iterations_snapshot_conditional_restore.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_dict_subclass_accepted.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_empty.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_empty_batch_preserves_lastrowid.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_none_seq_rejected.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_per_iter_structural_rejects.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_pragma_with_leading_comment.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_reject_scrubs_prior_result_state.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_rejects_pure_queries.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_rejects_transaction_verbs.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_executemany_returning_cap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_exit_logs_rollback_failure.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fetchall_row_factory_raise_replay.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fetchmany_edges.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fetchmany_negative_size_rejected.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fetchmany_non_int_validation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fetchmany_one_clear_per_call_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fetchmany_row_factory_raise_replay.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fetchmany_zero_docstring_stdlib_divergence.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_finalizer_strict_warnings.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_cancel_and_observe_narrow_suppress.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_observe_ast_pin.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_observe_callback_coverage.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_transport_cleanup_tail_unconditional.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_transport_clears_messages.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_transport_cross_thread_cancel_threadsafe.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_transport_cursor_cascade_threadsafe.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_transport_defensive_arms.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_transport_disarms_inner_finalizer.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_transport_pending_drain_resnapshot.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_force_close_transport_writer_cross_thread.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fork_diagnostic_includes_pids.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_fromticks_tz_policy.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_gc_cleanup.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_get_resolve_leader_cluster_no_loop_raises_runtime_error.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_in_transaction_after_close.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_in_transaction_closed_divergence_doc.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_in_transaction_closed_foreign_thread.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_in_transaction_property.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_init_docstring_fromticks_three_impls.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_is_multi_statement_walker.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_is_no_transaction_error.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_is_row_returning_multiline_whitespace.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_is_row_returning_sql_noise.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iso8601_aware_microseconds_round_trip.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iso8601_decoder_leap_seconds_rejected.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iso8601_decoder_subsecond_truncation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iso8601_encoder.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iso8601_encoder_fold.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iso8601_encoder_utcoffset_exception_wrap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iso8601_parse_error_truncation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iso8601_tzinfo_exception_classes_wrapped.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_isolation_level_stdlib_accept_set.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_iterdump_stub_signature_3_13_filter_kwarg.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_lastrowid_docstring_stdlib_parity_claim.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_lastrowid_insert_only.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_lastrowid_signed_int64_cast.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_leader_error_codes_identity.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_leader_redirect_on_connect.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_legacy_transaction_control_constant.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_max_continuation_frames_upper_bound.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_max_message_size_round_trip.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_max_total_rows_plumbing.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_max_total_rows_validation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_messages_attribute.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_messages_clear_on_ctxmgr_entry.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_nextset_noreturn_annotation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_no_transaction_substrings_source_of_truth.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_no_tx_primary_codes_invariant.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_op_lock_bounded_acquire.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_operational_error_classification.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_operational_error_raw_message.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_param_conversion.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_param_validation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_paramstyle_qmark_only_pin.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_parse_decltypes_colnames_constants.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_database_error_routing.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_exception_aliases.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_messages_clearing.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_messages_independence.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_misc_pins.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_stub_hasattr_divergence.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_type_constructors_vs_singletons.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_type_constructors_wrap_valueerror.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pep249_type_objects_final_annotated.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pickle_guard.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pragma_write_description_none.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_prepare_protocol_in_types_all.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_property_round_trip_and_fork_guard.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_property_setters_do_not_mirror_to_inner_async_conn.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_protocol_serialization.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_pyproject_dependency_graph.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_readme_truthfulness_pins.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_register_adapter_conform_fallback.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_register_adapter_global_sync_aio_symmetry.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_repr.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_repr_sanitizes_address.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_resolve_leader_cache_dial_func_id_recycle.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_resolve_leader_cache_lock_atfork_hook.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_resolve_leader_cluster_cache.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_resolve_leader_cross_loop_isolation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_row_factory_annotation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_row_factory_sqlite3_row_surfaces_dataerror.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_rowid_number_overlap_documented.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_rownumber_closed_cursor_returns_none_documented.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_ensure_loop_raise_closes_coroutine.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_keyboard_interrupt.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_keyboard_interrupt_cleanup_paths.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_ki_post_schedule_pre_await.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_ki_race_recovery.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_loop_closed_during_schedule.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_other_runtimeerror_propagates.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_timeout.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_run_sync_timeout_recovery_preserves_conn.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_secondary_methods_clear_messages_before_thread_check.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_setinputsizes_accepts_none.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_setinputsizes_clears_messages_per_pep249.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_setinputsizes_non_sequence_iterable.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_setinputsizes_sequence_acceptance.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_setinputsizes_short_circuit_on_closed_connection.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_setoutputsize_accepts_none.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_setter_closed_state_precedes_thread.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sqlite_errorname_property.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sqlite_version_not_public_attribute.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_strip_leading_comments_bom.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_strip_leading_comments_parity.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_strip_sql_noise_backtick_identifier.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_stub_signatures_normalize_to_dbapi_error.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_submodule_all.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_commit_rollback_closed_precedes_thread.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_commit_rollback_inside_ctxmgr_guard.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_commit_rollback_messages_under_lock.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_commit_rollback_retry_after_invalidate.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_connection_execute_shortcut.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_connection_executemany_shortcut.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_cursor_post_add_closed_recheck.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_exit_signal_mid_rollback.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_fetchmany_cancel_atomic.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_force_close_transport.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_force_close_transport_pending_drain_reap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_invalidated_property.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_sync_run_sync_multiphase_timeout.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_timeout_validation.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_top_level_logger_has_null_handler.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_total_changes_hasattr_safe.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_transitive_wire_import.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_type_coercion_documented_asymmetries.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_types_dbapi_compare.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_types_validator_branches.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_unregister_adapter.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_unregister_adapter_rejects_built_in_datetime_types.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_unsupported_method_stubs.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_validate_positive_int_pep249_wrap.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_validate_ticks.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_validate_ticks_and_unixtime_reject_bool_and_non_numeric.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_version.py +0 -0
- {dqlite_dbapi-0.2.0 → dqlite_dbapi-0.2.1}/tests/test_version_export.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dqlite-dbapi
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: PEP 249 (DB-API 2.0) compliant interface for dqlite
|
|
5
5
|
Project-URL: Homepage, https://github.com/letsdiscodev/python-dqlite-dbapi
|
|
6
6
|
Project-URL: Repository, https://github.com/letsdiscodev/python-dqlite-dbapi
|
|
@@ -22,8 +22,8 @@ Classifier: Topic :: Database :: Database Engines/Servers
|
|
|
22
22
|
Classifier: Topic :: Database :: Front-Ends
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
24
|
Requires-Python: >=3.13
|
|
25
|
-
Requires-Dist: dqlite-client<0.3.0,>=0.2.
|
|
26
|
-
Requires-Dist: dqlite-wire<0.3.0,>=0.2.
|
|
25
|
+
Requires-Dist: dqlite-client<0.3.0,>=0.2.1
|
|
26
|
+
Requires-Dist: dqlite-wire<0.3.0,>=0.2.1
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
29
29
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
@@ -73,7 +73,9 @@ asyncio.run(main())
|
|
|
73
73
|
## PEP 249 Compliance
|
|
74
74
|
|
|
75
75
|
- `apilevel = "2.0"`
|
|
76
|
-
- `threadsafety =
|
|
76
|
+
- `threadsafety = 2` (capability ceiling; default `check_same_thread=True`
|
|
77
|
+
enforces strict per-thread — matches stdlib sqlite3's "advertise
|
|
78
|
+
ceiling, default to floor" pattern)
|
|
77
79
|
- `paramstyle = "qmark"`
|
|
78
80
|
|
|
79
81
|
## Transactions
|
|
@@ -240,10 +242,16 @@ borrowed from one.
|
|
|
240
242
|
stdlib silently accepts empty / whitespace-only / comment-only SQL
|
|
241
243
|
as a no-op; the driver pre-flight rejects per PEP 249 §7 to surface
|
|
242
244
|
caller bugs at the call site.
|
|
243
|
-
- **`threadsafety =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
- **`threadsafety = 2`** (stdlib reports `3` — we don't claim Tier 3
|
|
246
|
+
because cursors aren't shareable per the documented "share
|
|
247
|
+
connections, not cursors" contract). Tier 2 = "threads may share
|
|
248
|
+
the module AND connections"; matches the dbapi's
|
|
249
|
+
``check_same_thread=False`` opt-in behaviour. Default
|
|
250
|
+
``check_same_thread=True`` enforces strict per-thread; methods
|
|
251
|
+
called from a foreign thread on a default-mode Connection raise
|
|
252
|
+
``ProgrammingError``. Tier 3 (cursor sharing) is tracked as
|
|
253
|
+
future work in
|
|
254
|
+
``issues/dbapi-threadsafety-tier-3-cursor-sharing-stdlib-parity.md``.
|
|
247
255
|
- **No `executescript` / `create_function` / `create_aggregate` /
|
|
248
256
|
`create_window_function` / `iterdump` / `backup` / `set_authorizer`
|
|
249
257
|
/ `serialize` / `blobopen`.** stdlib-specific APIs that have no
|
|
@@ -39,7 +39,9 @@ asyncio.run(main())
|
|
|
39
39
|
## PEP 249 Compliance
|
|
40
40
|
|
|
41
41
|
- `apilevel = "2.0"`
|
|
42
|
-
- `threadsafety =
|
|
42
|
+
- `threadsafety = 2` (capability ceiling; default `check_same_thread=True`
|
|
43
|
+
enforces strict per-thread — matches stdlib sqlite3's "advertise
|
|
44
|
+
ceiling, default to floor" pattern)
|
|
43
45
|
- `paramstyle = "qmark"`
|
|
44
46
|
|
|
45
47
|
## Transactions
|
|
@@ -206,10 +208,16 @@ borrowed from one.
|
|
|
206
208
|
stdlib silently accepts empty / whitespace-only / comment-only SQL
|
|
207
209
|
as a no-op; the driver pre-flight rejects per PEP 249 §7 to surface
|
|
208
210
|
caller bugs at the call site.
|
|
209
|
-
- **`threadsafety =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
- **`threadsafety = 2`** (stdlib reports `3` — we don't claim Tier 3
|
|
212
|
+
because cursors aren't shareable per the documented "share
|
|
213
|
+
connections, not cursors" contract). Tier 2 = "threads may share
|
|
214
|
+
the module AND connections"; matches the dbapi's
|
|
215
|
+
``check_same_thread=False`` opt-in behaviour. Default
|
|
216
|
+
``check_same_thread=True`` enforces strict per-thread; methods
|
|
217
|
+
called from a foreign thread on a default-mode Connection raise
|
|
218
|
+
``ProgrammingError``. Tier 3 (cursor sharing) is tracked as
|
|
219
|
+
future work in
|
|
220
|
+
``issues/dbapi-threadsafety-tier-3-cursor-sharing-stdlib-parity.md``.
|
|
213
221
|
- **No `executescript` / `create_function` / `create_aggregate` /
|
|
214
222
|
`create_window_function` / `iterdump` / `backup` / `set_authorizer`
|
|
215
223
|
/ `serialize` / `blobopen`.** stdlib-specific APIs that have no
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dqlite-dbapi"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "PEP 249 (DB-API 2.0) compliant interface for dqlite"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
@@ -26,8 +26,8 @@ classifiers = [
|
|
|
26
26
|
"Typing :: Typed",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"dqlite-client>=0.2.
|
|
30
|
-
"dqlite-wire>=0.2.
|
|
29
|
+
"dqlite-client>=0.2.1,<0.3.0",
|
|
30
|
+
"dqlite-wire>=0.2.1,<0.3.0",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[tool.uv.sources]
|
|
@@ -107,14 +107,38 @@ from dqlitewire import (
|
|
|
107
107
|
|
|
108
108
|
# PEP 249 module-level attributes
|
|
109
109
|
apilevel: _Final[_Literal["2.0"]] = "2.0"
|
|
110
|
-
# PEP 249 value
|
|
110
|
+
# PEP 249 value 2: threads may share the module and connections.
|
|
111
111
|
#
|
|
112
|
-
# This
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
|
|
112
|
+
# This declares the driver's CAPABILITY ceiling: connections are
|
|
113
|
+
# safe to share across threads once the user opts in via
|
|
114
|
+
# ``check_same_thread=False`` at ``connect()`` (matches stdlib
|
|
115
|
+
# sqlite3's convention — stdlib advertises ``threadsafety = 3``
|
|
116
|
+
# while defaulting ``check_same_thread=True`` to enforce strict
|
|
117
|
+
# per-thread at the Python layer). Cursors are explicitly NOT
|
|
118
|
+
# shareable across threads — the documented contract is "share
|
|
119
|
+
# connections, not cursors" (see ``Connection`` docstring); the
|
|
120
|
+
# ceiling stays at PEP 249 tier 2 for that reason. Tier 3 (cursors
|
|
121
|
+
# shareable too) is tracked as future work in
|
|
122
|
+
# ``issues/dbapi-threadsafety-tier-3-cursor-sharing-stdlib-parity.md``.
|
|
123
|
+
#
|
|
124
|
+
# DEFAULT enforcement is still per-thread: ``check_same_thread``
|
|
125
|
+
# defaults to ``True`` and ``Connection._check_thread()`` raises
|
|
126
|
+
# ``ProgrammingError`` on cross-thread method calls. Pass
|
|
127
|
+
# ``check_same_thread=False`` to use the connection sharing
|
|
128
|
+
# advertised here. The wire is always serialised by ``_op_lock``;
|
|
129
|
+
# the Phase 2 hardening (``_state_lock`` for transaction owner
|
|
130
|
+
# read-check-reserve, ``_cursors`` WeakSet add/discard) closes
|
|
131
|
+
# the cross-thread correctness gaps for the connection-shared
|
|
132
|
+
# path.
|
|
133
|
+
#
|
|
134
|
+
# This declaration uses the "advertise ceiling, default to floor"
|
|
135
|
+
# pattern stdlib sqlite3 established since CPython 3.11
|
|
136
|
+
# (bpo-45613 / gh-31464 made ``sqlite3.threadsafety`` dynamic via
|
|
137
|
+
# ``SQLITE_THREADSAFE`` mode; serialized = tier 3). The pattern
|
|
138
|
+
# describes capability rather than default enforcement;
|
|
139
|
+
# capability-detection libraries reading this value learn the
|
|
140
|
+
# safe-with-opt-in ceiling.
|
|
141
|
+
threadsafety: _Final[_Literal[2]] = 2
|
|
118
142
|
paramstyle: _Final[_Literal["qmark"]] = "qmark" # Question mark style: WHERE name=?
|
|
119
143
|
|
|
120
144
|
# SQLite compatibility attributes (for SQLAlchemy).
|
|
@@ -147,7 +171,7 @@ LEGACY_TRANSACTION_CONTROL: _Final[int] = -1
|
|
|
147
171
|
PARSE_DECLTYPES: _Final[int] = 1
|
|
148
172
|
PARSE_COLNAMES: _Final[int] = 2
|
|
149
173
|
|
|
150
|
-
__version__: _Final[str] = "0.2.
|
|
174
|
+
__version__: _Final[str] = "0.2.1"
|
|
151
175
|
|
|
152
176
|
__all__ = [ # grouped by PEP 249 section, not alphabetical
|
|
153
177
|
# Module attributes
|
|
@@ -222,6 +246,8 @@ def connect(
|
|
|
222
246
|
dial_timeout: float | None = None,
|
|
223
247
|
attempt_timeout: float | None = None,
|
|
224
248
|
dial_func: DialFunc | None = None,
|
|
249
|
+
busy_timeout: float = 5.0,
|
|
250
|
+
check_same_thread: bool = True,
|
|
225
251
|
**unknown_kwargs: object,
|
|
226
252
|
) -> Connection:
|
|
227
253
|
"""Connect to a dqlite database.
|
|
@@ -269,6 +295,33 @@ def connect(
|
|
|
269
295
|
``asyncio.open_connection`` path. Forwarded to the
|
|
270
296
|
underlying :class:`Connection`. See
|
|
271
297
|
:data:`dqliteclient.DialFunc` for the protocol.
|
|
298
|
+
busy_timeout: Maximum cumulative seconds to spend retrying
|
|
299
|
+
BUSY responses before raising. Default ``5.0`` matches
|
|
300
|
+
stdlib ``sqlite3.connect(timeout=5.0)``. Retries follow
|
|
301
|
+
SQLite's deterministic ``sqliteDefaultBusyCallback``
|
|
302
|
+
curve (1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50, 100 ms
|
|
303
|
+
then flat 100 ms). ``0`` disables retry (first BUSY
|
|
304
|
+
raises immediately — stdlib parity). dqlite's VFS
|
|
305
|
+
authorizer rejects the canonical ``PRAGMA busy_timeout``
|
|
306
|
+
on the server side; this driver intercepts the PRAGMA at
|
|
307
|
+
the Cursor layer so ``cur.execute("PRAGMA busy_timeout
|
|
308
|
+
= 30000")`` works transparently and updates the same
|
|
309
|
+
backing field as the kwarg.
|
|
310
|
+
check_same_thread: When ``True`` (default), every method
|
|
311
|
+
call on the returned Connection must come from the
|
|
312
|
+
thread that called ``connect()``; cross-thread calls
|
|
313
|
+
raise ``ProgrammingError``. When ``False``, the cross-
|
|
314
|
+
thread check is gated off and the Connection can be
|
|
315
|
+
shared across threads — the wire is already serialised
|
|
316
|
+
by the per-Connection op-lock regardless of caller
|
|
317
|
+
thread. Per-cursor result state is NOT thread-safe; the
|
|
318
|
+
documented pattern is "share connections, not cursors."
|
|
319
|
+
Matches stdlib ``sqlite3.connect(check_same_thread=
|
|
320
|
+
False)``. The fork check is NEVER relaxed; cross-process
|
|
321
|
+
Connection use raises ``InterfaceError`` in both modes.
|
|
322
|
+
See ``dqlitedbapi.Connection`` docstring for the
|
|
323
|
+
cursor / messages / close() contracts under the
|
|
324
|
+
relaxation.
|
|
272
325
|
|
|
273
326
|
Returns:
|
|
274
327
|
A Connection object
|
|
@@ -323,11 +376,10 @@ def connect(
|
|
|
323
376
|
f"(stdlib LEGACY_TRANSACTION_CONTROL) only; got {autoc!r}"
|
|
324
377
|
)
|
|
325
378
|
|
|
326
|
-
# Reject stdlib ``sqlite3.connect`` kwargs that this driver
|
|
327
|
-
# cannot honour (``detect_types``, ``
|
|
328
|
-
# ``
|
|
329
|
-
# ``
|
|
330
|
-
# protocol leak ``TypeError`` (which escapes ``except
|
|
379
|
+
# Reject other stdlib ``sqlite3.connect`` kwargs that this driver
|
|
380
|
+
# cannot honour (``detect_types``, ``factory``, ``cached_statements``,
|
|
381
|
+
# ``uri``) with ``NotSupportedError`` rather than letting Python's
|
|
382
|
+
# call-protocol leak ``TypeError`` (which escapes ``except
|
|
331
383
|
# dbapi.Error:``). Cross-driver code that passes stdlib kwargs
|
|
332
384
|
# through should be able to catch the rejection inside the
|
|
333
385
|
# dbapi error hierarchy.
|
|
@@ -352,6 +404,8 @@ def connect(
|
|
|
352
404
|
dial_timeout=dial_timeout,
|
|
353
405
|
attempt_timeout=attempt_timeout,
|
|
354
406
|
dial_func=dial_func,
|
|
407
|
+
busy_timeout=busy_timeout,
|
|
408
|
+
check_same_thread=check_same_thread,
|
|
355
409
|
)
|
|
356
410
|
# Apply the validated ``isolation_level`` / ``autocommit`` kwargs
|
|
357
411
|
# via the setters on the freshly-constructed connection. Stdlib
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Server-BUSY retry mechanism — stdlib ``sqlite3`` parity.
|
|
2
|
+
|
|
3
|
+
Stdlib ``sqlite3.connect(timeout=N)`` defaults to a 5-second
|
|
4
|
+
``busy_timeout`` at the SQLite C library level. When a write hits
|
|
5
|
+
contention the C library transparently sleeps and retries via a
|
|
6
|
+
deterministic curve (``main.c::sqliteDefaultBusyCallback``). dqlite's
|
|
7
|
+
server-side VFS authorizer denies ``PRAGMA busy_timeout`` (see
|
|
8
|
+
``done/dbapi-pragma-deny-list-no-regression-pin.md``), so callers
|
|
9
|
+
cannot tune the retry behaviour through the canonical SQLite escape
|
|
10
|
+
hatch. Without an in-driver retry, application code that worked
|
|
11
|
+
against stdlib ``sqlite3`` silently regresses on dqlite — two
|
|
12
|
+
concurrent writers contending for the dqlite write lock produce
|
|
13
|
+
``OperationalError: database is locked to <addr>`` surfacing as HTTP
|
|
14
|
+
500s.
|
|
15
|
+
|
|
16
|
+
This module hosts the SQLite-equivalent retry curve and the
|
|
17
|
+
``OperationalError`` ↔ BUSY classifier so the sync and async surfaces
|
|
18
|
+
can share both. The actual retry-loop helpers live on the
|
|
19
|
+
``Connection`` / ``AsyncConnection`` classes (they need access to the
|
|
20
|
+
per-connection ``busy_timeout`` and the run-sync / run-async
|
|
21
|
+
bridges).
|
|
22
|
+
|
|
23
|
+
Design notes:
|
|
24
|
+
|
|
25
|
+
- The curve is SQLite's own (``delays`` / ``totals`` arrays from
|
|
26
|
+
``main.c``). Battle-tested. No jitter — BUSY is local contention,
|
|
27
|
+
not distributed-systems flapping; jitter would only smear the
|
|
28
|
+
retry burst across siblings without changing the convergence shape.
|
|
29
|
+
|
|
30
|
+
- The classifier gates on ``code == SQLITE_BUSY`` not on a substring
|
|
31
|
+
scan. The user-supplied brief referenced
|
|
32
|
+
``dqliteclient/connection.py:_RAFT_BUSY_MESSAGE_FRAGMENTS`` but
|
|
33
|
+
that tuple is narrow (``("checkpoint in progress",)``) and is the
|
|
34
|
+
classifier for the SEPARATE "checkpoint-BUSY rewrap as
|
|
35
|
+
``DqliteConnectionError``" path — NOT the RAFT-BUSY-"is locked"
|
|
36
|
+
family this retry targets. Code-based gating catches both engine-
|
|
37
|
+
BUSY and RAFT-BUSY shapes per the existing comment at
|
|
38
|
+
``dqliteclient/connection.py:2683-2698``.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import asyncio
|
|
44
|
+
import logging
|
|
45
|
+
import time
|
|
46
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
47
|
+
from typing import Any, Final
|
|
48
|
+
|
|
49
|
+
from dqlitedbapi.exceptions import OperationalError as _OperationalError
|
|
50
|
+
from dqlitewire import SQLITE_BUSY as _SQLITE_BUSY
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_busy_timeout_seconds(connection: object) -> float:
|
|
56
|
+
"""Read ``connection._busy_timeout`` with a MagicMock-safe
|
|
57
|
+
fallback. Test fixtures construct ``Cursor`` instances with
|
|
58
|
+
``MagicMock`` connections whose attributes auto-spawn — without
|
|
59
|
+
this guard the retry curve's ``int(busy_timeout * 1000)`` would
|
|
60
|
+
crash on a non-numeric MagicMock. ``isinstance`` check on the
|
|
61
|
+
return value falls through to ``0.0`` (no retry — preserves the
|
|
62
|
+
pre-feature test semantics)."""
|
|
63
|
+
value = getattr(connection, "_busy_timeout", 0.0)
|
|
64
|
+
# ``bool`` is ``int`` to ``isinstance`` but must be rejected
|
|
65
|
+
# explicitly; production code rejects bool at validation. A
|
|
66
|
+
# MagicMock attribute is not a real number — fall through.
|
|
67
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
68
|
+
return 0.0
|
|
69
|
+
return float(value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# SQLite's busy-callback curve (``main.c::sqliteDefaultBusyCallback``).
|
|
73
|
+
# ``delays[i]`` is the milliseconds to sleep before retry attempt
|
|
74
|
+
# ``i+1``; ``totals[i]`` is the cumulative sleep BEFORE attempt
|
|
75
|
+
# ``i`` (so ``totals[i+1] == totals[i] + delays[i]``). After
|
|
76
|
+
# ``len(delays)`` attempts the curve flattens at the last
|
|
77
|
+
# ``delays`` value (100 ms) — see ``next_busy_delay_ms`` below.
|
|
78
|
+
#
|
|
79
|
+
# Pinned to the canonical SQLite values (verified internally
|
|
80
|
+
# consistent via the totals[i+1] = totals[i] + delays[i] check
|
|
81
|
+
# in ``tests/test_busy_retry_curve.py``). The constant 25 appears
|
|
82
|
+
# three times in a row at positions 6, 7, 8 — this is intentional
|
|
83
|
+
# in the upstream curve, not an editing mistake.
|
|
84
|
+
_BUSY_DELAYS_MS: Final[tuple[int, ...]] = (1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50, 100)
|
|
85
|
+
_BUSY_TOTALS_MS: Final[tuple[int, ...]] = (0, 1, 3, 8, 18, 33, 53, 78, 103, 128, 178, 228)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def next_busy_delay_ms(count: int, busy_timeout_ms: int) -> int | None:
|
|
89
|
+
"""Return the sleep duration (ms) before retry attempt ``count+1``,
|
|
90
|
+
or ``None`` when the cumulative budget is exhausted.
|
|
91
|
+
|
|
92
|
+
Mirrors SQLite's ``sqliteDefaultBusyCallback`` exactly:
|
|
93
|
+
|
|
94
|
+
- For ``count < len(_BUSY_DELAYS_MS)``, return the next entry from
|
|
95
|
+
the curve.
|
|
96
|
+
- For ``count >= len(_BUSY_DELAYS_MS)``, return the flat tail
|
|
97
|
+
value (100 ms).
|
|
98
|
+
- If the resulting ``prior + delay`` would overrun
|
|
99
|
+
``busy_timeout_ms``, clamp ``delay`` to fit; if the clamp would
|
|
100
|
+
produce ``delay <= 0``, return ``None`` (caller raises the
|
|
101
|
+
original BUSY).
|
|
102
|
+
|
|
103
|
+
``busy_timeout_ms <= 0`` short-circuits ``None`` regardless of
|
|
104
|
+
``count`` — stdlib parity for ``timeout=0`` meaning "no wait."
|
|
105
|
+
"""
|
|
106
|
+
if busy_timeout_ms <= 0:
|
|
107
|
+
return None
|
|
108
|
+
n_curve = len(_BUSY_DELAYS_MS)
|
|
109
|
+
if count < n_curve:
|
|
110
|
+
delay = _BUSY_DELAYS_MS[count]
|
|
111
|
+
prior = _BUSY_TOTALS_MS[count]
|
|
112
|
+
else:
|
|
113
|
+
delay = _BUSY_DELAYS_MS[-1]
|
|
114
|
+
prior = _BUSY_TOTALS_MS[-1] + delay * (count - (n_curve - 1))
|
|
115
|
+
if prior + delay > busy_timeout_ms:
|
|
116
|
+
delay = busy_timeout_ms - prior
|
|
117
|
+
if delay <= 0:
|
|
118
|
+
return None
|
|
119
|
+
return delay
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_busy_exception(exc: BaseException) -> bool:
|
|
123
|
+
"""Classify an exception as a retryable BUSY.
|
|
124
|
+
|
|
125
|
+
The retry mechanism gates on ``OperationalError`` whose ``code`` is
|
|
126
|
+
``SQLITE_BUSY (5)``. This covers both engine-BUSY (the SQLite-side
|
|
127
|
+
BUSY where the user can retry the failing statement and continue
|
|
128
|
+
the same tx) AND RAFT-BUSY translated by the server's gateway
|
|
129
|
+
(the in-flight write was not accepted; the server-side tx state
|
|
130
|
+
is the same as engine-BUSY from the client's perspective).
|
|
131
|
+
|
|
132
|
+
The two shapes are indistinguishable at the Python layer per the
|
|
133
|
+
existing comment at ``dqliteclient/connection.py:2683-2698``
|
|
134
|
+
("users must retry explicitly"). This retry IS that explicit
|
|
135
|
+
retry, applied uniformly across both BUSY origins.
|
|
136
|
+
|
|
137
|
+
Specifically NOT retried:
|
|
138
|
+
|
|
139
|
+
- ``DqliteConnectionError`` (the checkpoint-BUSY rewrap at
|
|
140
|
+
``dqliteclient/connection.py:2717``) — bypasses this classifier
|
|
141
|
+
because it's not an ``OperationalError`` subclass. The
|
|
142
|
+
checkpoint-BUSY case already triggers SA pool invalidation by
|
|
143
|
+
design (the connection's tx state was reset; retry on the same
|
|
144
|
+
connection is wrong).
|
|
145
|
+
- Other ``OperationalError`` codes (LEADER_ERROR_CODES,
|
|
146
|
+
INTERFACE_ERROR_CODES, etc.) — leader-flip and transport
|
|
147
|
+
faults have their own retry semantics elsewhere; this gate
|
|
148
|
+
narrows to BUSY only.
|
|
149
|
+
"""
|
|
150
|
+
if not isinstance(exc, _OperationalError):
|
|
151
|
+
return False
|
|
152
|
+
return getattr(exc, "code", None) == _SQLITE_BUSY
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def retry_sync_on_busy[T](
|
|
156
|
+
busy_timeout: float,
|
|
157
|
+
run_sync: Callable[[Coroutine[Any, Any, T]], T],
|
|
158
|
+
coro_factory: Callable[[], Coroutine[Any, Any, T]],
|
|
159
|
+
) -> T:
|
|
160
|
+
"""Module-level sync retry loop.
|
|
161
|
+
|
|
162
|
+
Caller provides ``run_sync`` (the function that drives the
|
|
163
|
+
coroutine on the connection's daemon loop — typically
|
|
164
|
+
``connection._run_sync``) and ``coro_factory`` (a callable that
|
|
165
|
+
returns a fresh coroutine each time the retry needs to re-submit).
|
|
166
|
+
Coroutines are single-use so the factory shape is mandatory.
|
|
167
|
+
|
|
168
|
+
Same retry shape as the inline-on-connection variants: SQLite-curve
|
|
169
|
+
sleeps, BUSY-only classification (``OperationalError(code=
|
|
170
|
+
SQLITE_BUSY)``), propagate non-BUSY exceptions unchanged.
|
|
171
|
+
``KeyboardInterrupt`` / ``SystemExit`` during ``time.sleep``
|
|
172
|
+
propagate (those are ``BaseException``, not caught by ``except
|
|
173
|
+
OperationalError``).
|
|
174
|
+
|
|
175
|
+
``busy_timeout <= 0`` short-circuits to a single ``run_sync``
|
|
176
|
+
call with no retry (stdlib parity for ``timeout=0``).
|
|
177
|
+
|
|
178
|
+
**Cross-thread caveat under** ``check_same_thread=False``:
|
|
179
|
+
|
|
180
|
+
The retry loop releases the connection's ``_op_lock`` between
|
|
181
|
+
attempts. ``run_sync`` acquires the lock at the START of each
|
|
182
|
+
attempt and releases it before returning; the ``time.sleep``
|
|
183
|
+
that follows runs OUTSIDE the lock so the sleep doesn't starve
|
|
184
|
+
sibling threads. By design — holding ``_op_lock`` across a
|
|
185
|
+
100ms sleep would defeat cross-thread sharing.
|
|
186
|
+
|
|
187
|
+
On a Connection shared across threads via
|
|
188
|
+
``check_same_thread=False``, a BUSY-retried statement may
|
|
189
|
+
observe interleaved writes from sibling threads between retry
|
|
190
|
+
attempts. Example:
|
|
191
|
+
|
|
192
|
+
Thread A: cur.execute("UPDATE t SET v=v+1 WHERE k=?") -> BUSY
|
|
193
|
+
Thread A: sleeps for SQLite-curve delay (op_lock released)
|
|
194
|
+
Thread B: cur.execute("UPDATE t SET v=v+1 WHERE k=?") -> succeeds
|
|
195
|
+
Thread A: retries -> sees B's update; may succeed against
|
|
196
|
+
a different row state than the original attempt.
|
|
197
|
+
|
|
198
|
+
This is the correct semantic for retry-on-contention (each
|
|
199
|
+
retry sees current state, the same as it would on a fresh
|
|
200
|
+
connection). Callers who need to retry against an atomic
|
|
201
|
+
snapshot must wrap in an explicit transaction:
|
|
202
|
+
|
|
203
|
+
with conn.transaction():
|
|
204
|
+
cur.execute("UPDATE t SET v=v+1 WHERE k=?", (k,))
|
|
205
|
+
|
|
206
|
+
The transaction holds the wire lock across the entire body, so
|
|
207
|
+
sibling threads cannot interleave statements between the retry
|
|
208
|
+
attempts inside that body.
|
|
209
|
+
"""
|
|
210
|
+
busy_timeout_ms = int(busy_timeout * 1000)
|
|
211
|
+
if busy_timeout_ms <= 0:
|
|
212
|
+
return run_sync(coro_factory())
|
|
213
|
+
count = 0
|
|
214
|
+
while True:
|
|
215
|
+
try:
|
|
216
|
+
return run_sync(coro_factory())
|
|
217
|
+
except _OperationalError as exc:
|
|
218
|
+
if not is_busy_exception(exc):
|
|
219
|
+
raise
|
|
220
|
+
delay_ms = next_busy_delay_ms(count, busy_timeout_ms)
|
|
221
|
+
if delay_ms is None:
|
|
222
|
+
logger.warning(
|
|
223
|
+
"busy_timeout budget %.3fs exhausted after %d retries; raising original BUSY",
|
|
224
|
+
busy_timeout,
|
|
225
|
+
count,
|
|
226
|
+
)
|
|
227
|
+
raise
|
|
228
|
+
logger.debug(
|
|
229
|
+
"BUSY retry attempt %d: sleeping %dms (budget %.3fs)",
|
|
230
|
+
count,
|
|
231
|
+
delay_ms,
|
|
232
|
+
busy_timeout,
|
|
233
|
+
)
|
|
234
|
+
time.sleep(delay_ms / 1000.0)
|
|
235
|
+
count += 1
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def retry_async_on_busy[T](
|
|
239
|
+
busy_timeout: float,
|
|
240
|
+
coro_factory: Callable[[], Awaitable[T]],
|
|
241
|
+
) -> T:
|
|
242
|
+
"""Module-level async retry loop. Used by the per-iteration
|
|
243
|
+
executemany path that cannot route through
|
|
244
|
+
``Connection._await_with_busy_retry`` because test fixtures
|
|
245
|
+
construct ``Cursor`` instances with a ``MagicMock`` connection
|
|
246
|
+
that does not have that method.
|
|
247
|
+
|
|
248
|
+
Same retry shape as the connection-method variants: SQLite-curve
|
|
249
|
+
sleeps, BUSY-only classification, propagate non-BUSY unchanged.
|
|
250
|
+
``CancelledError`` propagates through ``await asyncio.sleep``
|
|
251
|
+
because the ``except OperationalError`` arm does not catch
|
|
252
|
+
``BaseException``.
|
|
253
|
+
|
|
254
|
+
``busy_timeout <= 0`` short-circuits to a single awaitable call
|
|
255
|
+
with no retry (stdlib parity).
|
|
256
|
+
|
|
257
|
+
**Cross-task caveat**:
|
|
258
|
+
|
|
259
|
+
Mirrors the sync sibling's cross-thread caveat: the retry loop
|
|
260
|
+
releases any wire-level lock (the inner ``DqliteConnection``'s
|
|
261
|
+
op_lock or the AsyncConnection's op_lock) between attempts.
|
|
262
|
+
``await asyncio.sleep`` runs OUTSIDE the lock so siblings can
|
|
263
|
+
make progress; by design.
|
|
264
|
+
|
|
265
|
+
On an AsyncConnection shared across asyncio tasks on the same
|
|
266
|
+
loop, a BUSY-retried statement may observe interleaved writes
|
|
267
|
+
from sibling tasks between retry attempts. Same shape as the
|
|
268
|
+
sync sibling: each retry sees current state. Callers who need
|
|
269
|
+
to retry against an atomic snapshot must wrap in an explicit
|
|
270
|
+
``async with conn.transaction():`` block.
|
|
271
|
+
"""
|
|
272
|
+
busy_timeout_ms = int(busy_timeout * 1000)
|
|
273
|
+
if busy_timeout_ms <= 0:
|
|
274
|
+
return await coro_factory()
|
|
275
|
+
count = 0
|
|
276
|
+
while True:
|
|
277
|
+
try:
|
|
278
|
+
return await coro_factory()
|
|
279
|
+
except _OperationalError as exc:
|
|
280
|
+
if not is_busy_exception(exc):
|
|
281
|
+
raise
|
|
282
|
+
delay_ms = next_busy_delay_ms(count, busy_timeout_ms)
|
|
283
|
+
if delay_ms is None:
|
|
284
|
+
logger.warning(
|
|
285
|
+
"busy_timeout budget %.3fs exhausted after %d retries; raising original BUSY",
|
|
286
|
+
busy_timeout,
|
|
287
|
+
count,
|
|
288
|
+
)
|
|
289
|
+
raise
|
|
290
|
+
logger.debug(
|
|
291
|
+
"BUSY retry attempt %d: sleeping %dms (budget %.3fs)",
|
|
292
|
+
count,
|
|
293
|
+
delay_ms,
|
|
294
|
+
busy_timeout,
|
|
295
|
+
)
|
|
296
|
+
await asyncio.sleep(delay_ms / 1000.0)
|
|
297
|
+
count += 1
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""``PRAGMA busy_timeout`` interception at the dbapi cursor layer.
|
|
2
|
+
|
|
3
|
+
dqlite's C VFS authorizer rejects ``PRAGMA busy_timeout`` (see prior
|
|
4
|
+
issue ``done/dbapi-pragma-deny-list-no-regression-pin.md``), so the
|
|
5
|
+
canonical SQLite escape hatch — ``PRAGMA busy_timeout = N`` — would
|
|
6
|
+
otherwise raise ``DatabaseError("not authorized")`` on the dqlite
|
|
7
|
+
server. To preserve stdlib parity (and to give callers a single
|
|
8
|
+
canonical knob), this module intercepts the PRAGMA at the dbapi
|
|
9
|
+
cursor layer BEFORE the wire round-trip:
|
|
10
|
+
|
|
11
|
+
- Setter (``PRAGMA busy_timeout = N`` or ``PRAGMA busy_timeout(N)``)
|
|
12
|
+
updates ``connection._busy_timeout`` in seconds (N is ms per
|
|
13
|
+
SQLite convention; we divide by 1000) and emits the new value as
|
|
14
|
+
a single-row result, matching stdlib's PRAGMA result-set shape.
|
|
15
|
+
- Getter (``PRAGMA busy_timeout``) emits the current value as a
|
|
16
|
+
single-row result.
|
|
17
|
+
|
|
18
|
+
Case-insensitive and whitespace-tolerant per SQLite parser
|
|
19
|
+
conventions. Multi-statement strings ARE rejected (only a SINGLE
|
|
20
|
+
``PRAGMA busy_timeout`` statement is intercepted; multi-statement
|
|
21
|
+
inputs surface to the wire where the existing multi-statement
|
|
22
|
+
classifier rejects them with ``ProgrammingError`` — that path is
|
|
23
|
+
unchanged).
|
|
24
|
+
|
|
25
|
+
The interception writes the cursor's result state directly:
|
|
26
|
+
|
|
27
|
+
- ``_description = (("busy_timeout", None, None, None, None, None, None),)``
|
|
28
|
+
- ``_rows = [(N_ms_clamped,)]``
|
|
29
|
+
- ``_rowcount = -1`` (PRAGMA does not have a meaningful row count)
|
|
30
|
+
- ``_row_index = 0``
|
|
31
|
+
|
|
32
|
+
Then returns True so the caller short-circuits before the wire
|
|
33
|
+
round-trip.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import re
|
|
39
|
+
from typing import TYPE_CHECKING
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from dqlitedbapi.cursor import Cursor
|
|
43
|
+
|
|
44
|
+
# Match ``PRAGMA busy_timeout`` with optional setter forms:
|
|
45
|
+
#
|
|
46
|
+
# PRAGMA busy_timeout
|
|
47
|
+
# PRAGMA busy_timeout = 5000
|
|
48
|
+
# PRAGMA busy_timeout=5000
|
|
49
|
+
# pragma BUSY_TIMEOUT = 5000
|
|
50
|
+
# PRAGMA busy_timeout(5000)
|
|
51
|
+
#
|
|
52
|
+
# The trailing ``;?`` accommodates the canonical single-statement
|
|
53
|
+
# terminator. A semicolon followed by additional content (multi-
|
|
54
|
+
# statement) does NOT match — those route to the wire where the
|
|
55
|
+
# existing multi-statement classifier rejects them.
|
|
56
|
+
_PRAGMA_BUSY_TIMEOUT_RE = re.compile(
|
|
57
|
+
r"^\s*PRAGMA\s+busy_timeout\s*"
|
|
58
|
+
r"(?:=\s*(?P<setter_eq>-?\d+)|\(\s*(?P<setter_paren>-?\d+)\s*\))?"
|
|
59
|
+
r"\s*;?\s*$",
|
|
60
|
+
re.IGNORECASE,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def try_intercept_busy_timeout(
|
|
65
|
+
cursor: Cursor,
|
|
66
|
+
statement: str,
|
|
67
|
+
parameters: object,
|
|
68
|
+
) -> bool:
|
|
69
|
+
"""Return True if the statement is ``PRAGMA busy_timeout`` (any
|
|
70
|
+
form) and the cursor's result state has been populated.
|
|
71
|
+
|
|
72
|
+
When True, the caller MUST short-circuit BEFORE the wire round-
|
|
73
|
+
trip (the server would reject the PRAGMA via the VFS authorizer
|
|
74
|
+
if it reached the wire, defeating the interception's purpose).
|
|
75
|
+
|
|
76
|
+
``parameters`` is accepted for caller-signature parity but
|
|
77
|
+
PRAGMA does not take parameters. If parameters are non-empty
|
|
78
|
+
we still match the PRAGMA and ignore them — the engine would
|
|
79
|
+
do the same. (A future refinement could raise a friendlier
|
|
80
|
+
error here, but stdlib silently accepts the same shape.)
|
|
81
|
+
"""
|
|
82
|
+
if not isinstance(statement, str):
|
|
83
|
+
return False
|
|
84
|
+
match = _PRAGMA_BUSY_TIMEOUT_RE.match(statement)
|
|
85
|
+
if match is None:
|
|
86
|
+
return False
|
|
87
|
+
setter_eq = match.group("setter_eq")
|
|
88
|
+
setter_paren = match.group("setter_paren")
|
|
89
|
+
setter_value = setter_eq if setter_eq is not None else setter_paren
|
|
90
|
+
connection = cursor._connection
|
|
91
|
+
if setter_value is not None:
|
|
92
|
+
# Setter form: update connection._busy_timeout.
|
|
93
|
+
# SQLite clamps negative values to 0 (no wait) — stdlib parity.
|
|
94
|
+
new_ms = max(0, int(setter_value))
|
|
95
|
+
connection._busy_timeout = new_ms / 1000.0
|
|
96
|
+
# Both setter AND getter forms emit the (possibly updated) value
|
|
97
|
+
# as a single-row result. Matches SQLite's PRAGMA convention:
|
|
98
|
+
# ``PRAGMA busy_timeout = 5000`` returns ``(5000,)``,
|
|
99
|
+
# ``PRAGMA busy_timeout`` (getter) also returns ``(5000,)``.
|
|
100
|
+
current_ms = int(connection._busy_timeout * 1000)
|
|
101
|
+
cursor._description = (("busy_timeout", None, None, None, None, None, None),)
|
|
102
|
+
cursor._rows = [(current_ms,)]
|
|
103
|
+
cursor._rowcount = -1
|
|
104
|
+
cursor._row_index = 0
|
|
105
|
+
# Match stdlib: PRAGMA does not set lastrowid.
|
|
106
|
+
return True
|