sqlalchemy-dqlite 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.
Files changed (263) hide show
  1. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/PKG-INFO +4 -4
  2. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/pyproject.toml +4 -4
  3. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/__init__.py +1 -1
  4. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/base.py +72 -1
  5. sqlalchemy_dqlite-0.2.1/tests/test_busy_timeout_url_and_connect_args.py +138 -0
  6. sqlalchemy_dqlite-0.2.1/tests/test_check_same_thread_url_and_connect_args.py +177 -0
  7. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect.py +6 -1
  8. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_bare_databaseerror_type_identity.py +14 -7
  9. sqlalchemy_dqlite-0.2.1/tests/test_is_disconnect_bare_dbe_short_circuits_on_slot_fatal_codes.py +115 -0
  10. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_database_error_substring.py +13 -7
  11. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/uv.lock +4 -4
  12. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/.github/workflows/publish-to-pypi.yml +0 -0
  13. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/.gitignore +0 -0
  14. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/DEVELOPMENT.md +0 -0
  15. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/LICENSE.md +0 -0
  16. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/README.md +0 -0
  17. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/scripts/check-commit-msg.sh +0 -0
  18. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/setup.cfg +0 -0
  19. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/aio.py +0 -0
  20. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/provision.py +0 -0
  21. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/py.typed +0 -0
  22. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/requirements.py +0 -0
  23. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/__init__.py +0 -0
  24. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/compliance/conftest.py +0 -0
  25. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/compliance/test_suite.py +0 -0
  26. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/conftest.py +0 -0
  27. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/__init__.py +0 -0
  28. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/conftest.py +0 -0
  29. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_cancel_during_commit_invalidates_slot.py +0 -0
  30. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_cancel_inside_engine_begin.py +0 -0
  31. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_concurrent_sessions.py +0 -0
  32. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_context.py +0 -0
  33. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_executemany.py +0 -0
  34. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_savepoint_cancellation_invalidates_slot.py +0 -0
  35. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_bulk_dml_returning.py +0 -0
  36. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_date_tz_drop.py +0 -0
  37. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_dial_func_via_connect_args_reaches_dbapi.py +0 -0
  38. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_engine_begin_is_atomic.py +0 -0
  39. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_engine_events_fire.py +0 -0
  40. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_inspect_async_and_close_parity.py +0 -0
  41. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_isolation_level_pin.py +0 -0
  42. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_json_column.py +0 -0
  43. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_native_boolean.py +0 -0
  44. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_orm_operations.py +0 -0
  45. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_pool_pre_ping_leader_flip.py +0 -0
  46. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_pool_recycle_and_dispose_fan_out.py +0 -0
  47. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_pool_reset_on_return.py +0 -0
  48. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_reflect_column_types.py +0 -0
  49. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_savepoint_basic.py +0 -0
  50. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_savepoint_orm_session_begin_nested.py +0 -0
  51. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_savepoint_quoted_identifiers.py +0 -0
  52. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_time_and_reflection.py +0 -0
  53. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_upsert.py +0 -0
  54. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_2pc_not_supported_error.py +0 -0
  55. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_arraysize_default_and_semantics.py +0 -0
  56. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_arraysize_validation.py +0 -0
  57. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_cursor_iterator_protocol.py +0 -0
  58. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_fetchmany_negative.py +0 -0
  59. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_handle_exception_hook.py +0 -0
  60. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_pickle_guards.py +0 -0
  61. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_slots.py +0 -0
  62. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_stale_rows.py +0 -0
  63. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_adapted_connection_public_paths.py +0 -0
  64. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_async_creator_fn.py +0 -0
  65. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_async_creator_fn_shape_check.py +0 -0
  66. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_await_staticmethod.py +0 -0
  67. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_final_close_suppression.py +0 -0
  68. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_loop_mismatch_remap.py +0 -0
  69. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_releases_inner_connection_pin.py +0 -0
  70. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_rollback_log.py +0 -0
  71. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_rollback_loop_already_running_remap.py +0 -0
  72. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_rollback_runtime_error_case_insensitive.py +0 -0
  73. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_suppress_and_soft_close_noop.py +0 -0
  74. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_cancellederror_force_close.py +0 -0
  75. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_in_greenlet_preflight.py +0 -0
  76. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_outside_greenlet.py +0 -0
  77. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_proxy_swap_arms.py +0 -0
  78. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_runtime_error_post_await.py +0 -0
  79. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_connect_construction_in_try_frame.py +0 -0
  80. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_connect_loop_state_remap.py +0 -0
  81. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_cursor_class_hooks.py +0 -0
  82. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_cursor_closed_before_server_side_precedence.py +0 -0
  83. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_cursor_time_error_remapped.py +0 -0
  84. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_dialect_do_close_releases_inner_strong_ref.py +0 -0
  85. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_close_fallback_force_close_transport.py +0 -0
  86. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_ping_cursor_construct_exception_arms.py +0 -0
  87. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_ping_override.py +0 -0
  88. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_ping_runtimeerror_returns_false.py +0 -0
  89. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_terminate_suppresses_exception.py +0 -0
  90. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_force_close_transport_cancelled_error.py +0 -0
  91. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_force_close_transport_dead_proxy.py +0 -0
  92. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_force_close_transport_docstring_pins.py +0 -0
  93. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_handle_exception_baseexceptiongroup_cancel_split.py +0 -0
  94. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_handle_exception_call_sites.py +0 -0
  95. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_handle_exception_delegates_to_remap_helper.py +0 -0
  96. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_is_disconnect_closed_proxy.py +0 -0
  97. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_no_in_function_imports.py +0 -0
  98. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_post_close_proxy_guards.py +0 -0
  99. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_terminate.py +0 -0
  100. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_terminate_close_idempotence.py +0 -0
  101. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_cursor_closed_check.py +0 -0
  102. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_execute_handle_exception.py +0 -0
  103. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_execute_suppress_cancellederror.py +0 -0
  104. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_init_sa_reference_signature.py +0 -0
  105. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_init_three_call_shapes.py +0 -0
  106. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_cursor_doc.py +0 -0
  107. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_cursor_sync_ctxmgr.py +0 -0
  108. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter.py +0 -0
  109. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_autocommit_property.py +0 -0
  110. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_closed_checks.py +0 -0
  111. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_lastrowid_sticky.py +0 -0
  112. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_op_lock_serialisation.py +0 -0
  113. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_terminate_handled_exceptions.py +0 -0
  114. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_commit_cancel_invalidates_slot.py +0 -0
  115. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_creator_fn_noncallable_rejected.py +0 -0
  116. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_after_connection_close.py +0 -0
  117. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_arraysize_accepts_zero_and_negative.py +0 -0
  118. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_arraysize_propagates_to_underlying.py +0 -0
  119. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_closed_guard.py +0 -0
  120. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_closed_guards.py +0 -0
  121. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_execute_close_suppress_behavioural.py +0 -0
  122. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_executemany_listifies_iterable.py +0 -0
  123. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_handle_exception.py +0 -0
  124. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_sync_only_context_manager.py +0 -0
  125. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_dialect_isolation_set.py +0 -0
  126. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_executemany_leader_flip_state_reset.py +0 -0
  127. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_handle_exception_case_insensitive.py +0 -0
  128. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_handle_exception_cause_walk.py +0 -0
  129. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_isolation_level.py +0 -0
  130. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_ping_cancel_cleanup.py +0 -0
  131. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_ping_close_arm_narrow_vs_programmer_bug.py +0 -0
  132. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_ping_single_execute_no_fetchone.py +0 -0
  133. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_ping_translates_dead_proxy_to_interface_error.py +0 -0
  134. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_audit_2026_05_round2_sa_changes.py +0 -0
  135. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_audit_2026_05_sa_changes.py +0 -0
  136. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_audit_2026_05_sa_coverage.py +0 -0
  137. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_bind_side_cross_type_rejection.py +0 -0
  138. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_close_runs_close_after_rollback_raise.py +0 -0
  139. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_collated_coverage_gaps.py +0 -0
  140. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_compiler_regexp_match.py +0 -0
  141. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_compliance_conftest_staleness_pin.py +0 -0
  142. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_connect_args_kwarg_validation.py +0 -0
  143. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_connect_args_value_validators.py +0 -0
  144. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_cross_package_version_parity.py +0 -0
  145. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_cursor_close_narrow_suppress.py +0 -0
  146. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_datetime_bind_widens_date.py +0 -0
  147. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_datetime_date_parse_errors.py +0 -0
  148. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_datetime_time_cross_type_payload.py +0 -0
  149. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_datetime_tz_conversion.py +0 -0
  150. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_ddl_kwarg_prefix_documented.py +0 -0
  151. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_description_alias_shared.py +0 -0
  152. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_detect_autocommit_setting.py +0 -0
  153. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dial_attempt_timeout_url_and_connect_args.py +0 -0
  154. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_dialect_config.py +0 -0
  155. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_docstring_pins.py +0 -0
  156. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_final_annotated.py +0 -0
  157. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_inherits_pysqlite.py +0 -0
  158. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_init_eager_autocommit_rejection.py +0 -0
  159. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_init_preserves_pins.py +0 -0
  160. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_ipv6_url.py +0 -0
  161. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_set_isolation_level_annotation.py +0 -0
  162. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_sqlite_incompatibility_flags_pinned.py +0 -0
  163. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_url_credentials.py +0 -0
  164. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_disconnect_substring_producer_emission.py +0 -0
  165. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_begin_cancel.py +0 -0
  166. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_begin_emits_begin.py +0 -0
  167. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_begin_savepoint_asymmetry_documented.py +0 -0
  168. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_close_calls_dbapi_close_directly.py +0 -0
  169. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_close_fallback_suppress_arm.py +0 -0
  170. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_close_fallback_uses_sync_transport_teardown.py +0 -0
  171. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_close_first_arm_absorbs_runtime_error.py +0 -0
  172. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_executemany_local_override_pin.py +0 -0
  173. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_catches_programming_error.py +0 -0
  174. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_close_narrow.py +0 -0
  175. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_cursor_call_disconnect.py +0 -0
  176. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_database_error.py +0 -0
  177. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_dialect_specific_select_one.py +0 -0
  178. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_interface_error_with_code.py +0 -0
  179. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_non_disconnect_databaseerror_reraise.py +0 -0
  180. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_terminate_narrow_catch_propagates_refactor_break.py +0 -0
  181. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dqlitetime_bind_processor_returns_none.py +0 -0
  182. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dqlitetime_cross_axis_narrow_with_timezone_true.py +0 -0
  183. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_drift_defence_pins.py +0 -0
  184. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_drop_user_tables_log_record_bounded.py +0 -0
  185. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_floor_compile_time_gate.py +0 -0
  186. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_handle_exception_must_raise.py +0 -0
  187. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_handle_exception_remap.py +0 -0
  188. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_handle_exception_two_pass_recovery_after_mixed_cancel_group.py +0 -0
  189. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_bare_oserror_walked_not_early_return.py +0 -0
  190. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_case_insensitive.py +0 -0
  191. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_cause_walk.py +0 -0
  192. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_closed_handle_with_id_suffix.py +0 -0
  193. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_cluster_error.py +0 -0
  194. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_event_loop_already_running_prefix.py +0 -0
  195. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_event_loop_closed.py +0 -0
  196. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_event_loop_mismatch_substring.py +0 -0
  197. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_exception_group_and_substring_walk.py +0 -0
  198. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_interface_error_cause_walk.py +0 -0
  199. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_leader_lost_substring_case_insensitive.py +0 -0
  200. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_notfound_lookup_db.py +0 -0
  201. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_oe_code_restriction.py +0 -0
  202. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_protocol_error_arm.py +0 -0
  203. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_raft_collapse.py +0 -0
  204. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_raw_message.py +0 -0
  205. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_recognises_connection_invalidated_interfaceerror.py +0 -0
  206. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_recognises_cross_loop_interfaceerror.py +0 -0
  207. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_recognises_used_after_fork.py +0 -0
  208. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_substring_visible_within_dbapi_4kib_cap.py +0 -0
  209. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_untracked_savepoint_negative.py +0 -0
  210. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_walks_cause_oserror.py +0 -0
  211. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_isolation_level_in_connect_args_directional_message.py +0 -0
  212. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_isolation_lookup_vs_values_list_divergence_documented.py +0 -0
  213. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_leader_error_codes_identity.py +0 -0
  214. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_log_safe_peer_helper.py +0 -0
  215. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_log_safe_peer_routing.py +0 -0
  216. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_log_safe_peer_uses_sanitize_for_log.py +0 -0
  217. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_max_message_size_url_and_connect_args.py +0 -0
  218. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_max_total_rows_warning_does_not_advertise_disabled_token.py +0 -0
  219. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_phantom_returning_flag_does_not_exist_in_sa.py +0 -0
  220. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_create_drop_db_log_sanitises_ident.py +0 -0
  221. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_drop_user_tables_and_reap.py +0 -0
  222. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_drop_user_tables_log_sanitized.py +0 -0
  223. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_drop_user_tables_quote_escaping.py +0 -0
  224. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_format_url_ident_sanitisation.py +0 -0
  225. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_generate_driver_url.py +0 -0
  226. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_reap_dbs_info_log_credential_mask_and_sanitize.py +0 -0
  227. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_reap_dbs_log_sanitized.py +0 -0
  228. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_session_token_shape.py +0 -0
  229. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_upsert.py +0 -0
  230. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_public_api.py +0 -0
  231. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_raft_collapse_markers_upstream_provenance.py +0 -0
  232. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_remap_loop_state_chains_from_matched_hop.py +0 -0
  233. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_remap_loop_state_helper_docstring_pin.py +0 -0
  234. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_requirements.py +0 -0
  235. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_requirements_closed_marker_present.py +0 -0
  236. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_requirements_recently_opened.py +0 -0
  237. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_reset_isolation_level_is_local_noop.py +0 -0
  238. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_result_processor_edge_branches.py +0 -0
  239. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_result_processor_log_payload_sanitized.py +0 -0
  240. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_result_processor_log_truncates_server_text.py +0 -0
  241. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_result_processor_warning_one_shot.py +0 -0
  242. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_savepoint_leader_flip_loud_failure.py +0 -0
  243. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_set_isolation_level_case_insensitive.py +0 -0
  244. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_stream_results_documented_eager_buffer.py +0 -0
  245. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_submodule_all.py +0 -0
  246. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_sync_connect_creator_fn_shape.py +0 -0
  247. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_sync_do_rollback_transport_fault_classified.py +0 -0
  248. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_sync_do_terminate.py +0 -0
  249. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_sync_do_terminate_log_includes_peer_and_id.py +0 -0
  250. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_time_colspec.py +0 -0
  251. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_time_tz_conversion.py +0 -0
  252. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_top_level_logger_has_null_handler.py +0 -0
  253. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_transitive_wire_import.py +0 -0
  254. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_transport_class_exceptions_consolidation.py +0 -0
  255. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_url_int_upper_boundary_off_by_one.py +0 -0
  256. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_url_max_rows_none_token.py +0 -0
  257. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_url_pre_validation.py +0 -0
  258. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_url_timeout_validators_share_ssot.py +0 -0
  259. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_version.py +0 -0
  260. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_walk_cause_chain_caps_pathological_group_size.py +0 -0
  261. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_walk_cause_chain_cycle_at_depth.py +0 -0
  262. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_walk_cause_chain_depth_budget.py +0 -0
  263. {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_wire_decode_failed_prefix_constant_used_by_dialect.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-dqlite
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: SQLAlchemy 2.0 dialect for dqlite distributed SQLite
5
5
  Project-URL: Homepage, https://github.com/letsdiscodev/sqlalchemy-dqlite
6
6
  Project-URL: Repository, https://github.com/letsdiscodev/sqlalchemy-dqlite
@@ -22,9 +22,9 @@ 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.0
26
- Requires-Dist: dqlite-dbapi<0.3.0,>=0.2.0
27
- Requires-Dist: dqlite-wire<0.3.0,>=0.2.0
25
+ Requires-Dist: dqlite-client<0.3.0,>=0.2.1
26
+ Requires-Dist: dqlite-dbapi<0.3.0,>=0.2.1
27
+ Requires-Dist: dqlite-wire<0.3.0,>=0.2.1
28
28
  Requires-Dist: sqlalchemy<3.0,>=2.0
29
29
  Provides-Extra: dev
30
30
  Requires-Dist: mypy>=1.0; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlalchemy-dqlite"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "SQLAlchemy 2.0 dialect for dqlite distributed SQLite"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -26,15 +26,15 @@ classifiers = [
26
26
  "Typing :: Typed",
27
27
  ]
28
28
  dependencies = [
29
- "dqlite-dbapi>=0.2.0,<0.3.0",
29
+ "dqlite-dbapi>=0.2.1,<0.3.0",
30
30
  # The SA dialect imports dqliteclient and dqlitewire directly
31
31
  # (base.py / aio.py reach into client exception classes for
32
32
  # is_disconnect, and into dqlitewire for shared helpers). Pin
33
33
  # both transitively so a future major-version bump in either
34
34
  # package surfaces at install time, not at module-import time
35
35
  # in user code.
36
- "dqlite-client>=0.2.0,<0.3.0",
37
- "dqlite-wire>=0.2.0,<0.3.0",
36
+ "dqlite-client>=0.2.1,<0.3.0",
37
+ "dqlite-wire>=0.2.1,<0.3.0",
38
38
  "sqlalchemy>=2.0,<3.0",
39
39
  ]
40
40
 
@@ -27,7 +27,7 @@ from sqlalchemy.dialects.sqlite import (
27
27
  from sqlalchemydqlite.aio import DqliteDialect_aio
28
28
  from sqlalchemydqlite.base import DqliteCompiler, DqliteDialect
29
29
 
30
- __version__: _Final[str] = "0.2.0"
30
+ __version__: _Final[str] = "0.2.1"
31
31
 
32
32
  # SA convention (every shipped dialect package — sqlite, mysql, mssql,
33
33
  # oracle, postgresql): expose ``<package>.dialect`` pointing at the
@@ -1515,6 +1515,27 @@ class DqliteDialect(SQLiteDialect_pysqlite):
1515
1515
  # always a typo, so it stays out of ``_URL_QUERY_ALLOWED``
1516
1516
  # below.
1517
1517
  "dial_func",
1518
+ # ``busy_timeout`` (seconds, float) — stdlib sqlite3
1519
+ # parity. Default ``5.0`` matches sqlite3's C-library
1520
+ # default; SA users can override via
1521
+ # ``create_engine(connect_args={"busy_timeout": 30.0})``
1522
+ # or via the URL form ``?busy_timeout=30.0``. Forwards
1523
+ # to ``dqlitedbapi.connect()`` which validates the
1524
+ # value (non-negative finite number, non-bool).
1525
+ "busy_timeout",
1526
+ # ``check_same_thread`` (bool) — stdlib sqlite3 parity.
1527
+ # Default ``True`` (dbapi side); set ``False`` to share
1528
+ # a Connection across threads. The dialect pins
1529
+ # ``pool.QueuePool``; SA's QueuePool checkout/checkin
1530
+ # plus the ``_finalize_fairy`` weakref-finalizer (which
1531
+ # runs from arbitrary GC threads) both produce cross-
1532
+ # thread Connection access patterns. The pool self-
1533
+ # heals via ``force_close_transport`` (no _check_thread)
1534
+ # even today, but the GC-finalize raise produces a noisy
1535
+ # ``logger.error("Exception during reset or similar")``
1536
+ # line; passing ``check_same_thread=False`` quiets that
1537
+ # and matches the established SA + sqlite3 pattern.
1538
+ "check_same_thread",
1518
1539
  }
1519
1540
  )
1520
1541
 
@@ -1605,6 +1626,34 @@ class DqliteDialect(SQLiteDialect_pysqlite):
1605
1626
  # timeout floor's rationale does not apply.
1606
1627
  "dial_timeout": (float, _validate_dial_timeout_url),
1607
1628
  "attempt_timeout": (float, _validate_attempt_timeout_url),
1629
+ # ``busy_timeout`` (seconds, float) — stdlib sqlite3 parity.
1630
+ # Validator mirrors the dbapi-side ``Connection.__init__``
1631
+ # check: non-negative (zero accepted, meaning "no retry"),
1632
+ # finite, non-bool. The URL converter is ``float`` (per
1633
+ # the same shape as ``timeout`` / ``close_timeout``); the
1634
+ # validator's accept criterion is the canonical floor
1635
+ # because connect_args= bypasses the converter.
1636
+ "busy_timeout": (
1637
+ float,
1638
+ lambda v: (
1639
+ not isinstance(v, bool)
1640
+ and isinstance(v, int | float)
1641
+ and math.isfinite(v)
1642
+ and v >= 0
1643
+ ),
1644
+ ),
1645
+ # ``check_same_thread`` (bool) — stdlib sqlite3 parity. The
1646
+ # URL parser uses ``_parse_url_bool`` (same as
1647
+ # ``trust_server_heartbeat``) to accept ``true``/``false``/
1648
+ # ``1``/``0``/``yes``/``no`` case-insensitive. The
1649
+ # validator gate on the connect_args= path requires strict
1650
+ # bool (rejecting int 0/1 explicitly because
1651
+ # ``isinstance(True, int)`` is True and the dbapi-side
1652
+ # strict-bool validation would otherwise silently re-fire).
1653
+ "check_same_thread": (
1654
+ lambda s: _parse_url_bool("check_same_thread", s),
1655
+ lambda v: isinstance(v, bool),
1656
+ ),
1608
1657
  }
1609
1658
 
1610
1659
  def create_connect_args(self, url: URL) -> tuple[list[Any], dict[str, Any]]:
@@ -2646,7 +2695,29 @@ class DqliteDialect(SQLiteDialect_pysqlite):
2646
2695
  # silently activate disconnect classification on
2647
2696
  # caller bugs. ``type is`` makes the scope explicit
2648
2697
  # and matches the docstring intent above.
2649
- applies_substring = getattr(cause, "code", None) in _BARE_DBE_DISCONNECT_CODES
2698
+ if getattr(cause, "code", None) in _BARE_DBE_DISCONNECT_CODES:
2699
+ # Short-circuit ``return True`` mirrors ``do_ping``'s
2700
+ # classification at base.py:2935-2945. A bare
2701
+ # DatabaseError under one of the slot-fatal codes
2702
+ # IS slot-fatal regardless of the message text —
2703
+ # the substring scan exists for the
2704
+ # ``OperationalError`` arm above where ``code`` is
2705
+ # None and the message IS the load-bearing
2706
+ # classification signal. The canonical engine
2707
+ # wordings for CORRUPT / FORMAT / NOTADB
2708
+ # ("database disk image is malformed", "file is
2709
+ # not a database", "file is encrypted or is not a
2710
+ # database") are NOT in ``_dqlite_disconnect_
2711
+ # messages`` by design — those messages are caller
2712
+ # diagnostics, not transport-state signals. Without
2713
+ # this short-circuit, ``is_disconnect`` returned
2714
+ # False on a real CORRUPT response, SA's pool
2715
+ # returned the slot to the queue, and every
2716
+ # subsequent checkout re-tripped the same fault.
2717
+ # ``_BARE_DBE_DISCONNECT_CODES`` is the SSOT both
2718
+ # classifiers gate on.
2719
+ return True
2720
+ applies_substring = False
2650
2721
  elif isinstance(cause, _dbapi_exc.InterfaceError):
2651
2722
  # The dedicated-phrase arm at the top of this loop
2652
2723
  # (lines 2461-2539) already handles the
@@ -0,0 +1,138 @@
1
+ """Pin: ``busy_timeout`` works via both URL query and connect_args=
2
+ on the SA dialect, forwarding to the dbapi layer.
3
+
4
+ The dbapi-side feature defaults ``busy_timeout=5.0`` (stdlib parity);
5
+ SA-side users tune via:
6
+
7
+ - URL: ``dqlite://host:9001/db?busy_timeout=30.0``
8
+ - connect_args: ``create_engine(..., connect_args={"busy_timeout": 30.0})``
9
+
10
+ Both routes go through the dialect's ``create_connect_args`` /
11
+ allowlist machinery and end up as a kwarg on ``dqlitedbapi.connect``.
12
+
13
+ Validation:
14
+ - Negative / non-finite / non-numeric values raise ``ArgumentError`` at
15
+ URL-parse or first-checkout time (NOT at runtime when the contended
16
+ write hits BUSY).
17
+ - bool is rejected (would silently coerce True→1.0).
18
+ - Zero is accepted (stdlib parity — "no retry").
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import pytest
24
+ from sqlalchemy import create_engine
25
+ from sqlalchemy.engine import URL
26
+ from sqlalchemy.exc import ArgumentError
27
+
28
+ from sqlalchemydqlite.base import DqliteDialect
29
+
30
+
31
+ def test_url_query_busy_timeout_forwards_to_connect_kwargs() -> None:
32
+ """``?busy_timeout=30.0`` in the URL ends up in the dialect's
33
+ connect kwargs."""
34
+ url = URL.create(
35
+ "dqlite",
36
+ host="localhost",
37
+ port=9001,
38
+ database="default",
39
+ query={"busy_timeout": "30.0"},
40
+ )
41
+ dialect = DqliteDialect()
42
+ _args, kwargs = dialect.create_connect_args(url)
43
+ assert kwargs.get("busy_timeout") == 30.0
44
+
45
+
46
+ def test_url_query_busy_timeout_zero_accepted() -> None:
47
+ """``?busy_timeout=0`` is the canonical "no retry" — must be
48
+ accepted at URL-parse time (stdlib parity for timeout=0)."""
49
+ url = URL.create(
50
+ "dqlite",
51
+ host="localhost",
52
+ port=9001,
53
+ database="default",
54
+ query={"busy_timeout": "0"},
55
+ )
56
+ dialect = DqliteDialect()
57
+ _args, kwargs = dialect.create_connect_args(url)
58
+ assert kwargs.get("busy_timeout") == 0.0
59
+
60
+
61
+ def test_url_query_busy_timeout_negative_rejected() -> None:
62
+ """Negative values are rejected at URL-parse time as
63
+ ``ArgumentError`` (not at first-checkout time)."""
64
+ url = URL.create(
65
+ "dqlite",
66
+ host="localhost",
67
+ port=9001,
68
+ database="default",
69
+ query={"busy_timeout": "-1"},
70
+ )
71
+ dialect = DqliteDialect()
72
+ with pytest.raises(ArgumentError):
73
+ dialect.create_connect_args(url)
74
+
75
+
76
+ def test_url_query_busy_timeout_non_numeric_rejected() -> None:
77
+ """``?busy_timeout=abc`` raises at URL-parse time (the URL
78
+ converter is ``float`` and rejects non-numeric strings)."""
79
+ url = URL.create(
80
+ "dqlite",
81
+ host="localhost",
82
+ port=9001,
83
+ database="default",
84
+ query={"busy_timeout": "abc"},
85
+ )
86
+ dialect = DqliteDialect()
87
+ with pytest.raises(ArgumentError):
88
+ dialect.create_connect_args(url)
89
+
90
+
91
+ def test_connect_args_busy_timeout_accepted() -> None:
92
+ """``create_engine(..., connect_args={"busy_timeout": N})`` works
93
+ — the kwarg is in the allowlist."""
94
+ engine = create_engine(
95
+ "dqlite://localhost:9001/default",
96
+ connect_args={"busy_timeout": 30.0},
97
+ )
98
+ # Engine construction must succeed without ArgumentError. We can't
99
+ # actually connect (no server) — the kwarg validation happens at
100
+ # first checkout / dispose. The fact that create_engine returned
101
+ # without raising means the dialect accepted the kwarg.
102
+ assert engine is not None
103
+ engine.dispose()
104
+
105
+
106
+ def test_connect_args_busy_timeout_zero_accepted() -> None:
107
+ """Zero accepted via connect_args= same as URL."""
108
+ engine = create_engine(
109
+ "dqlite://localhost:9001/default",
110
+ connect_args={"busy_timeout": 0.0},
111
+ )
112
+ assert engine is not None
113
+ engine.dispose()
114
+
115
+
116
+ def test_connect_args_busy_timeout_negative_rejected() -> None:
117
+ """Negative busy_timeout in connect_args= is rejected. The
118
+ validator runs at first connection-arg merge (in the dialect's
119
+ connect() override)."""
120
+ engine = create_engine(
121
+ "dqlite://localhost:9001/default",
122
+ connect_args={"busy_timeout": -1.0},
123
+ )
124
+ # The allowlist's validator runs at first checkout time.
125
+ with pytest.raises(ArgumentError):
126
+ engine.connect()
127
+ engine.dispose()
128
+
129
+
130
+ def test_connect_args_busy_timeout_bool_rejected() -> None:
131
+ """bool is rejected (would silently coerce True→1.0)."""
132
+ engine = create_engine(
133
+ "dqlite://localhost:9001/default",
134
+ connect_args={"busy_timeout": True},
135
+ )
136
+ with pytest.raises(ArgumentError):
137
+ engine.connect()
138
+ engine.dispose()
@@ -0,0 +1,177 @@
1
+ """Pin: SA dialect accepts ``check_same_thread`` via both the URL
2
+ query string and the ``connect_args=`` kwarg on
3
+ ``create_engine()``, forwarding to ``dqlitedbapi.connect()``.
4
+
5
+ Stdlib sqlite3 parity. SA users porting from sqlite use:
6
+
7
+ create_engine(
8
+ "sqlite:///app.db",
9
+ connect_args={"check_same_thread": False},
10
+ )
11
+
12
+ The same incantation against dqlite now works:
13
+
14
+ create_engine(
15
+ "dqlite://node:9001/default",
16
+ connect_args={"check_same_thread": False},
17
+ )
18
+
19
+ Plus the URL form:
20
+
21
+ create_engine("dqlite://node:9001/default?check_same_thread=false")
22
+
23
+ Pinned behaviours:
24
+ - Both surfaces (URL + connect_args) reach the dbapi connect kwarg.
25
+ - Boolean parsing via URL accepts the standard tokens.
26
+ - Validation gates: bool required on connect_args; URL parser
27
+ rejects unknown tokens.
28
+ - The aio dialect rejects the kwarg via the dbapi-layer sync-only
29
+ message (the aio surface is loop-bound).
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import pytest
35
+ from sqlalchemy import create_engine
36
+ from sqlalchemy.engine import URL
37
+ from sqlalchemy.exc import ArgumentError
38
+
39
+ from sqlalchemydqlite.base import DqliteDialect
40
+
41
+
42
+ def test_url_query_check_same_thread_false_forwards_to_kwargs() -> None:
43
+ """``?check_same_thread=false`` parses to the kwarg."""
44
+ url = URL.create(
45
+ "dqlite",
46
+ host="localhost",
47
+ port=9001,
48
+ database="default",
49
+ query={"check_same_thread": "false"},
50
+ )
51
+ dialect = DqliteDialect()
52
+ _args, kwargs = dialect.create_connect_args(url)
53
+ assert kwargs.get("check_same_thread") is False
54
+
55
+
56
+ def test_url_query_check_same_thread_true_forwards_to_kwargs() -> None:
57
+ """``?check_same_thread=true`` parses to the kwarg."""
58
+ url = URL.create(
59
+ "dqlite",
60
+ host="localhost",
61
+ port=9001,
62
+ database="default",
63
+ query={"check_same_thread": "true"},
64
+ )
65
+ dialect = DqliteDialect()
66
+ _args, kwargs = dialect.create_connect_args(url)
67
+ assert kwargs.get("check_same_thread") is True
68
+
69
+
70
+ @pytest.mark.parametrize(
71
+ "token,expected",
72
+ [
73
+ ("true", True),
74
+ ("True", True),
75
+ ("TRUE", True),
76
+ ("1", True),
77
+ ("yes", True),
78
+ ("false", False),
79
+ ("False", False),
80
+ ("FALSE", False),
81
+ ("0", False),
82
+ ("no", False),
83
+ ],
84
+ )
85
+ def test_url_query_bool_tokens_accepted(token: str, expected: bool) -> None:
86
+ """The URL bool parser accepts the standard token set
87
+ case-insensitively, same shape as the ``trust_server_heartbeat``
88
+ URL knob (which uses the same ``_parse_url_bool`` helper)."""
89
+ url = URL.create(
90
+ "dqlite",
91
+ host="localhost",
92
+ port=9001,
93
+ database="default",
94
+ query={"check_same_thread": token},
95
+ )
96
+ dialect = DqliteDialect()
97
+ _args, kwargs = dialect.create_connect_args(url)
98
+ assert kwargs.get("check_same_thread") is expected
99
+
100
+
101
+ def test_url_query_invalid_token_rejected() -> None:
102
+ """``?check_same_thread=notabool`` raises at URL-parse time."""
103
+ url = URL.create(
104
+ "dqlite",
105
+ host="localhost",
106
+ port=9001,
107
+ database="default",
108
+ query={"check_same_thread": "notabool"},
109
+ )
110
+ dialect = DqliteDialect()
111
+ with pytest.raises(ArgumentError):
112
+ dialect.create_connect_args(url)
113
+
114
+
115
+ def test_connect_args_check_same_thread_false_accepted() -> None:
116
+ """``connect_args={"check_same_thread": False}`` works — the
117
+ canonical SA + sqlite incantation transparently ports to
118
+ dqlite."""
119
+ engine = create_engine(
120
+ "dqlite://localhost:9001/default",
121
+ connect_args={"check_same_thread": False},
122
+ )
123
+ # Engine construction succeeds without ArgumentError. We can't
124
+ # actually connect (no server) — the kwarg validation happens
125
+ # at first checkout.
126
+ assert engine is not None
127
+ engine.dispose()
128
+
129
+
130
+ def test_connect_args_check_same_thread_true_accepted() -> None:
131
+ """Explicit ``True`` also works (no-op vs default)."""
132
+ engine = create_engine(
133
+ "dqlite://localhost:9001/default",
134
+ connect_args={"check_same_thread": True},
135
+ )
136
+ assert engine is not None
137
+ engine.dispose()
138
+
139
+
140
+ def test_connect_args_check_same_thread_rejects_int_zero() -> None:
141
+ """Strict bool: ``0`` is not ``False`` for this kwarg
142
+ (mirrors the dbapi-side strict-bool validation)."""
143
+ engine = create_engine(
144
+ "dqlite://localhost:9001/default",
145
+ connect_args={"check_same_thread": 0},
146
+ )
147
+ with pytest.raises(ArgumentError):
148
+ engine.connect()
149
+ engine.dispose()
150
+
151
+
152
+ def test_connect_args_check_same_thread_rejects_string() -> None:
153
+ """Strict bool: string ``"false"`` rejected via the dbapi
154
+ surface (the SA validator catches non-bool too)."""
155
+ engine = create_engine(
156
+ "dqlite://localhost:9001/default",
157
+ connect_args={"check_same_thread": "false"},
158
+ )
159
+ with pytest.raises(ArgumentError):
160
+ engine.connect()
161
+ engine.dispose()
162
+
163
+
164
+ def test_url_query_combined_with_other_knobs() -> None:
165
+ """Combine check_same_thread with other URL knobs (busy_timeout)
166
+ to confirm they don't interfere."""
167
+ url = URL.create(
168
+ "dqlite",
169
+ host="localhost",
170
+ port=9001,
171
+ database="default",
172
+ query={"check_same_thread": "false", "busy_timeout": "10.0"},
173
+ )
174
+ dialect = DqliteDialect()
175
+ _args, kwargs = dialect.create_connect_args(url)
176
+ assert kwargs.get("check_same_thread") is False
177
+ assert kwargs.get("busy_timeout") == 10.0
@@ -315,7 +315,12 @@ class TestDqliteDialectAio:
315
315
  """Async dbapi module must expose PEP 249 attributes for SQLAlchemy."""
316
316
  dbapi = DqliteDialect_aio.import_dbapi()
317
317
  assert dbapi.apilevel == "2.0"
318
- assert dbapi.threadsafety == 1
318
+ # threadsafety=2 = "threads may share the module AND
319
+ # connections" — the dbapi opts into Tier 2 once the user
320
+ # passes ``check_same_thread=False``. Cursors are NOT
321
+ # shareable per the documented contract. See
322
+ # ``dqlitedbapi.__init__:108-149`` for the full rationale.
323
+ assert dbapi.threadsafety == 2
319
324
 
320
325
  def test_create_async_engine(self) -> None:
321
326
  """create_async_engine must not raise during dialect initialization."""
@@ -96,20 +96,27 @@ def test_bare_databaseerror_with_disconnect_code_and_substring_does_classify(
96
96
  code: int,
97
97
  ) -> None:
98
98
  """Sanity: a BARE ``DatabaseError`` (not a subclass) with a code in
99
- ``_BARE_DBE_DISCONNECT_CODES`` and a disconnect-style message DOES
100
- classify as disconnect — pin the type-identity arm's positive
101
- side too."""
99
+ ``_BARE_DBE_DISCONNECT_CODES`` classifies as disconnect — pin the
100
+ type-identity arm's positive side. After the substring-gate fix,
101
+ the disconnect-style message is no longer required (mirrors
102
+ do_ping's code-only classification)."""
102
103
  exc = DatabaseError("wire decode failed validation", code=code)
103
104
  dialect = _dialect()
104
105
  assert dialect.is_disconnect(exc, None, None) is True
105
106
 
106
107
 
107
108
  @pytest.mark.parametrize("code", list(_BARE_DBE_DISCONNECT_CODES))
108
- def test_bare_databaseerror_with_disconnect_code_no_substring_does_not_classify(
109
+ def test_bare_databaseerror_with_disconnect_code_classifies_regardless_of_message(
109
110
  code: int,
110
111
  ) -> None:
111
- """Sanity: bare ``DatabaseError`` with disconnect code but a
112
- non-matching message does NOT classify (substring is required)."""
112
+ """Sanity: bare ``DatabaseError`` with disconnect code classifies
113
+ as disconnect regardless of message — the code is the load-bearing
114
+ signal. Pre-fix this returned False (the substring gate blocked
115
+ classification on the canonical engine wordings); post-fix it
116
+ short-circuits ``return True`` mirroring ``do_ping``'s arm at
117
+ base.py:2935-2945. See
118
+ ``test_is_disconnect_bare_dbe_short_circuits_on_slot_fatal_codes.py``
119
+ for the full parametrised coverage."""
113
120
  exc = DatabaseError("some unrelated server message", code=code)
114
121
  dialect = _dialect()
115
- assert dialect.is_disconnect(exc, None, None) is False
122
+ assert dialect.is_disconnect(exc, None, None) is True
@@ -0,0 +1,115 @@
1
+ """Pin: ``is_disconnect``'s bare-``DatabaseError`` arm short-circuits
2
+ ``return True`` for codes in ``_BARE_DBE_DISCONNECT_CODES``
3
+ (SQLITE_CORRUPT=11, SQLITE_FORMAT=24, SQLITE_NOTADB=26), regardless
4
+ of the message text. Mirrors ``do_ping``'s classification (base.py:
5
+ 2935-2945).
6
+
7
+ Before the fix, the bare-``DatabaseError`` arm set
8
+ ``applies_substring = True`` and then routed through the same
9
+ substring scan used for the ``OperationalError`` arm. The substring
10
+ list (``_dqlite_disconnect_messages``) contains transport-state
11
+ phrases ("connection closed", "wire decode failed", "timed out",
12
+ "failed to connect", ...) but NONE of the canonical engine wordings
13
+ for the three slot-fatal codes:
14
+
15
+ - SQLITE_CORRUPT (11) → "database disk image is malformed"
16
+ - SQLITE_NOTADB (26) → "file is not a database"
17
+ - SQLITE_FORMAT (24) → "unsupported file format" / "file is
18
+ encrypted or is not a database"
19
+
20
+ So a real CORRUPT response from the engine returned False from
21
+ ``is_disconnect``, SA's pool returned the slot to the queue, and
22
+ the next checkout reused the same slot — re-tripping the same
23
+ fault on every subsequent query. The companion ``do_ping`` arm
24
+ correctly short-circuits ``return False`` for the same code set
25
+ WITHOUT a substring filter; this restores parity.
26
+
27
+ This is a HIGH-severity silent data-correctness hazard for any
28
+ user running without ``pool_pre_ping=True``.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import pytest
34
+
35
+ from dqlitedbapi.exceptions import DatabaseError
36
+ from dqlitewire.constants import SQLITE_CORRUPT, SQLITE_FORMAT, SQLITE_NOTADB
37
+ from sqlalchemydqlite.base import DqliteDialect
38
+
39
+
40
+ @pytest.mark.parametrize(
41
+ "code,message",
42
+ [
43
+ (SQLITE_CORRUPT, "database disk image is malformed"),
44
+ (SQLITE_NOTADB, "file is not a database"),
45
+ (SQLITE_FORMAT, "unsupported file format"),
46
+ (SQLITE_FORMAT, "file is encrypted or is not a database"),
47
+ # Hostile-control: message that explicitly avoids any
48
+ # substring in _dqlite_disconnect_messages.
49
+ (SQLITE_CORRUPT, "irrecoverable on-disk page"),
50
+ (SQLITE_NOTADB, "header check failed"),
51
+ ],
52
+ )
53
+ def test_bare_database_error_with_slot_fatal_code_classifies_as_disconnect(
54
+ code: int, message: str
55
+ ) -> None:
56
+ """Every (code, message) shape in the bare-DBE slot-fatal set
57
+ classifies as disconnect, regardless of substring."""
58
+ exc = DatabaseError(message, code=code)
59
+ assert DqliteDialect().is_disconnect(exc, None, None) is True
60
+
61
+
62
+ def test_bare_database_error_with_non_slot_fatal_code_does_not_classify() -> None:
63
+ """Regression: a bare ``DatabaseError`` whose code is NOT in
64
+ ``_BARE_DBE_DISCONNECT_CODES`` must not be classified as
65
+ disconnect, even with a transport-shaped message. Per the
66
+ `_BARE_DBE_DISCONNECT_CODES` SSOT and ``do_ping`` arm precedent."""
67
+ exc = DatabaseError("wire decode failed near column 42", code=99)
68
+ assert DqliteDialect().is_disconnect(exc, None, None) is False
69
+
70
+
71
+ def test_subclasses_of_database_error_are_not_routed_through_bare_arm() -> None:
72
+ """Regression: ``IntegrityError`` / ``DataError`` (subclasses of
73
+ DatabaseError) take the ``type(cause) is DatabaseError`` arm via
74
+ type-identity check — they MUST NOT match the slot-fatal
75
+ short-circuit even when they happen to carry a transport-shaped
76
+ message. SA's retry path would otherwise duplicate non-idempotent
77
+ INSERTs."""
78
+ from dqlitedbapi.exceptions import DataError, IntegrityError
79
+
80
+ integrity = IntegrityError(
81
+ "constraint failed: timed out validating peer",
82
+ code=19,
83
+ )
84
+ assert DqliteDialect().is_disconnect(integrity, None, None) is False
85
+
86
+ data = DataError(
87
+ "datatype mismatch: connection closed in payload",
88
+ code=20,
89
+ )
90
+ assert DqliteDialect().is_disconnect(data, None, None) is False
91
+
92
+
93
+ def test_is_disconnect_and_do_ping_agree_on_slot_fatal_codes() -> None:
94
+ """Cross-classifier parity pin: for every code in
95
+ ``_BARE_DBE_DISCONNECT_CODES``, ``is_disconnect`` returns True AND
96
+ ``do_ping`` returns False (False == "slot dead, invalidate"). The
97
+ two classifiers MUST agree on the slot-fatal code set — that's
98
+ the whole point of extracting ``_BARE_DBE_DISCONNECT_CODES`` as
99
+ a shared constant.
100
+
101
+ do_ping's classifier is tested separately in the do_ping test
102
+ suite; this test pins the is_disconnect side and asserts the
103
+ invariant relation: is_disconnect(exc) == True iff do_ping
104
+ would return False on the same exc-raising path."""
105
+ from sqlalchemydqlite.base import _BARE_DBE_DISCONNECT_CODES
106
+
107
+ dialect = DqliteDialect()
108
+ for code in _BARE_DBE_DISCONNECT_CODES:
109
+ exc = DatabaseError(f"engine error with code {code}", code=code)
110
+ assert dialect.is_disconnect(exc, None, None) is True, (
111
+ f"is_disconnect must classify bare DatabaseError(code={code}) as "
112
+ f"disconnect; got False. The do_ping arm at base.py:2935-2945 "
113
+ f"already short-circuits this code set — the two classifiers "
114
+ f"must agree (_BARE_DBE_DISCONNECT_CODES is the SSOT)."
115
+ )
@@ -27,14 +27,20 @@ def test_database_error_with_wire_decode_substring_classifies_as_disconnect() ->
27
27
  assert DqliteDialect().is_disconnect(e, None, None) is True
28
28
 
29
29
 
30
- def test_database_error_without_disconnect_substring_not_classified() -> None:
31
- """Negative pin: a bare ``DatabaseError`` whose message does NOT
32
- contain a disconnect substring must NOT be classified as a
33
- disconnect — the widening only wires the substring branch to the
34
- larger class hierarchy; it does not turn every ``DatabaseError``
35
- into a disconnect."""
30
+ def test_database_error_with_slot_fatal_code_classifies_even_without_substring() -> None:
31
+ """A bare ``DatabaseError`` carrying one of the slot-fatal codes
32
+ (CORRUPT=11 / FORMAT=24 / NOTADB=26) classifies as disconnect
33
+ regardless of message — pre-fix this returned False (the substring
34
+ gate blocked classification on the canonical engine wording);
35
+ post-fix the bare-DBE arm short-circuits ``return True`` for the
36
+ code set, mirroring ``do_ping``'s arm at base.py:2935-2945. See
37
+ ``test_is_disconnect_bare_dbe_short_circuits_on_slot_fatal_codes.py``
38
+ for the full pin set explaining the HIGH-severity hazard the
39
+ pre-fix behaviour created (without ``pool_pre_ping=True``, a slot
40
+ surfacing CORRUPT stayed in the pool and re-tripped on every
41
+ subsequent checkout)."""
36
42
  e = DatabaseError("database disk image is malformed", code=11)
37
- assert DqliteDialect().is_disconnect(e, None, None) is False
43
+ assert DqliteDialect().is_disconnect(e, None, None) is True
38
44
 
39
45
 
40
46
  def test_database_error_via_cause_walk() -> None: