sqlalchemy-dqlite 0.1.2__tar.gz → 0.2.0__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/DEVELOPMENT.md +199 -0
- sqlalchemy_dqlite-0.2.0/PKG-INFO +185 -0
- sqlalchemy_dqlite-0.2.0/README.md +149 -0
- sqlalchemy_dqlite-0.2.0/pyproject.toml +141 -0
- sqlalchemy_dqlite-0.2.0/scripts/check-commit-msg.sh +140 -0
- sqlalchemy_dqlite-0.2.0/setup.cfg +32 -0
- sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/__init__.py +75 -0
- sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/aio.py +2267 -0
- sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/base.py +2992 -0
- sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/provision.py +518 -0
- sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/requirements.py +484 -0
- sqlalchemy_dqlite-0.2.0/tests/compliance/conftest.py +107 -0
- sqlalchemy_dqlite-0.2.0/tests/compliance/test_suite.py +19 -0
- sqlalchemy_dqlite-0.2.0/tests/conftest.py +80 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_async_cancel_during_commit_invalidates_slot.py +187 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_async_cancel_inside_engine_begin.py +80 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_async_concurrent_sessions.py +135 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_async_executemany.py +98 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_async_savepoint_cancellation_invalidates_slot.py +119 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_bulk_dml_returning.py +120 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_date_tz_drop.py +62 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_dial_func_via_connect_args_reaches_dbapi.py +77 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_engine_begin_is_atomic.py +291 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_engine_events_fire.py +90 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_inspect_async_and_close_parity.py +157 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_isolation_level_pin.py +56 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_json_column.py +152 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_native_boolean.py +74 -0
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/integration/test_orm_operations.py +74 -96
- sqlalchemy_dqlite-0.2.0/tests/integration/test_pool_pre_ping_leader_flip.py +110 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_pool_recycle_and_dispose_fan_out.py +176 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_pool_reset_on_return.py +66 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_reflect_column_types.py +83 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_savepoint_basic.py +173 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_savepoint_orm_session_begin_nested.py +189 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_savepoint_quoted_identifiers.py +152 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_time_and_reflection.py +125 -0
- sqlalchemy_dqlite-0.2.0/tests/integration/test_upsert.py +113 -0
- sqlalchemy_dqlite-0.2.0/tests/test_2pc_not_supported_error.py +86 -0
- sqlalchemy_dqlite-0.2.0/tests/test_adapter_arraysize_default_and_semantics.py +64 -0
- sqlalchemy_dqlite-0.2.0/tests/test_adapter_arraysize_validation.py +52 -0
- sqlalchemy_dqlite-0.2.0/tests/test_adapter_cursor_iterator_protocol.py +52 -0
- sqlalchemy_dqlite-0.2.0/tests/test_adapter_fetchmany_negative.py +57 -0
- sqlalchemy_dqlite-0.2.0/tests/test_adapter_handle_exception_hook.py +49 -0
- sqlalchemy_dqlite-0.2.0/tests/test_adapter_pickle_guards.py +60 -0
- sqlalchemy_dqlite-0.2.0/tests/test_adapter_slots.py +19 -0
- sqlalchemy_dqlite-0.2.0/tests/test_adapter_stale_rows.py +124 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_adapted_connection_public_paths.py +157 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_async_creator_fn.py +169 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_async_creator_fn_shape_check.py +75 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_await_staticmethod.py +32 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_final_close_suppression.py +157 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_loop_mismatch_remap.py +48 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_releases_inner_connection_pin.py +135 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_rollback_log.py +229 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_rollback_loop_already_running_remap.py +66 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_rollback_runtime_error_case_insensitive.py +64 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_suppress_and_soft_close_noop.py +117 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_cancellederror_force_close.py +97 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_in_greenlet_preflight.py +63 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_outside_greenlet.py +173 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_proxy_swap_arms.py +277 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_runtime_error_post_await.py +129 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_connect_construction_in_try_frame.py +108 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_connect_loop_state_remap.py +133 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_cursor_class_hooks.py +103 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_cursor_closed_before_server_side_precedence.py +63 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_cursor_time_error_remapped.py +80 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_dialect_do_close_releases_inner_strong_ref.py +99 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_do_close_fallback_force_close_transport.py +109 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_do_ping_cursor_construct_exception_arms.py +79 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_do_ping_override.py +118 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_do_ping_runtimeerror_returns_false.py +44 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_do_terminate_suppresses_exception.py +66 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_force_close_transport_cancelled_error.py +45 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_force_close_transport_dead_proxy.py +104 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_force_close_transport_docstring_pins.py +61 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_handle_exception_baseexceptiongroup_cancel_split.py +221 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_handle_exception_call_sites.py +114 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_handle_exception_delegates_to_remap_helper.py +199 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_is_disconnect_closed_proxy.py +70 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_no_in_function_imports.py +42 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_post_close_proxy_guards.py +166 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_terminate.py +229 -0
- sqlalchemy_dqlite-0.2.0/tests/test_aio_terminate_close_idempotence.py +71 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_cursor_closed_check.py +74 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_execute_handle_exception.py +90 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_execute_suppress_cancellederror.py +55 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_init_sa_reference_signature.py +59 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_init_three_call_shapes.py +64 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_cursor_doc.py +69 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_cursor_sync_ctxmgr.py +66 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapter.py +664 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_autocommit_property.py +43 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_closed_checks.py +68 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_lastrowid_sticky.py +122 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_op_lock_serialisation.py +102 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_terminate_handled_exceptions.py +36 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_commit_cancel_invalidates_slot.py +126 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_creator_fn_noncallable_rejected.py +31 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_after_connection_close.py +51 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_arraysize_accepts_zero_and_negative.py +73 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_arraysize_propagates_to_underlying.py +94 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_closed_guard.py +67 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_closed_guards.py +58 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_execute_close_suppress_behavioural.py +135 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_executemany_listifies_iterable.py +84 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_handle_exception.py +99 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_sync_only_context_manager.py +51 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_dialect_isolation_set.py +60 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_executemany_leader_flip_state_reset.py +121 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_handle_exception_case_insensitive.py +66 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_handle_exception_cause_walk.py +97 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_isolation_level.py +44 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_ping_cancel_cleanup.py +275 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_ping_close_arm_narrow_vs_programmer_bug.py +56 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_ping_single_execute_no_fetchone.py +37 -0
- sqlalchemy_dqlite-0.2.0/tests/test_async_ping_translates_dead_proxy_to_interface_error.py +73 -0
- sqlalchemy_dqlite-0.2.0/tests/test_audit_2026_05_round2_sa_changes.py +127 -0
- sqlalchemy_dqlite-0.2.0/tests/test_audit_2026_05_sa_changes.py +184 -0
- sqlalchemy_dqlite-0.2.0/tests/test_audit_2026_05_sa_coverage.py +220 -0
- sqlalchemy_dqlite-0.2.0/tests/test_bind_side_cross_type_rejection.py +93 -0
- sqlalchemy_dqlite-0.2.0/tests/test_close_runs_close_after_rollback_raise.py +95 -0
- sqlalchemy_dqlite-0.2.0/tests/test_collated_coverage_gaps.py +52 -0
- sqlalchemy_dqlite-0.2.0/tests/test_compiler_regexp_match.py +71 -0
- sqlalchemy_dqlite-0.2.0/tests/test_compliance_conftest_staleness_pin.py +129 -0
- sqlalchemy_dqlite-0.2.0/tests/test_connect_args_kwarg_validation.py +84 -0
- sqlalchemy_dqlite-0.2.0/tests/test_connect_args_value_validators.py +247 -0
- sqlalchemy_dqlite-0.2.0/tests/test_cross_package_version_parity.py +137 -0
- sqlalchemy_dqlite-0.2.0/tests/test_cursor_close_narrow_suppress.py +134 -0
- sqlalchemy_dqlite-0.2.0/tests/test_datetime_bind_widens_date.py +47 -0
- sqlalchemy_dqlite-0.2.0/tests/test_datetime_date_parse_errors.py +64 -0
- sqlalchemy_dqlite-0.2.0/tests/test_datetime_time_cross_type_payload.py +131 -0
- sqlalchemy_dqlite-0.2.0/tests/test_datetime_tz_conversion.py +59 -0
- sqlalchemy_dqlite-0.2.0/tests/test_ddl_kwarg_prefix_documented.py +66 -0
- sqlalchemy_dqlite-0.2.0/tests/test_description_alias_shared.py +32 -0
- sqlalchemy_dqlite-0.2.0/tests/test_detect_autocommit_setting.py +102 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dial_attempt_timeout_url_and_connect_args.py +75 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect.py +851 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_dialect_config.py +524 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_docstring_pins.py +81 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_final_annotated.py +40 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_inherits_pysqlite.py +36 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_init_eager_autocommit_rejection.py +50 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_init_preserves_pins.py +129 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_ipv6_url.py +88 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_set_isolation_level_annotation.py +35 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_sqlite_incompatibility_flags_pinned.py +41 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dialect_url_credentials.py +53 -0
- sqlalchemy_dqlite-0.2.0/tests/test_disconnect_substring_producer_emission.py +199 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_begin_cancel.py +49 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_begin_emits_begin.py +199 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_begin_savepoint_asymmetry_documented.py +58 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_close_calls_dbapi_close_directly.py +137 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_close_fallback_suppress_arm.py +131 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_close_fallback_uses_sync_transport_teardown.py +94 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_close_first_arm_absorbs_runtime_error.py +89 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_executemany_local_override_pin.py +52 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_ping_catches_programming_error.py +50 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_ping_close_narrow.py +135 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_ping_cursor_call_disconnect.py +92 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_ping_database_error.py +58 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_ping_dialect_specific_select_one.py +63 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_ping_interface_error_with_code.py +43 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_ping_non_disconnect_databaseerror_reraise.py +45 -0
- sqlalchemy_dqlite-0.2.0/tests/test_do_terminate_narrow_catch_propagates_refactor_break.py +65 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dqlitetime_bind_processor_returns_none.py +35 -0
- sqlalchemy_dqlite-0.2.0/tests/test_dqlitetime_cross_axis_narrow_with_timezone_true.py +50 -0
- sqlalchemy_dqlite-0.2.0/tests/test_drift_defence_pins.py +90 -0
- sqlalchemy_dqlite-0.2.0/tests/test_drop_user_tables_log_record_bounded.py +52 -0
- sqlalchemy_dqlite-0.2.0/tests/test_floor_compile_time_gate.py +113 -0
- sqlalchemy_dqlite-0.2.0/tests/test_handle_exception_must_raise.py +57 -0
- sqlalchemy_dqlite-0.2.0/tests/test_handle_exception_remap.py +79 -0
- sqlalchemy_dqlite-0.2.0/tests/test_handle_exception_two_pass_recovery_after_mixed_cancel_group.py +74 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_bare_databaseerror_type_identity.py +115 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_bare_oserror_walked_not_early_return.py +70 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_case_insensitive.py +35 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_cause_walk.py +121 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_closed_handle_with_id_suffix.py +99 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_cluster_error.py +108 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_database_error_substring.py +102 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_event_loop_already_running_prefix.py +53 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_event_loop_closed.py +49 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_event_loop_mismatch_substring.py +78 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_exception_group_and_substring_walk.py +82 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_interface_error_cause_walk.py +104 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_leader_lost_substring_case_insensitive.py +61 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_notfound_lookup_db.py +132 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_oe_code_restriction.py +37 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_protocol_error_arm.py +69 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_raft_collapse.py +80 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_raw_message.py +80 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_recognises_connection_invalidated_interfaceerror.py +96 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_recognises_cross_loop_interfaceerror.py +114 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_recognises_used_after_fork.py +63 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_substring_visible_within_dbapi_4kib_cap.py +56 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_untracked_savepoint_negative.py +84 -0
- sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_walks_cause_oserror.py +70 -0
- sqlalchemy_dqlite-0.2.0/tests/test_isolation_level_in_connect_args_directional_message.py +37 -0
- sqlalchemy_dqlite-0.2.0/tests/test_isolation_lookup_vs_values_list_divergence_documented.py +95 -0
- sqlalchemy_dqlite-0.2.0/tests/test_leader_error_codes_identity.py +16 -0
- sqlalchemy_dqlite-0.2.0/tests/test_log_safe_peer_helper.py +70 -0
- sqlalchemy_dqlite-0.2.0/tests/test_log_safe_peer_routing.py +95 -0
- sqlalchemy_dqlite-0.2.0/tests/test_log_safe_peer_uses_sanitize_for_log.py +52 -0
- sqlalchemy_dqlite-0.2.0/tests/test_max_message_size_url_and_connect_args.py +146 -0
- sqlalchemy_dqlite-0.2.0/tests/test_max_total_rows_warning_does_not_advertise_disabled_token.py +79 -0
- sqlalchemy_dqlite-0.2.0/tests/test_phantom_returning_flag_does_not_exist_in_sa.py +42 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_create_drop_db_log_sanitises_ident.py +58 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_drop_user_tables_and_reap.py +208 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_drop_user_tables_log_sanitized.py +78 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_drop_user_tables_quote_escaping.py +83 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_format_url_ident_sanitisation.py +172 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_generate_driver_url.py +53 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_reap_dbs_info_log_credential_mask_and_sanitize.py +86 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_reap_dbs_log_sanitized.py +54 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_session_token_shape.py +72 -0
- sqlalchemy_dqlite-0.2.0/tests/test_provision_upsert.py +42 -0
- sqlalchemy_dqlite-0.2.0/tests/test_public_api.py +122 -0
- sqlalchemy_dqlite-0.2.0/tests/test_raft_collapse_markers_upstream_provenance.py +55 -0
- sqlalchemy_dqlite-0.2.0/tests/test_remap_loop_state_chains_from_matched_hop.py +104 -0
- sqlalchemy_dqlite-0.2.0/tests/test_remap_loop_state_helper_docstring_pin.py +33 -0
- sqlalchemy_dqlite-0.2.0/tests/test_requirements.py +146 -0
- sqlalchemy_dqlite-0.2.0/tests/test_requirements_closed_marker_present.py +54 -0
- sqlalchemy_dqlite-0.2.0/tests/test_requirements_recently_opened.py +46 -0
- sqlalchemy_dqlite-0.2.0/tests/test_reset_isolation_level_is_local_noop.py +58 -0
- sqlalchemy_dqlite-0.2.0/tests/test_result_processor_edge_branches.py +58 -0
- sqlalchemy_dqlite-0.2.0/tests/test_result_processor_log_payload_sanitized.py +101 -0
- sqlalchemy_dqlite-0.2.0/tests/test_result_processor_log_truncates_server_text.py +73 -0
- sqlalchemy_dqlite-0.2.0/tests/test_result_processor_warning_one_shot.py +89 -0
- sqlalchemy_dqlite-0.2.0/tests/test_savepoint_leader_flip_loud_failure.py +138 -0
- sqlalchemy_dqlite-0.2.0/tests/test_set_isolation_level_case_insensitive.py +77 -0
- sqlalchemy_dqlite-0.2.0/tests/test_stream_results_documented_eager_buffer.py +116 -0
- sqlalchemy_dqlite-0.2.0/tests/test_submodule_all.py +34 -0
- sqlalchemy_dqlite-0.2.0/tests/test_sync_connect_creator_fn_shape.py +53 -0
- sqlalchemy_dqlite-0.2.0/tests/test_sync_do_rollback_transport_fault_classified.py +98 -0
- sqlalchemy_dqlite-0.2.0/tests/test_sync_do_terminate.py +67 -0
- sqlalchemy_dqlite-0.2.0/tests/test_sync_do_terminate_log_includes_peer_and_id.py +34 -0
- sqlalchemy_dqlite-0.2.0/tests/test_time_colspec.py +44 -0
- sqlalchemy_dqlite-0.2.0/tests/test_time_tz_conversion.py +58 -0
- sqlalchemy_dqlite-0.2.0/tests/test_top_level_logger_has_null_handler.py +16 -0
- sqlalchemy_dqlite-0.2.0/tests/test_transitive_wire_import.py +43 -0
- sqlalchemy_dqlite-0.2.0/tests/test_transport_class_exceptions_consolidation.py +249 -0
- sqlalchemy_dqlite-0.2.0/tests/test_url_int_upper_boundary_off_by_one.py +87 -0
- sqlalchemy_dqlite-0.2.0/tests/test_url_max_rows_none_token.py +95 -0
- sqlalchemy_dqlite-0.2.0/tests/test_url_pre_validation.py +77 -0
- sqlalchemy_dqlite-0.2.0/tests/test_url_timeout_validators_share_ssot.py +98 -0
- sqlalchemy_dqlite-0.2.0/tests/test_version.py +18 -0
- sqlalchemy_dqlite-0.2.0/tests/test_walk_cause_chain_caps_pathological_group_size.py +83 -0
- sqlalchemy_dqlite-0.2.0/tests/test_walk_cause_chain_cycle_at_depth.py +33 -0
- sqlalchemy_dqlite-0.2.0/tests/test_walk_cause_chain_depth_budget.py +82 -0
- sqlalchemy_dqlite-0.2.0/tests/test_wire_decode_failed_prefix_constant_used_by_dialect.py +25 -0
- sqlalchemy_dqlite-0.2.0/uv.lock +455 -0
- sqlalchemy_dqlite-0.1.2/DEVELOPMENT.md +0 -90
- sqlalchemy_dqlite-0.1.2/PKG-INFO +0 -76
- sqlalchemy_dqlite-0.1.2/README.md +0 -44
- sqlalchemy_dqlite-0.1.2/pyproject.toml +0 -67
- sqlalchemy_dqlite-0.1.2/src/sqlalchemydqlite/__init__.py +0 -7
- sqlalchemy_dqlite-0.1.2/src/sqlalchemydqlite/aio.py +0 -199
- sqlalchemy_dqlite-0.1.2/src/sqlalchemydqlite/base.py +0 -122
- sqlalchemy_dqlite-0.1.2/src/sqlalchemydqlite/requirements.py +0 -45
- sqlalchemy_dqlite-0.1.2/tests/conftest.py +0 -15
- sqlalchemy_dqlite-0.1.2/tests/test_dialect.py +0 -94
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/.gitignore +0 -0
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/LICENSE.md +0 -0
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/src/sqlalchemydqlite/py.typed +0 -0
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/__init__.py +0 -0
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/integration/__init__.py +0 -0
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/integration/conftest.py +0 -0
- {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/integration/test_async_context.py +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- Python 3.13+
|
|
6
|
+
- [uv](https://github.com/astral-sh/uv) - Fast Python package manager
|
|
7
|
+
- Docker (for integration tests)
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Clone [dqlite-wire](https://github.com/letsdiscodev/python-dqlite-wire), [dqlite-client](https://github.com/letsdiscodev/python-dqlite-client), and [dqlite-dbapi](https://github.com/letsdiscodev/python-dqlite-dbapi) alongside this checkout; `[tool.uv.sources]` in `pyproject.toml` points the sibling packages at their `../python-dqlite-*` directories, so `uv sync` picks up in-tree changes automatically.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Install uv (if not already installed)
|
|
15
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
16
|
+
|
|
17
|
+
# Create virtual environment and install dependencies
|
|
18
|
+
uv sync --extra dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Development Tools
|
|
22
|
+
|
|
23
|
+
| Tool | Purpose | Command |
|
|
24
|
+
|------|---------|---------|
|
|
25
|
+
| **pytest** | Testing framework | `pytest` |
|
|
26
|
+
| **ruff** | Linter (replaces flake8, isort, etc.) | `ruff check` |
|
|
27
|
+
| **ruff format** | Code formatter (replaces black) | `ruff format` |
|
|
28
|
+
| **mypy** | Static type checker | `mypy src` |
|
|
29
|
+
|
|
30
|
+
## Running Tests
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Run unit tests only
|
|
34
|
+
.venv/bin/pytest tests/ --ignore=tests/integration
|
|
35
|
+
|
|
36
|
+
# Run all tests (requires Docker cluster)
|
|
37
|
+
cd ../dqlite-test-cluster && docker compose up -d
|
|
38
|
+
.venv/bin/pytest tests/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The default pytest run **skips** `tests/compliance/` (the SQLAlchemy
|
|
42
|
+
dialect compliance suite). See "SQLAlchemy compliance suite" below
|
|
43
|
+
for the why and how to run it.
|
|
44
|
+
|
|
45
|
+
## SQLAlchemy compliance suite
|
|
46
|
+
|
|
47
|
+
The SQLAlchemy project ships an in-tree, dialect-agnostic test suite
|
|
48
|
+
at `sqlalchemy.testing.suite`. Third-party dialects engage it by
|
|
49
|
+
providing a `Requirements` class (declares which features are
|
|
50
|
+
supported), a `provision.py` module (handles per-test database
|
|
51
|
+
lifecycle), and a `tests/test_suite.py` that imports the suite. SA's
|
|
52
|
+
pytest plugin then runs every suite test against the dialect, gated
|
|
53
|
+
by `Requirements.<feature>.enabled`.
|
|
54
|
+
|
|
55
|
+
This package wires the suite under `tests/compliance/`. Running it:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Start the cluster (any reachable dqlite cluster works; the suite
|
|
59
|
+
# defaults to localhost:9001 via setup.cfg's [db] section).
|
|
60
|
+
.venv/bin/pytest tests/compliance/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Why the default pytest run skips it
|
|
64
|
+
|
|
65
|
+
`tests/compliance/conftest.py` loads `sqlalchemy.testing.plugin.pytestplugin`,
|
|
66
|
+
which **replaces pytest's default test-collection model** — it filters
|
|
67
|
+
discovery to `sqlalchemy.testing.fixtures.TestBase` subclasses only.
|
|
68
|
+
If we let it load globally for `pytest tests/`, the project's existing
|
|
69
|
+
unit and integration tests under `tests/test_*.py` would silently
|
|
70
|
+
disappear from the run.
|
|
71
|
+
|
|
72
|
+
The plugin also requires CLI options (`--dburi`, etc.) the project's
|
|
73
|
+
runners do not pass; without them it errors at session start.
|
|
74
|
+
|
|
75
|
+
To keep the two test corpora cleanly separated, `pyproject.toml`'s
|
|
76
|
+
pytest config has `addopts=["--ignore=tests/compliance"]`. Run the
|
|
77
|
+
suite explicitly with `pytest tests/compliance/`.
|
|
78
|
+
|
|
79
|
+
### Why ~700 suite tests are skipped (and that's correct)
|
|
80
|
+
|
|
81
|
+
Each compliance run reports a large `skipped` count (currently
|
|
82
|
+
~689). Every skip is gated by a `Requirements.<feature>` declaration
|
|
83
|
+
in `src/sqlalchemydqlite/requirements.py` that says "dqlite doesn't
|
|
84
|
+
support this." The skipped tests are testing capabilities the
|
|
85
|
+
underlying database genuinely lacks — running them would fail with
|
|
86
|
+
`syntax error` / `unknown database` / `no such function`, not with
|
|
87
|
+
real bugs.
|
|
88
|
+
|
|
89
|
+
Examples of features dqlite doesn't have (each declared
|
|
90
|
+
`exclusions.closed()`):
|
|
91
|
+
|
|
92
|
+
- ATTACH-DATABASE schemas (`schemas`)
|
|
93
|
+
- Two-phase commit (`two_phase_transactions`)
|
|
94
|
+
- `CREATE SEQUENCE` (`sequences` — SQLite has no native sequences;
|
|
95
|
+
this one is gated by the dialect-level `supports_sequences = False`
|
|
96
|
+
flag inherited from the SQLite dialect rather than an explicit
|
|
97
|
+
`closed()` in `requirements.py`)
|
|
98
|
+
- Parenthesised SELECTs in UNION (`parens_in_union_*`)
|
|
99
|
+
- Cross-schema FK reflection (`cross_schema_fk_reflection`)
|
|
100
|
+
|
|
101
|
+
A future dqlite version that adds the feature flips the requirement
|
|
102
|
+
to `exclusions.open()`, and the suite tests for that feature start
|
|
103
|
+
running.
|
|
104
|
+
|
|
105
|
+
### Adjusting Requirements
|
|
106
|
+
|
|
107
|
+
When the suite surfaces a real bug, fix the dialect. When the suite
|
|
108
|
+
runs a test for a capability dqlite genuinely doesn't have, add or
|
|
109
|
+
adjust a `Requirements.<name>` property in `requirements.py` with a
|
|
110
|
+
docstring explaining why. The matching property in SA's
|
|
111
|
+
`SuiteRequirements` has a docstring describing what the feature is
|
|
112
|
+
— mirror that in the override.
|
|
113
|
+
|
|
114
|
+
### Per-session unique database names
|
|
115
|
+
|
|
116
|
+
dqlite has no `DROP DATABASE` primitive. The cluster persists every
|
|
117
|
+
database name ever opened, across pytest sessions and across the
|
|
118
|
+
project's other integration tests. To prevent cross-run schema
|
|
119
|
+
bleed in reflection tests that enumerate "all tables in the schema",
|
|
120
|
+
`src/sqlalchemydqlite/provision.py` generates a per-session token
|
|
121
|
+
(`sa_<pid>_<monotonic-ns>`) and suffixes every test database name
|
|
122
|
+
with it. Each pytest run gets a fresh, empty database namespace on
|
|
123
|
+
the cluster.
|
|
124
|
+
|
|
125
|
+
## Linting & Formatting
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Lint
|
|
129
|
+
.venv/bin/ruff check src tests
|
|
130
|
+
|
|
131
|
+
# Auto-fix lint issues
|
|
132
|
+
.venv/bin/ruff check --fix src tests
|
|
133
|
+
|
|
134
|
+
# Format
|
|
135
|
+
.venv/bin/ruff format src tests
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Type Checking
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
.venv/bin/mypy src
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Pre-commit Workflow
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
.venv/bin/ruff format src tests
|
|
148
|
+
.venv/bin/ruff check --fix src tests
|
|
149
|
+
.venv/bin/mypy src
|
|
150
|
+
.venv/bin/pytest tests/ --ignore=tests/integration
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Commit message hygiene
|
|
154
|
+
|
|
155
|
+
Commit messages must not contain workflow vocabulary
|
|
156
|
+
("Round N" / "Cycle N" / "Phase N" / "Bundle X" / "ultrathink"),
|
|
157
|
+
`ISSUE-<token>` identifiers, or `done/*.md` filename references.
|
|
158
|
+
The durable referent for cross-commit citations is the commit hash;
|
|
159
|
+
the workflow token is ornamental and leaks the development process
|
|
160
|
+
into the published history.
|
|
161
|
+
|
|
162
|
+
`scripts/check-commit-msg.sh` enforces the rule. Install it as a
|
|
163
|
+
local `commit-msg` hook:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
ln -s ../../scripts/check-commit-msg.sh .git/hooks/commit-msg
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Or lint a range manually:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
scripts/check-commit-msg.sh --range origin/main..HEAD
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The same script ships in `python-dqlite-wire/scripts/` so the
|
|
176
|
+
workspace shares a single rule across packages. Published commits
|
|
177
|
+
that already contain the pattern (e.g. `33d1d96`'s "Round 7"
|
|
178
|
+
opener) are left intact — amending would rewrite SHAs sibling
|
|
179
|
+
packages reference; the script is forward-looking only.
|
|
180
|
+
|
|
181
|
+
## SQLAlchemy URL Format
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
# Sync
|
|
185
|
+
dqlite://host:port/database
|
|
186
|
+
|
|
187
|
+
# Async
|
|
188
|
+
dqlite+aio://host:port/database
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Dialect Registration
|
|
192
|
+
|
|
193
|
+
The dialects are registered via entry points in `pyproject.toml`:
|
|
194
|
+
|
|
195
|
+
```toml
|
|
196
|
+
[project.entry-points."sqlalchemy.dialects"]
|
|
197
|
+
dqlite = "sqlalchemydqlite:DqliteDialect"
|
|
198
|
+
"dqlite.aio" = "sqlalchemydqlite.aio:DqliteDialect_aio"
|
|
199
|
+
```
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-dqlite
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: SQLAlchemy 2.0 dialect for dqlite distributed SQLite
|
|
5
|
+
Project-URL: Homepage, https://github.com/letsdiscodev/sqlalchemy-dqlite
|
|
6
|
+
Project-URL: Repository, https://github.com/letsdiscodev/sqlalchemy-dqlite
|
|
7
|
+
Project-URL: Issues, https://github.com/letsdiscodev/sqlalchemy-dqlite/issues
|
|
8
|
+
Author-email: Antoine Leclair <antoineleclair@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.md
|
|
11
|
+
Keywords: database,distributed,dqlite,orm,sqlalchemy,sqlite
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
22
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
23
|
+
Classifier: Typing :: Typed
|
|
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
|
|
28
|
+
Requires-Dist: sqlalchemy<3.0,>=2.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# sqlalchemy-dqlite
|
|
38
|
+
|
|
39
|
+
SQLAlchemy 2.0 dialect for [dqlite](https://dqlite.io/).
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install sqlalchemy-dqlite
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from sqlalchemy import create_engine, text
|
|
51
|
+
|
|
52
|
+
# Sync
|
|
53
|
+
engine = create_engine("dqlite://localhost:9001/mydb")
|
|
54
|
+
with engine.connect() as conn:
|
|
55
|
+
result = conn.execute(text("SELECT 1"))
|
|
56
|
+
print(result.fetchone())
|
|
57
|
+
|
|
58
|
+
# Async
|
|
59
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
60
|
+
|
|
61
|
+
async_engine = create_async_engine("dqlite+aio://localhost:9001/mydb")
|
|
62
|
+
async with async_engine.connect() as conn:
|
|
63
|
+
result = await conn.execute(text("SELECT 1"))
|
|
64
|
+
print(result.fetchone())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Transactions
|
|
68
|
+
|
|
69
|
+
SQLAlchemy owns the `BEGIN`/`COMMIT`/`ROLLBACK` for any block opened via
|
|
70
|
+
`engine.begin()`, `connection.begin()`, or `session.begin()`. Do not
|
|
71
|
+
issue raw `BEGIN` yourself.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from sqlalchemy import create_engine, text
|
|
75
|
+
|
|
76
|
+
engine = create_engine("dqlite://localhost:9001/mydb")
|
|
77
|
+
|
|
78
|
+
# OK — SA emits BEGIN / COMMIT for you
|
|
79
|
+
with engine.begin() as conn:
|
|
80
|
+
conn.execute(text("INSERT INTO t VALUES (1)"))
|
|
81
|
+
|
|
82
|
+
# WRONG — second BEGIN inside an SA-managed transaction errors with
|
|
83
|
+
# OperationalError: cannot start a transaction within a transaction
|
|
84
|
+
with engine.begin() as conn:
|
|
85
|
+
conn.execute(text("BEGIN")) # error
|
|
86
|
+
conn.execute(text("INSERT INTO t VALUES (1)"))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The same rule applies to `engine.connect()`: SA auto-begins a
|
|
90
|
+
transaction on the first execute, so a user-issued `text("BEGIN")`
|
|
91
|
+
collides the same way. This matches every other SA backend (pysqlite,
|
|
92
|
+
postgres, mysql); SA's transaction model is universal.
|
|
93
|
+
|
|
94
|
+
`isolation_level="AUTOCOMMIT"` is rejected — every dqlite statement
|
|
95
|
+
goes through Raft consensus and there is no per-statement autocommit
|
|
96
|
+
mode. Use `engine.begin()` (or `connection.begin()`) for writes.
|
|
97
|
+
|
|
98
|
+
See SQLAlchemy's [transaction
|
|
99
|
+
docs](https://docs.sqlalchemy.org/en/20/core/connections.html#using-transactions)
|
|
100
|
+
for the full model.
|
|
101
|
+
|
|
102
|
+
## Savepoint naming
|
|
103
|
+
|
|
104
|
+
The dqlite client tracks active SAVEPOINTs to keep the SQLAlchemy
|
|
105
|
+
pool's ROLLBACK-on-checkin path correct. The tracker only handles
|
|
106
|
+
bare-ASCII SQLite identifiers (e.g. `sa_savepoint_1`, `my_sp`) —
|
|
107
|
+
SQLAlchemy's generated savepoint names always match this shape, so
|
|
108
|
+
`engine.begin()` / `Session.begin_nested()` / `connection.begin_nested()`
|
|
109
|
+
are unaffected.
|
|
110
|
+
|
|
111
|
+
If user-issued raw SQL uses quoted, backticked, square-bracketed,
|
|
112
|
+
unicode, or leading-digit savepoint names (e.g.
|
|
113
|
+
`text('SAVEPOINT "weird name"')`), the client conservatively flags
|
|
114
|
+
the connection as carrying an untracked savepoint. On the next pool
|
|
115
|
+
checkin SQLAlchemy issues a safety `ROLLBACK`, paying one extra
|
|
116
|
+
round-trip per checkout for the remainder of that connection's
|
|
117
|
+
lifetime in the pool. Stick to bare-ASCII SAVEPOINT names in raw
|
|
118
|
+
text SQL to avoid the overhead, or accept the per-checkout cost.
|
|
119
|
+
|
|
120
|
+
## URL Format
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
dqlite://host:port/database
|
|
124
|
+
dqlite+aio://host:port/database
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When a query parameter is repeated
|
|
128
|
+
(`?max_total_rows=100&max_total_rows=200`), the **last** occurrence
|
|
129
|
+
wins. This matches `urllib.parse.parse_qsl` ordering. Templated
|
|
130
|
+
connection URLs that layer values from multiple sources should be
|
|
131
|
+
aware that duplicated keys silently override earlier values rather
|
|
132
|
+
than raising.
|
|
133
|
+
|
|
134
|
+
The URL host:port pair is the bootstrap address — the dqlite client
|
|
135
|
+
discovers the rest of the cluster from that one node's leader-info
|
|
136
|
+
response. If the URL host is unreachable, leader-discovery cannot
|
|
137
|
+
start; operators that want bootstrap-from-many-addresses should put
|
|
138
|
+
a load balancer or DNS round-robin in front of the cluster, or rotate
|
|
139
|
+
the URL host across deployments. Multi-address bootstrap is not
|
|
140
|
+
exposed at the dialect URL surface.
|
|
141
|
+
|
|
142
|
+
## STRICT-table DDL is unavailable through SA's compiler
|
|
143
|
+
|
|
144
|
+
SA's SQLite dialect gates STRICT-table compilation on
|
|
145
|
+
`server_version_info >= (3, 37)` (`sqlalchemy/dialects/sqlite/base.py`).
|
|
146
|
+
The dqlite dbapi pins `sqlite_version_info = (3, 35, 0)` as the
|
|
147
|
+
documented floor (see
|
|
148
|
+
`python-dqlite-dbapi/src/dqlitedbapi/_constants.py`) to avoid silently
|
|
149
|
+
rejecting connections to older-server clusters that ship SQLite below
|
|
150
|
+
the floor. As a consequence, SA's compiler will NOT emit
|
|
151
|
+
`CREATE TABLE ... STRICT` DDL through this dialect, even when the
|
|
152
|
+
cluster ships SQLite 3.37 or newer.
|
|
153
|
+
|
|
154
|
+
If your cluster supports STRICT tables and you want STRICT semantics,
|
|
155
|
+
emit the DDL via raw SQL (`engine.execute(text("CREATE TABLE ...
|
|
156
|
+
STRICT"))`) rather than SA's `Table` / `Column` model.
|
|
157
|
+
|
|
158
|
+
## Cross-version semantic shift: NULL in BOOLEAN/DATETIME columns
|
|
159
|
+
|
|
160
|
+
Upstream dqlite commit `f30fc99` (`query: preserve SQLITE_NULL type
|
|
161
|
+
for NULL values`, 2026-01-25) changed the wire encoding of NULL cells
|
|
162
|
+
in columns declared `BOOLEAN`, `DATE`, `DATETIME`, or `TIMESTAMP`.
|
|
163
|
+
Before that commit, a NULL in a `BOOLEAN` column was emitted as
|
|
164
|
+
`BOOLEAN(0)` (decodes to `False`) and a NULL in a `DATETIME` column
|
|
165
|
+
was emitted as `ISO8601("")` — indistinguishable on the wire from
|
|
166
|
+
real `FALSE` / empty-string values. After the commit, NULL is emitted
|
|
167
|
+
with the SQLite NULL type and decodes to `None`.
|
|
168
|
+
|
|
169
|
+
ORM models with `Boolean()` and `DateTime()` columns will start
|
|
170
|
+
returning `None` for previously-`False` / `""` values after a server
|
|
171
|
+
upgrade. SQLAlchemy `nullable=False` constraints will start tripping
|
|
172
|
+
on rows that previously decoded to non-NULL. There is no driver-level
|
|
173
|
+
handshake distinguishing the two server versions — check your dqlite
|
|
174
|
+
cluster version before relying on the post-fix semantics.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
See [DEVELOPMENT.md](DEVELOPMENT.md) for setup, contribution guidelines,
|
|
179
|
+
and the SQLAlchemy compliance test-suite documentation (how to run it
|
|
180
|
+
locally, why it's not part of the default `pytest` invocation, and
|
|
181
|
+
why ~700 of its tests are correctly skipped).
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# sqlalchemy-dqlite
|
|
2
|
+
|
|
3
|
+
SQLAlchemy 2.0 dialect for [dqlite](https://dqlite.io/).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install sqlalchemy-dqlite
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from sqlalchemy import create_engine, text
|
|
15
|
+
|
|
16
|
+
# Sync
|
|
17
|
+
engine = create_engine("dqlite://localhost:9001/mydb")
|
|
18
|
+
with engine.connect() as conn:
|
|
19
|
+
result = conn.execute(text("SELECT 1"))
|
|
20
|
+
print(result.fetchone())
|
|
21
|
+
|
|
22
|
+
# Async
|
|
23
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
24
|
+
|
|
25
|
+
async_engine = create_async_engine("dqlite+aio://localhost:9001/mydb")
|
|
26
|
+
async with async_engine.connect() as conn:
|
|
27
|
+
result = await conn.execute(text("SELECT 1"))
|
|
28
|
+
print(result.fetchone())
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Transactions
|
|
32
|
+
|
|
33
|
+
SQLAlchemy owns the `BEGIN`/`COMMIT`/`ROLLBACK` for any block opened via
|
|
34
|
+
`engine.begin()`, `connection.begin()`, or `session.begin()`. Do not
|
|
35
|
+
issue raw `BEGIN` yourself.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from sqlalchemy import create_engine, text
|
|
39
|
+
|
|
40
|
+
engine = create_engine("dqlite://localhost:9001/mydb")
|
|
41
|
+
|
|
42
|
+
# OK — SA emits BEGIN / COMMIT for you
|
|
43
|
+
with engine.begin() as conn:
|
|
44
|
+
conn.execute(text("INSERT INTO t VALUES (1)"))
|
|
45
|
+
|
|
46
|
+
# WRONG — second BEGIN inside an SA-managed transaction errors with
|
|
47
|
+
# OperationalError: cannot start a transaction within a transaction
|
|
48
|
+
with engine.begin() as conn:
|
|
49
|
+
conn.execute(text("BEGIN")) # error
|
|
50
|
+
conn.execute(text("INSERT INTO t VALUES (1)"))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The same rule applies to `engine.connect()`: SA auto-begins a
|
|
54
|
+
transaction on the first execute, so a user-issued `text("BEGIN")`
|
|
55
|
+
collides the same way. This matches every other SA backend (pysqlite,
|
|
56
|
+
postgres, mysql); SA's transaction model is universal.
|
|
57
|
+
|
|
58
|
+
`isolation_level="AUTOCOMMIT"` is rejected — every dqlite statement
|
|
59
|
+
goes through Raft consensus and there is no per-statement autocommit
|
|
60
|
+
mode. Use `engine.begin()` (or `connection.begin()`) for writes.
|
|
61
|
+
|
|
62
|
+
See SQLAlchemy's [transaction
|
|
63
|
+
docs](https://docs.sqlalchemy.org/en/20/core/connections.html#using-transactions)
|
|
64
|
+
for the full model.
|
|
65
|
+
|
|
66
|
+
## Savepoint naming
|
|
67
|
+
|
|
68
|
+
The dqlite client tracks active SAVEPOINTs to keep the SQLAlchemy
|
|
69
|
+
pool's ROLLBACK-on-checkin path correct. The tracker only handles
|
|
70
|
+
bare-ASCII SQLite identifiers (e.g. `sa_savepoint_1`, `my_sp`) —
|
|
71
|
+
SQLAlchemy's generated savepoint names always match this shape, so
|
|
72
|
+
`engine.begin()` / `Session.begin_nested()` / `connection.begin_nested()`
|
|
73
|
+
are unaffected.
|
|
74
|
+
|
|
75
|
+
If user-issued raw SQL uses quoted, backticked, square-bracketed,
|
|
76
|
+
unicode, or leading-digit savepoint names (e.g.
|
|
77
|
+
`text('SAVEPOINT "weird name"')`), the client conservatively flags
|
|
78
|
+
the connection as carrying an untracked savepoint. On the next pool
|
|
79
|
+
checkin SQLAlchemy issues a safety `ROLLBACK`, paying one extra
|
|
80
|
+
round-trip per checkout for the remainder of that connection's
|
|
81
|
+
lifetime in the pool. Stick to bare-ASCII SAVEPOINT names in raw
|
|
82
|
+
text SQL to avoid the overhead, or accept the per-checkout cost.
|
|
83
|
+
|
|
84
|
+
## URL Format
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
dqlite://host:port/database
|
|
88
|
+
dqlite+aio://host:port/database
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
When a query parameter is repeated
|
|
92
|
+
(`?max_total_rows=100&max_total_rows=200`), the **last** occurrence
|
|
93
|
+
wins. This matches `urllib.parse.parse_qsl` ordering. Templated
|
|
94
|
+
connection URLs that layer values from multiple sources should be
|
|
95
|
+
aware that duplicated keys silently override earlier values rather
|
|
96
|
+
than raising.
|
|
97
|
+
|
|
98
|
+
The URL host:port pair is the bootstrap address — the dqlite client
|
|
99
|
+
discovers the rest of the cluster from that one node's leader-info
|
|
100
|
+
response. If the URL host is unreachable, leader-discovery cannot
|
|
101
|
+
start; operators that want bootstrap-from-many-addresses should put
|
|
102
|
+
a load balancer or DNS round-robin in front of the cluster, or rotate
|
|
103
|
+
the URL host across deployments. Multi-address bootstrap is not
|
|
104
|
+
exposed at the dialect URL surface.
|
|
105
|
+
|
|
106
|
+
## STRICT-table DDL is unavailable through SA's compiler
|
|
107
|
+
|
|
108
|
+
SA's SQLite dialect gates STRICT-table compilation on
|
|
109
|
+
`server_version_info >= (3, 37)` (`sqlalchemy/dialects/sqlite/base.py`).
|
|
110
|
+
The dqlite dbapi pins `sqlite_version_info = (3, 35, 0)` as the
|
|
111
|
+
documented floor (see
|
|
112
|
+
`python-dqlite-dbapi/src/dqlitedbapi/_constants.py`) to avoid silently
|
|
113
|
+
rejecting connections to older-server clusters that ship SQLite below
|
|
114
|
+
the floor. As a consequence, SA's compiler will NOT emit
|
|
115
|
+
`CREATE TABLE ... STRICT` DDL through this dialect, even when the
|
|
116
|
+
cluster ships SQLite 3.37 or newer.
|
|
117
|
+
|
|
118
|
+
If your cluster supports STRICT tables and you want STRICT semantics,
|
|
119
|
+
emit the DDL via raw SQL (`engine.execute(text("CREATE TABLE ...
|
|
120
|
+
STRICT"))`) rather than SA's `Table` / `Column` model.
|
|
121
|
+
|
|
122
|
+
## Cross-version semantic shift: NULL in BOOLEAN/DATETIME columns
|
|
123
|
+
|
|
124
|
+
Upstream dqlite commit `f30fc99` (`query: preserve SQLITE_NULL type
|
|
125
|
+
for NULL values`, 2026-01-25) changed the wire encoding of NULL cells
|
|
126
|
+
in columns declared `BOOLEAN`, `DATE`, `DATETIME`, or `TIMESTAMP`.
|
|
127
|
+
Before that commit, a NULL in a `BOOLEAN` column was emitted as
|
|
128
|
+
`BOOLEAN(0)` (decodes to `False`) and a NULL in a `DATETIME` column
|
|
129
|
+
was emitted as `ISO8601("")` — indistinguishable on the wire from
|
|
130
|
+
real `FALSE` / empty-string values. After the commit, NULL is emitted
|
|
131
|
+
with the SQLite NULL type and decodes to `None`.
|
|
132
|
+
|
|
133
|
+
ORM models with `Boolean()` and `DateTime()` columns will start
|
|
134
|
+
returning `None` for previously-`False` / `""` values after a server
|
|
135
|
+
upgrade. SQLAlchemy `nullable=False` constraints will start tripping
|
|
136
|
+
on rows that previously decoded to non-NULL. There is no driver-level
|
|
137
|
+
handshake distinguishing the two server versions — check your dqlite
|
|
138
|
+
cluster version before relying on the post-fix semantics.
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
See [DEVELOPMENT.md](DEVELOPMENT.md) for setup, contribution guidelines,
|
|
143
|
+
and the SQLAlchemy compliance test-suite documentation (how to run it
|
|
144
|
+
locally, why it's not part of the default `pytest` invocation, and
|
|
145
|
+
why ~700 of its tests are correctly skipped).
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sqlalchemy-dqlite"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "SQLAlchemy 2.0 dialect for dqlite distributed SQLite"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Antoine Leclair", email = "antoineleclair@gmail.com" }]
|
|
13
|
+
keywords = ["dqlite", "sqlite", "distributed", "database", "sqlalchemy", "orm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Framework :: AsyncIO",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: Database",
|
|
24
|
+
"Topic :: Database :: Database Engines/Servers",
|
|
25
|
+
"Topic :: Database :: Front-Ends",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"dqlite-dbapi>=0.2.0,<0.3.0",
|
|
30
|
+
# The SA dialect imports dqliteclient and dqlitewire directly
|
|
31
|
+
# (base.py / aio.py reach into client exception classes for
|
|
32
|
+
# is_disconnect, and into dqlitewire for shared helpers). Pin
|
|
33
|
+
# both transitively so a future major-version bump in either
|
|
34
|
+
# package surfaces at install time, not at module-import time
|
|
35
|
+
# in user code.
|
|
36
|
+
"dqlite-client>=0.2.0,<0.3.0",
|
|
37
|
+
"dqlite-wire>=0.2.0,<0.3.0",
|
|
38
|
+
"sqlalchemy>=2.0,<3.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.uv.sources]
|
|
42
|
+
dqlite-dbapi = { path = "../python-dqlite-dbapi", editable = true }
|
|
43
|
+
dqlite-client = { path = "../python-dqlite-client", editable = true }
|
|
44
|
+
dqlite-wire = { path = "../python-dqlite-wire", editable = true }
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/letsdiscodev/sqlalchemy-dqlite"
|
|
48
|
+
Repository = "https://github.com/letsdiscodev/sqlalchemy-dqlite"
|
|
49
|
+
Issues = "https://github.com/letsdiscodev/sqlalchemy-dqlite/issues"
|
|
50
|
+
|
|
51
|
+
[project.optional-dependencies]
|
|
52
|
+
dev = ["pytest>=8.0", "pytest-cov>=4.0", "pytest-asyncio>=0.23", "mypy>=1.0", "ruff>=0.4"]
|
|
53
|
+
|
|
54
|
+
[project.entry-points."sqlalchemy.dialects"]
|
|
55
|
+
# Bare ``dqlite`` is the URL-default; ``dqlite.dqlitedbapi`` is the
|
|
56
|
+
# explicit driver-suffix form (URL ``dqlite+dqlitedbapi://``). Both
|
|
57
|
+
# resolve to the same class, mirroring how ``sqlite+pysqlite://`` and
|
|
58
|
+
# bare ``sqlite://`` both reach ``SQLiteDialect_pysqlite`` in SA's
|
|
59
|
+
# built-in dispatch (see ``sqlalchemy/dialects/__init__.py:_auto_fn``).
|
|
60
|
+
# We have to register the explicit form because we are external to SA;
|
|
61
|
+
# its built-ins are auto-routed by name-splitting, ours are not.
|
|
62
|
+
dqlite = "sqlalchemydqlite:DqliteDialect"
|
|
63
|
+
"dqlite.dqlitedbapi" = "sqlalchemydqlite:DqliteDialect"
|
|
64
|
+
"dqlite.aio" = "sqlalchemydqlite.aio:DqliteDialect_aio"
|
|
65
|
+
|
|
66
|
+
[tool.hatch.build.targets.wheel]
|
|
67
|
+
packages = ["src/sqlalchemydqlite"]
|
|
68
|
+
|
|
69
|
+
# Hatchling's default file-selection would already ship ``py.typed`` via
|
|
70
|
+
# ``packages``, but declaring it explicitly is the documented PEP 561
|
|
71
|
+
# pattern and makes the intent obvious to anyone auditing the wheel.
|
|
72
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
73
|
+
"src/sqlalchemydqlite/py.typed" = "sqlalchemydqlite/py.typed"
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
testpaths = ["tests"]
|
|
77
|
+
pythonpath = ["src"]
|
|
78
|
+
asyncio_mode = "auto"
|
|
79
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
80
|
+
# Exclude the SA compliance suite from the default pytest run.
|
|
81
|
+
# tests/compliance/ loads SA's pytest plugin via its conftest, which
|
|
82
|
+
# replaces pytest's normal collection model and requires CLI options
|
|
83
|
+
# (--dburi etc.) the project-wide test runner does not pass. Run the
|
|
84
|
+
# suite explicitly with ``pytest tests/compliance/``.
|
|
85
|
+
addopts = ["--ignore=tests/compliance"]
|
|
86
|
+
filterwarnings = [
|
|
87
|
+
# Promote deprecations to errors so the suite catches stdlib
|
|
88
|
+
# deprecation surface under the CPython version the workspace
|
|
89
|
+
# already runs CI on (3.13/3.14). Whitelist genuine third-party
|
|
90
|
+
# noise with explicit ``ignore:<pattern>:DeprecationWarning:<module>``
|
|
91
|
+
# entries below, with a comment explaining the upstream tracker.
|
|
92
|
+
"error::DeprecationWarning",
|
|
93
|
+
"error::PendingDeprecationWarning",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[tool.mypy]
|
|
97
|
+
strict = true
|
|
98
|
+
python_version = "3.13"
|
|
99
|
+
# Tests live under ``tests/`` with no ``__init__.py``; mypy needs
|
|
100
|
+
# ``explicit_package_bases`` (or namespace packages) to avoid the
|
|
101
|
+
# duplicate-conftest module-name collision when scanning sibling
|
|
102
|
+
# integration/ subdirectories. ``mypy_path`` mirrors the
|
|
103
|
+
# ``pythonpath = ["src"]`` declared for pytest so the source files
|
|
104
|
+
# resolve under the package name (``sqlalchemydqlite``), not the
|
|
105
|
+
# layout path (``src.sqlalchemydqlite``).
|
|
106
|
+
explicit_package_bases = true
|
|
107
|
+
mypy_path = "src"
|
|
108
|
+
|
|
109
|
+
# Tests use a few patterns that mypy's strict mode flags but are
|
|
110
|
+
# correct at runtime. Disabling these codes only inside ``tests.*``
|
|
111
|
+
# keeps strict typing on production code while letting the test suite
|
|
112
|
+
# use idiomatic patterns without per-line ``# type: ignore`` noise:
|
|
113
|
+
#
|
|
114
|
+
# * ``method-assign`` — monkey-patching bound methods
|
|
115
|
+
# (``conn.execute = fake_execute``) is the standard way to inject a
|
|
116
|
+
# fake without subclassing.
|
|
117
|
+
# * ``no-untyped-def`` / ``no-untyped-call`` — pytest fixtures and
|
|
118
|
+
# small helpers are conventionally written without type annotations.
|
|
119
|
+
# * ``comparison-overlap`` — ``assert SomeIntEnum.FOO == 0`` is the
|
|
120
|
+
# canonical "this enum matches its int value" pin against a
|
|
121
|
+
# reference table; mypy's literal-type narrowing incorrectly flags
|
|
122
|
+
# it as non-overlapping even though IntEnum's ``__eq__`` works.
|
|
123
|
+
[[tool.mypy.overrides]]
|
|
124
|
+
module = "tests.*"
|
|
125
|
+
disable_error_code = ["method-assign", "no-untyped-def", "no-untyped-call", "comparison-overlap"]
|
|
126
|
+
|
|
127
|
+
[tool.ruff]
|
|
128
|
+
target-version = "py313"
|
|
129
|
+
line-length = 100
|
|
130
|
+
src = ["src", "tests"]
|
|
131
|
+
|
|
132
|
+
[tool.ruff.lint]
|
|
133
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
134
|
+
|
|
135
|
+
[tool.ruff.lint.isort]
|
|
136
|
+
known-first-party = ["sqlalchemydqlite", "dqlitedbapi", "dqliteclient", "dqlitewire"]
|
|
137
|
+
|
|
138
|
+
[tool.ruff.format]
|
|
139
|
+
quote-style = "double"
|
|
140
|
+
indent-style = "space"
|
|
141
|
+
docstring-code-format = true
|