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.
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/PKG-INFO +4 -4
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/pyproject.toml +4 -4
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/__init__.py +1 -1
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/base.py +72 -1
- sqlalchemy_dqlite-0.2.1/tests/test_busy_timeout_url_and_connect_args.py +138 -0
- sqlalchemy_dqlite-0.2.1/tests/test_check_same_thread_url_and_connect_args.py +177 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect.py +6 -1
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_bare_databaseerror_type_identity.py +14 -7
- sqlalchemy_dqlite-0.2.1/tests/test_is_disconnect_bare_dbe_short_circuits_on_slot_fatal_codes.py +115 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_database_error_substring.py +13 -7
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/uv.lock +4 -4
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/.gitignore +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/DEVELOPMENT.md +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/LICENSE.md +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/README.md +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/scripts/check-commit-msg.sh +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/setup.cfg +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/aio.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/provision.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/py.typed +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/src/sqlalchemydqlite/requirements.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/__init__.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/compliance/conftest.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/compliance/test_suite.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/conftest.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/__init__.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/conftest.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_cancel_during_commit_invalidates_slot.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_cancel_inside_engine_begin.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_concurrent_sessions.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_context.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_executemany.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_async_savepoint_cancellation_invalidates_slot.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_bulk_dml_returning.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_date_tz_drop.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_dial_func_via_connect_args_reaches_dbapi.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_engine_begin_is_atomic.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_engine_events_fire.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_inspect_async_and_close_parity.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_isolation_level_pin.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_json_column.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_native_boolean.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_orm_operations.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_pool_pre_ping_leader_flip.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_pool_recycle_and_dispose_fan_out.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_pool_reset_on_return.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_reflect_column_types.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_savepoint_basic.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_savepoint_orm_session_begin_nested.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_savepoint_quoted_identifiers.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_time_and_reflection.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/integration/test_upsert.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_2pc_not_supported_error.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_arraysize_default_and_semantics.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_arraysize_validation.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_cursor_iterator_protocol.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_fetchmany_negative.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_handle_exception_hook.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_pickle_guards.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_slots.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_adapter_stale_rows.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_adapted_connection_public_paths.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_async_creator_fn.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_async_creator_fn_shape_check.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_await_staticmethod.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_final_close_suppression.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_loop_mismatch_remap.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_releases_inner_connection_pin.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_rollback_log.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_rollback_loop_already_running_remap.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_rollback_runtime_error_case_insensitive.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_suppress_and_soft_close_noop.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_cancellederror_force_close.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_in_greenlet_preflight.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_outside_greenlet.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_proxy_swap_arms.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_close_terminate_runtime_error_post_await.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_connect_construction_in_try_frame.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_connect_loop_state_remap.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_cursor_class_hooks.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_cursor_closed_before_server_side_precedence.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_cursor_time_error_remapped.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_dialect_do_close_releases_inner_strong_ref.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_close_fallback_force_close_transport.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_ping_cursor_construct_exception_arms.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_ping_override.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_ping_runtimeerror_returns_false.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_do_terminate_suppresses_exception.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_force_close_transport_cancelled_error.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_force_close_transport_dead_proxy.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_force_close_transport_docstring_pins.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_handle_exception_baseexceptiongroup_cancel_split.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_handle_exception_call_sites.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_handle_exception_delegates_to_remap_helper.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_is_disconnect_closed_proxy.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_no_in_function_imports.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_post_close_proxy_guards.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_terminate.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_aio_terminate_close_idempotence.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_cursor_closed_check.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_execute_handle_exception.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_execute_suppress_cancellederror.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_init_sa_reference_signature.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_connection_init_three_call_shapes.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_cursor_doc.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapted_cursor_sync_ctxmgr.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_autocommit_property.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_closed_checks.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_lastrowid_sticky.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_op_lock_serialisation.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_adapter_terminate_handled_exceptions.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_commit_cancel_invalidates_slot.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_creator_fn_noncallable_rejected.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_after_connection_close.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_arraysize_accepts_zero_and_negative.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_arraysize_propagates_to_underlying.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_closed_guard.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_closed_guards.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_execute_close_suppress_behavioural.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_executemany_listifies_iterable.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_handle_exception.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_cursor_sync_only_context_manager.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_dialect_isolation_set.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_executemany_leader_flip_state_reset.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_handle_exception_case_insensitive.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_handle_exception_cause_walk.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_isolation_level.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_ping_cancel_cleanup.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_ping_close_arm_narrow_vs_programmer_bug.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_ping_single_execute_no_fetchone.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_async_ping_translates_dead_proxy_to_interface_error.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_audit_2026_05_round2_sa_changes.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_audit_2026_05_sa_changes.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_audit_2026_05_sa_coverage.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_bind_side_cross_type_rejection.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_close_runs_close_after_rollback_raise.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_collated_coverage_gaps.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_compiler_regexp_match.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_compliance_conftest_staleness_pin.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_connect_args_kwarg_validation.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_connect_args_value_validators.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_cross_package_version_parity.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_cursor_close_narrow_suppress.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_datetime_bind_widens_date.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_datetime_date_parse_errors.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_datetime_time_cross_type_payload.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_datetime_tz_conversion.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_ddl_kwarg_prefix_documented.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_description_alias_shared.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_detect_autocommit_setting.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dial_attempt_timeout_url_and_connect_args.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_dialect_config.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_docstring_pins.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_final_annotated.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_inherits_pysqlite.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_init_eager_autocommit_rejection.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_init_preserves_pins.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_ipv6_url.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_set_isolation_level_annotation.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_sqlite_incompatibility_flags_pinned.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dialect_url_credentials.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_disconnect_substring_producer_emission.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_begin_cancel.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_begin_emits_begin.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_begin_savepoint_asymmetry_documented.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_close_calls_dbapi_close_directly.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_close_fallback_suppress_arm.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_close_fallback_uses_sync_transport_teardown.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_close_first_arm_absorbs_runtime_error.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_executemany_local_override_pin.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_catches_programming_error.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_close_narrow.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_cursor_call_disconnect.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_database_error.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_dialect_specific_select_one.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_interface_error_with_code.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_ping_non_disconnect_databaseerror_reraise.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_do_terminate_narrow_catch_propagates_refactor_break.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dqlitetime_bind_processor_returns_none.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_dqlitetime_cross_axis_narrow_with_timezone_true.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_drift_defence_pins.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_drop_user_tables_log_record_bounded.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_floor_compile_time_gate.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_handle_exception_must_raise.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_handle_exception_remap.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_handle_exception_two_pass_recovery_after_mixed_cancel_group.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_bare_oserror_walked_not_early_return.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_case_insensitive.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_cause_walk.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_closed_handle_with_id_suffix.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_cluster_error.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_event_loop_already_running_prefix.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_event_loop_closed.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_event_loop_mismatch_substring.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_exception_group_and_substring_walk.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_interface_error_cause_walk.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_leader_lost_substring_case_insensitive.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_notfound_lookup_db.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_oe_code_restriction.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_protocol_error_arm.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_raft_collapse.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_raw_message.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_recognises_connection_invalidated_interfaceerror.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_recognises_cross_loop_interfaceerror.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_recognises_used_after_fork.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_substring_visible_within_dbapi_4kib_cap.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_untracked_savepoint_negative.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_is_disconnect_walks_cause_oserror.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_isolation_level_in_connect_args_directional_message.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_isolation_lookup_vs_values_list_divergence_documented.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_leader_error_codes_identity.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_log_safe_peer_helper.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_log_safe_peer_routing.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_log_safe_peer_uses_sanitize_for_log.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_max_message_size_url_and_connect_args.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_max_total_rows_warning_does_not_advertise_disabled_token.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_phantom_returning_flag_does_not_exist_in_sa.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_create_drop_db_log_sanitises_ident.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_drop_user_tables_and_reap.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_drop_user_tables_log_sanitized.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_drop_user_tables_quote_escaping.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_format_url_ident_sanitisation.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_generate_driver_url.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_reap_dbs_info_log_credential_mask_and_sanitize.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_reap_dbs_log_sanitized.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_session_token_shape.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_provision_upsert.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_public_api.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_raft_collapse_markers_upstream_provenance.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_remap_loop_state_chains_from_matched_hop.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_remap_loop_state_helper_docstring_pin.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_requirements.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_requirements_closed_marker_present.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_requirements_recently_opened.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_reset_isolation_level_is_local_noop.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_result_processor_edge_branches.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_result_processor_log_payload_sanitized.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_result_processor_log_truncates_server_text.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_result_processor_warning_one_shot.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_savepoint_leader_flip_loud_failure.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_set_isolation_level_case_insensitive.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_stream_results_documented_eager_buffer.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_submodule_all.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_sync_connect_creator_fn_shape.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_sync_do_rollback_transport_fault_classified.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_sync_do_terminate.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_sync_do_terminate_log_includes_peer_and_id.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_time_colspec.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_time_tz_conversion.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_top_level_logger_has_null_handler.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_transitive_wire_import.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_transport_class_exceptions_consolidation.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_url_int_upper_boundary_off_by_one.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_url_max_rows_none_token.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_url_pre_validation.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_url_timeout_validators_share_ssot.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_version.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_walk_cause_chain_caps_pathological_group_size.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_walk_cause_chain_cycle_at_depth.py +0 -0
- {sqlalchemy_dqlite-0.2.0 → sqlalchemy_dqlite-0.2.1}/tests/test_walk_cause_chain_depth_budget.py +0 -0
- {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.
|
|
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.
|
|
26
|
-
Requires-Dist: dqlite-dbapi<0.3.0,>=0.2.
|
|
27
|
-
Requires-Dist: dqlite-wire<0.3.0,>=0.2.
|
|
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.
|
|
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.
|
|
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.
|
|
37
|
-
"dqlite-wire>=0.2.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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``
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
|
112
|
-
|
|
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
|
|
122
|
+
assert dialect.is_disconnect(exc, None, None) is True
|
sqlalchemy_dqlite-0.2.1/tests/test_is_disconnect_bare_dbe_short_circuits_on_slot_fatal_codes.py
ADDED
|
@@ -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
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
43
|
+
assert DqliteDialect().is_disconnect(e, None, None) is True
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
def test_database_error_via_cause_walk() -> None:
|