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.
Files changed (270) hide show
  1. sqlalchemy_dqlite-0.2.0/DEVELOPMENT.md +199 -0
  2. sqlalchemy_dqlite-0.2.0/PKG-INFO +185 -0
  3. sqlalchemy_dqlite-0.2.0/README.md +149 -0
  4. sqlalchemy_dqlite-0.2.0/pyproject.toml +141 -0
  5. sqlalchemy_dqlite-0.2.0/scripts/check-commit-msg.sh +140 -0
  6. sqlalchemy_dqlite-0.2.0/setup.cfg +32 -0
  7. sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/__init__.py +75 -0
  8. sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/aio.py +2267 -0
  9. sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/base.py +2992 -0
  10. sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/provision.py +518 -0
  11. sqlalchemy_dqlite-0.2.0/src/sqlalchemydqlite/requirements.py +484 -0
  12. sqlalchemy_dqlite-0.2.0/tests/compliance/conftest.py +107 -0
  13. sqlalchemy_dqlite-0.2.0/tests/compliance/test_suite.py +19 -0
  14. sqlalchemy_dqlite-0.2.0/tests/conftest.py +80 -0
  15. sqlalchemy_dqlite-0.2.0/tests/integration/test_async_cancel_during_commit_invalidates_slot.py +187 -0
  16. sqlalchemy_dqlite-0.2.0/tests/integration/test_async_cancel_inside_engine_begin.py +80 -0
  17. sqlalchemy_dqlite-0.2.0/tests/integration/test_async_concurrent_sessions.py +135 -0
  18. sqlalchemy_dqlite-0.2.0/tests/integration/test_async_executemany.py +98 -0
  19. sqlalchemy_dqlite-0.2.0/tests/integration/test_async_savepoint_cancellation_invalidates_slot.py +119 -0
  20. sqlalchemy_dqlite-0.2.0/tests/integration/test_bulk_dml_returning.py +120 -0
  21. sqlalchemy_dqlite-0.2.0/tests/integration/test_date_tz_drop.py +62 -0
  22. sqlalchemy_dqlite-0.2.0/tests/integration/test_dial_func_via_connect_args_reaches_dbapi.py +77 -0
  23. sqlalchemy_dqlite-0.2.0/tests/integration/test_engine_begin_is_atomic.py +291 -0
  24. sqlalchemy_dqlite-0.2.0/tests/integration/test_engine_events_fire.py +90 -0
  25. sqlalchemy_dqlite-0.2.0/tests/integration/test_inspect_async_and_close_parity.py +157 -0
  26. sqlalchemy_dqlite-0.2.0/tests/integration/test_isolation_level_pin.py +56 -0
  27. sqlalchemy_dqlite-0.2.0/tests/integration/test_json_column.py +152 -0
  28. sqlalchemy_dqlite-0.2.0/tests/integration/test_native_boolean.py +74 -0
  29. {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/integration/test_orm_operations.py +74 -96
  30. sqlalchemy_dqlite-0.2.0/tests/integration/test_pool_pre_ping_leader_flip.py +110 -0
  31. sqlalchemy_dqlite-0.2.0/tests/integration/test_pool_recycle_and_dispose_fan_out.py +176 -0
  32. sqlalchemy_dqlite-0.2.0/tests/integration/test_pool_reset_on_return.py +66 -0
  33. sqlalchemy_dqlite-0.2.0/tests/integration/test_reflect_column_types.py +83 -0
  34. sqlalchemy_dqlite-0.2.0/tests/integration/test_savepoint_basic.py +173 -0
  35. sqlalchemy_dqlite-0.2.0/tests/integration/test_savepoint_orm_session_begin_nested.py +189 -0
  36. sqlalchemy_dqlite-0.2.0/tests/integration/test_savepoint_quoted_identifiers.py +152 -0
  37. sqlalchemy_dqlite-0.2.0/tests/integration/test_time_and_reflection.py +125 -0
  38. sqlalchemy_dqlite-0.2.0/tests/integration/test_upsert.py +113 -0
  39. sqlalchemy_dqlite-0.2.0/tests/test_2pc_not_supported_error.py +86 -0
  40. sqlalchemy_dqlite-0.2.0/tests/test_adapter_arraysize_default_and_semantics.py +64 -0
  41. sqlalchemy_dqlite-0.2.0/tests/test_adapter_arraysize_validation.py +52 -0
  42. sqlalchemy_dqlite-0.2.0/tests/test_adapter_cursor_iterator_protocol.py +52 -0
  43. sqlalchemy_dqlite-0.2.0/tests/test_adapter_fetchmany_negative.py +57 -0
  44. sqlalchemy_dqlite-0.2.0/tests/test_adapter_handle_exception_hook.py +49 -0
  45. sqlalchemy_dqlite-0.2.0/tests/test_adapter_pickle_guards.py +60 -0
  46. sqlalchemy_dqlite-0.2.0/tests/test_adapter_slots.py +19 -0
  47. sqlalchemy_dqlite-0.2.0/tests/test_adapter_stale_rows.py +124 -0
  48. sqlalchemy_dqlite-0.2.0/tests/test_aio_adapted_connection_public_paths.py +157 -0
  49. sqlalchemy_dqlite-0.2.0/tests/test_aio_async_creator_fn.py +169 -0
  50. sqlalchemy_dqlite-0.2.0/tests/test_aio_async_creator_fn_shape_check.py +75 -0
  51. sqlalchemy_dqlite-0.2.0/tests/test_aio_await_staticmethod.py +32 -0
  52. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_final_close_suppression.py +157 -0
  53. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_loop_mismatch_remap.py +48 -0
  54. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_releases_inner_connection_pin.py +135 -0
  55. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_rollback_log.py +229 -0
  56. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_rollback_loop_already_running_remap.py +66 -0
  57. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_rollback_runtime_error_case_insensitive.py +64 -0
  58. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_suppress_and_soft_close_noop.py +117 -0
  59. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_cancellederror_force_close.py +97 -0
  60. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_in_greenlet_preflight.py +63 -0
  61. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_outside_greenlet.py +173 -0
  62. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_proxy_swap_arms.py +277 -0
  63. sqlalchemy_dqlite-0.2.0/tests/test_aio_close_terminate_runtime_error_post_await.py +129 -0
  64. sqlalchemy_dqlite-0.2.0/tests/test_aio_connect_construction_in_try_frame.py +108 -0
  65. sqlalchemy_dqlite-0.2.0/tests/test_aio_connect_loop_state_remap.py +133 -0
  66. sqlalchemy_dqlite-0.2.0/tests/test_aio_cursor_class_hooks.py +103 -0
  67. sqlalchemy_dqlite-0.2.0/tests/test_aio_cursor_closed_before_server_side_precedence.py +63 -0
  68. sqlalchemy_dqlite-0.2.0/tests/test_aio_cursor_time_error_remapped.py +80 -0
  69. sqlalchemy_dqlite-0.2.0/tests/test_aio_dialect_do_close_releases_inner_strong_ref.py +99 -0
  70. sqlalchemy_dqlite-0.2.0/tests/test_aio_do_close_fallback_force_close_transport.py +109 -0
  71. sqlalchemy_dqlite-0.2.0/tests/test_aio_do_ping_cursor_construct_exception_arms.py +79 -0
  72. sqlalchemy_dqlite-0.2.0/tests/test_aio_do_ping_override.py +118 -0
  73. sqlalchemy_dqlite-0.2.0/tests/test_aio_do_ping_runtimeerror_returns_false.py +44 -0
  74. sqlalchemy_dqlite-0.2.0/tests/test_aio_do_terminate_suppresses_exception.py +66 -0
  75. sqlalchemy_dqlite-0.2.0/tests/test_aio_force_close_transport_cancelled_error.py +45 -0
  76. sqlalchemy_dqlite-0.2.0/tests/test_aio_force_close_transport_dead_proxy.py +104 -0
  77. sqlalchemy_dqlite-0.2.0/tests/test_aio_force_close_transport_docstring_pins.py +61 -0
  78. sqlalchemy_dqlite-0.2.0/tests/test_aio_handle_exception_baseexceptiongroup_cancel_split.py +221 -0
  79. sqlalchemy_dqlite-0.2.0/tests/test_aio_handle_exception_call_sites.py +114 -0
  80. sqlalchemy_dqlite-0.2.0/tests/test_aio_handle_exception_delegates_to_remap_helper.py +199 -0
  81. sqlalchemy_dqlite-0.2.0/tests/test_aio_is_disconnect_closed_proxy.py +70 -0
  82. sqlalchemy_dqlite-0.2.0/tests/test_aio_no_in_function_imports.py +42 -0
  83. sqlalchemy_dqlite-0.2.0/tests/test_aio_post_close_proxy_guards.py +166 -0
  84. sqlalchemy_dqlite-0.2.0/tests/test_aio_terminate.py +229 -0
  85. sqlalchemy_dqlite-0.2.0/tests/test_aio_terminate_close_idempotence.py +71 -0
  86. sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_cursor_closed_check.py +74 -0
  87. sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_execute_handle_exception.py +90 -0
  88. sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_execute_suppress_cancellederror.py +55 -0
  89. sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_init_sa_reference_signature.py +59 -0
  90. sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_connection_init_three_call_shapes.py +64 -0
  91. sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_cursor_doc.py +69 -0
  92. sqlalchemy_dqlite-0.2.0/tests/test_async_adapted_cursor_sync_ctxmgr.py +66 -0
  93. sqlalchemy_dqlite-0.2.0/tests/test_async_adapter.py +664 -0
  94. sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_autocommit_property.py +43 -0
  95. sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_closed_checks.py +68 -0
  96. sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_lastrowid_sticky.py +122 -0
  97. sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_op_lock_serialisation.py +102 -0
  98. sqlalchemy_dqlite-0.2.0/tests/test_async_adapter_terminate_handled_exceptions.py +36 -0
  99. sqlalchemy_dqlite-0.2.0/tests/test_async_commit_cancel_invalidates_slot.py +126 -0
  100. sqlalchemy_dqlite-0.2.0/tests/test_async_creator_fn_noncallable_rejected.py +31 -0
  101. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_after_connection_close.py +51 -0
  102. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_arraysize_accepts_zero_and_negative.py +73 -0
  103. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_arraysize_propagates_to_underlying.py +94 -0
  104. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_closed_guard.py +67 -0
  105. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_closed_guards.py +58 -0
  106. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_execute_close_suppress_behavioural.py +135 -0
  107. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_executemany_listifies_iterable.py +84 -0
  108. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_handle_exception.py +99 -0
  109. sqlalchemy_dqlite-0.2.0/tests/test_async_cursor_sync_only_context_manager.py +51 -0
  110. sqlalchemy_dqlite-0.2.0/tests/test_async_dialect_isolation_set.py +60 -0
  111. sqlalchemy_dqlite-0.2.0/tests/test_async_executemany_leader_flip_state_reset.py +121 -0
  112. sqlalchemy_dqlite-0.2.0/tests/test_async_handle_exception_case_insensitive.py +66 -0
  113. sqlalchemy_dqlite-0.2.0/tests/test_async_handle_exception_cause_walk.py +97 -0
  114. sqlalchemy_dqlite-0.2.0/tests/test_async_isolation_level.py +44 -0
  115. sqlalchemy_dqlite-0.2.0/tests/test_async_ping_cancel_cleanup.py +275 -0
  116. sqlalchemy_dqlite-0.2.0/tests/test_async_ping_close_arm_narrow_vs_programmer_bug.py +56 -0
  117. sqlalchemy_dqlite-0.2.0/tests/test_async_ping_single_execute_no_fetchone.py +37 -0
  118. sqlalchemy_dqlite-0.2.0/tests/test_async_ping_translates_dead_proxy_to_interface_error.py +73 -0
  119. sqlalchemy_dqlite-0.2.0/tests/test_audit_2026_05_round2_sa_changes.py +127 -0
  120. sqlalchemy_dqlite-0.2.0/tests/test_audit_2026_05_sa_changes.py +184 -0
  121. sqlalchemy_dqlite-0.2.0/tests/test_audit_2026_05_sa_coverage.py +220 -0
  122. sqlalchemy_dqlite-0.2.0/tests/test_bind_side_cross_type_rejection.py +93 -0
  123. sqlalchemy_dqlite-0.2.0/tests/test_close_runs_close_after_rollback_raise.py +95 -0
  124. sqlalchemy_dqlite-0.2.0/tests/test_collated_coverage_gaps.py +52 -0
  125. sqlalchemy_dqlite-0.2.0/tests/test_compiler_regexp_match.py +71 -0
  126. sqlalchemy_dqlite-0.2.0/tests/test_compliance_conftest_staleness_pin.py +129 -0
  127. sqlalchemy_dqlite-0.2.0/tests/test_connect_args_kwarg_validation.py +84 -0
  128. sqlalchemy_dqlite-0.2.0/tests/test_connect_args_value_validators.py +247 -0
  129. sqlalchemy_dqlite-0.2.0/tests/test_cross_package_version_parity.py +137 -0
  130. sqlalchemy_dqlite-0.2.0/tests/test_cursor_close_narrow_suppress.py +134 -0
  131. sqlalchemy_dqlite-0.2.0/tests/test_datetime_bind_widens_date.py +47 -0
  132. sqlalchemy_dqlite-0.2.0/tests/test_datetime_date_parse_errors.py +64 -0
  133. sqlalchemy_dqlite-0.2.0/tests/test_datetime_time_cross_type_payload.py +131 -0
  134. sqlalchemy_dqlite-0.2.0/tests/test_datetime_tz_conversion.py +59 -0
  135. sqlalchemy_dqlite-0.2.0/tests/test_ddl_kwarg_prefix_documented.py +66 -0
  136. sqlalchemy_dqlite-0.2.0/tests/test_description_alias_shared.py +32 -0
  137. sqlalchemy_dqlite-0.2.0/tests/test_detect_autocommit_setting.py +102 -0
  138. sqlalchemy_dqlite-0.2.0/tests/test_dial_attempt_timeout_url_and_connect_args.py +75 -0
  139. sqlalchemy_dqlite-0.2.0/tests/test_dialect.py +851 -0
  140. sqlalchemy_dqlite-0.2.0/tests/test_dialect_dialect_config.py +524 -0
  141. sqlalchemy_dqlite-0.2.0/tests/test_dialect_docstring_pins.py +81 -0
  142. sqlalchemy_dqlite-0.2.0/tests/test_dialect_final_annotated.py +40 -0
  143. sqlalchemy_dqlite-0.2.0/tests/test_dialect_inherits_pysqlite.py +36 -0
  144. sqlalchemy_dqlite-0.2.0/tests/test_dialect_init_eager_autocommit_rejection.py +50 -0
  145. sqlalchemy_dqlite-0.2.0/tests/test_dialect_init_preserves_pins.py +129 -0
  146. sqlalchemy_dqlite-0.2.0/tests/test_dialect_ipv6_url.py +88 -0
  147. sqlalchemy_dqlite-0.2.0/tests/test_dialect_set_isolation_level_annotation.py +35 -0
  148. sqlalchemy_dqlite-0.2.0/tests/test_dialect_sqlite_incompatibility_flags_pinned.py +41 -0
  149. sqlalchemy_dqlite-0.2.0/tests/test_dialect_url_credentials.py +53 -0
  150. sqlalchemy_dqlite-0.2.0/tests/test_disconnect_substring_producer_emission.py +199 -0
  151. sqlalchemy_dqlite-0.2.0/tests/test_do_begin_cancel.py +49 -0
  152. sqlalchemy_dqlite-0.2.0/tests/test_do_begin_emits_begin.py +199 -0
  153. sqlalchemy_dqlite-0.2.0/tests/test_do_begin_savepoint_asymmetry_documented.py +58 -0
  154. sqlalchemy_dqlite-0.2.0/tests/test_do_close_calls_dbapi_close_directly.py +137 -0
  155. sqlalchemy_dqlite-0.2.0/tests/test_do_close_fallback_suppress_arm.py +131 -0
  156. sqlalchemy_dqlite-0.2.0/tests/test_do_close_fallback_uses_sync_transport_teardown.py +94 -0
  157. sqlalchemy_dqlite-0.2.0/tests/test_do_close_first_arm_absorbs_runtime_error.py +89 -0
  158. sqlalchemy_dqlite-0.2.0/tests/test_do_executemany_local_override_pin.py +52 -0
  159. sqlalchemy_dqlite-0.2.0/tests/test_do_ping_catches_programming_error.py +50 -0
  160. sqlalchemy_dqlite-0.2.0/tests/test_do_ping_close_narrow.py +135 -0
  161. sqlalchemy_dqlite-0.2.0/tests/test_do_ping_cursor_call_disconnect.py +92 -0
  162. sqlalchemy_dqlite-0.2.0/tests/test_do_ping_database_error.py +58 -0
  163. sqlalchemy_dqlite-0.2.0/tests/test_do_ping_dialect_specific_select_one.py +63 -0
  164. sqlalchemy_dqlite-0.2.0/tests/test_do_ping_interface_error_with_code.py +43 -0
  165. sqlalchemy_dqlite-0.2.0/tests/test_do_ping_non_disconnect_databaseerror_reraise.py +45 -0
  166. sqlalchemy_dqlite-0.2.0/tests/test_do_terminate_narrow_catch_propagates_refactor_break.py +65 -0
  167. sqlalchemy_dqlite-0.2.0/tests/test_dqlitetime_bind_processor_returns_none.py +35 -0
  168. sqlalchemy_dqlite-0.2.0/tests/test_dqlitetime_cross_axis_narrow_with_timezone_true.py +50 -0
  169. sqlalchemy_dqlite-0.2.0/tests/test_drift_defence_pins.py +90 -0
  170. sqlalchemy_dqlite-0.2.0/tests/test_drop_user_tables_log_record_bounded.py +52 -0
  171. sqlalchemy_dqlite-0.2.0/tests/test_floor_compile_time_gate.py +113 -0
  172. sqlalchemy_dqlite-0.2.0/tests/test_handle_exception_must_raise.py +57 -0
  173. sqlalchemy_dqlite-0.2.0/tests/test_handle_exception_remap.py +79 -0
  174. sqlalchemy_dqlite-0.2.0/tests/test_handle_exception_two_pass_recovery_after_mixed_cancel_group.py +74 -0
  175. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_bare_databaseerror_type_identity.py +115 -0
  176. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_bare_oserror_walked_not_early_return.py +70 -0
  177. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_case_insensitive.py +35 -0
  178. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_cause_walk.py +121 -0
  179. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_closed_handle_with_id_suffix.py +99 -0
  180. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_cluster_error.py +108 -0
  181. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_database_error_substring.py +102 -0
  182. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_event_loop_already_running_prefix.py +53 -0
  183. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_event_loop_closed.py +49 -0
  184. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_event_loop_mismatch_substring.py +78 -0
  185. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_exception_group_and_substring_walk.py +82 -0
  186. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_interface_error_cause_walk.py +104 -0
  187. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_leader_lost_substring_case_insensitive.py +61 -0
  188. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_notfound_lookup_db.py +132 -0
  189. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_oe_code_restriction.py +37 -0
  190. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_protocol_error_arm.py +69 -0
  191. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_raft_collapse.py +80 -0
  192. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_raw_message.py +80 -0
  193. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_recognises_connection_invalidated_interfaceerror.py +96 -0
  194. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_recognises_cross_loop_interfaceerror.py +114 -0
  195. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_recognises_used_after_fork.py +63 -0
  196. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_substring_visible_within_dbapi_4kib_cap.py +56 -0
  197. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_untracked_savepoint_negative.py +84 -0
  198. sqlalchemy_dqlite-0.2.0/tests/test_is_disconnect_walks_cause_oserror.py +70 -0
  199. sqlalchemy_dqlite-0.2.0/tests/test_isolation_level_in_connect_args_directional_message.py +37 -0
  200. sqlalchemy_dqlite-0.2.0/tests/test_isolation_lookup_vs_values_list_divergence_documented.py +95 -0
  201. sqlalchemy_dqlite-0.2.0/tests/test_leader_error_codes_identity.py +16 -0
  202. sqlalchemy_dqlite-0.2.0/tests/test_log_safe_peer_helper.py +70 -0
  203. sqlalchemy_dqlite-0.2.0/tests/test_log_safe_peer_routing.py +95 -0
  204. sqlalchemy_dqlite-0.2.0/tests/test_log_safe_peer_uses_sanitize_for_log.py +52 -0
  205. sqlalchemy_dqlite-0.2.0/tests/test_max_message_size_url_and_connect_args.py +146 -0
  206. sqlalchemy_dqlite-0.2.0/tests/test_max_total_rows_warning_does_not_advertise_disabled_token.py +79 -0
  207. sqlalchemy_dqlite-0.2.0/tests/test_phantom_returning_flag_does_not_exist_in_sa.py +42 -0
  208. sqlalchemy_dqlite-0.2.0/tests/test_provision_create_drop_db_log_sanitises_ident.py +58 -0
  209. sqlalchemy_dqlite-0.2.0/tests/test_provision_drop_user_tables_and_reap.py +208 -0
  210. sqlalchemy_dqlite-0.2.0/tests/test_provision_drop_user_tables_log_sanitized.py +78 -0
  211. sqlalchemy_dqlite-0.2.0/tests/test_provision_drop_user_tables_quote_escaping.py +83 -0
  212. sqlalchemy_dqlite-0.2.0/tests/test_provision_format_url_ident_sanitisation.py +172 -0
  213. sqlalchemy_dqlite-0.2.0/tests/test_provision_generate_driver_url.py +53 -0
  214. sqlalchemy_dqlite-0.2.0/tests/test_provision_reap_dbs_info_log_credential_mask_and_sanitize.py +86 -0
  215. sqlalchemy_dqlite-0.2.0/tests/test_provision_reap_dbs_log_sanitized.py +54 -0
  216. sqlalchemy_dqlite-0.2.0/tests/test_provision_session_token_shape.py +72 -0
  217. sqlalchemy_dqlite-0.2.0/tests/test_provision_upsert.py +42 -0
  218. sqlalchemy_dqlite-0.2.0/tests/test_public_api.py +122 -0
  219. sqlalchemy_dqlite-0.2.0/tests/test_raft_collapse_markers_upstream_provenance.py +55 -0
  220. sqlalchemy_dqlite-0.2.0/tests/test_remap_loop_state_chains_from_matched_hop.py +104 -0
  221. sqlalchemy_dqlite-0.2.0/tests/test_remap_loop_state_helper_docstring_pin.py +33 -0
  222. sqlalchemy_dqlite-0.2.0/tests/test_requirements.py +146 -0
  223. sqlalchemy_dqlite-0.2.0/tests/test_requirements_closed_marker_present.py +54 -0
  224. sqlalchemy_dqlite-0.2.0/tests/test_requirements_recently_opened.py +46 -0
  225. sqlalchemy_dqlite-0.2.0/tests/test_reset_isolation_level_is_local_noop.py +58 -0
  226. sqlalchemy_dqlite-0.2.0/tests/test_result_processor_edge_branches.py +58 -0
  227. sqlalchemy_dqlite-0.2.0/tests/test_result_processor_log_payload_sanitized.py +101 -0
  228. sqlalchemy_dqlite-0.2.0/tests/test_result_processor_log_truncates_server_text.py +73 -0
  229. sqlalchemy_dqlite-0.2.0/tests/test_result_processor_warning_one_shot.py +89 -0
  230. sqlalchemy_dqlite-0.2.0/tests/test_savepoint_leader_flip_loud_failure.py +138 -0
  231. sqlalchemy_dqlite-0.2.0/tests/test_set_isolation_level_case_insensitive.py +77 -0
  232. sqlalchemy_dqlite-0.2.0/tests/test_stream_results_documented_eager_buffer.py +116 -0
  233. sqlalchemy_dqlite-0.2.0/tests/test_submodule_all.py +34 -0
  234. sqlalchemy_dqlite-0.2.0/tests/test_sync_connect_creator_fn_shape.py +53 -0
  235. sqlalchemy_dqlite-0.2.0/tests/test_sync_do_rollback_transport_fault_classified.py +98 -0
  236. sqlalchemy_dqlite-0.2.0/tests/test_sync_do_terminate.py +67 -0
  237. sqlalchemy_dqlite-0.2.0/tests/test_sync_do_terminate_log_includes_peer_and_id.py +34 -0
  238. sqlalchemy_dqlite-0.2.0/tests/test_time_colspec.py +44 -0
  239. sqlalchemy_dqlite-0.2.0/tests/test_time_tz_conversion.py +58 -0
  240. sqlalchemy_dqlite-0.2.0/tests/test_top_level_logger_has_null_handler.py +16 -0
  241. sqlalchemy_dqlite-0.2.0/tests/test_transitive_wire_import.py +43 -0
  242. sqlalchemy_dqlite-0.2.0/tests/test_transport_class_exceptions_consolidation.py +249 -0
  243. sqlalchemy_dqlite-0.2.0/tests/test_url_int_upper_boundary_off_by_one.py +87 -0
  244. sqlalchemy_dqlite-0.2.0/tests/test_url_max_rows_none_token.py +95 -0
  245. sqlalchemy_dqlite-0.2.0/tests/test_url_pre_validation.py +77 -0
  246. sqlalchemy_dqlite-0.2.0/tests/test_url_timeout_validators_share_ssot.py +98 -0
  247. sqlalchemy_dqlite-0.2.0/tests/test_version.py +18 -0
  248. sqlalchemy_dqlite-0.2.0/tests/test_walk_cause_chain_caps_pathological_group_size.py +83 -0
  249. sqlalchemy_dqlite-0.2.0/tests/test_walk_cause_chain_cycle_at_depth.py +33 -0
  250. sqlalchemy_dqlite-0.2.0/tests/test_walk_cause_chain_depth_budget.py +82 -0
  251. sqlalchemy_dqlite-0.2.0/tests/test_wire_decode_failed_prefix_constant_used_by_dialect.py +25 -0
  252. sqlalchemy_dqlite-0.2.0/uv.lock +455 -0
  253. sqlalchemy_dqlite-0.1.2/DEVELOPMENT.md +0 -90
  254. sqlalchemy_dqlite-0.1.2/PKG-INFO +0 -76
  255. sqlalchemy_dqlite-0.1.2/README.md +0 -44
  256. sqlalchemy_dqlite-0.1.2/pyproject.toml +0 -67
  257. sqlalchemy_dqlite-0.1.2/src/sqlalchemydqlite/__init__.py +0 -7
  258. sqlalchemy_dqlite-0.1.2/src/sqlalchemydqlite/aio.py +0 -199
  259. sqlalchemy_dqlite-0.1.2/src/sqlalchemydqlite/base.py +0 -122
  260. sqlalchemy_dqlite-0.1.2/src/sqlalchemydqlite/requirements.py +0 -45
  261. sqlalchemy_dqlite-0.1.2/tests/conftest.py +0 -15
  262. sqlalchemy_dqlite-0.1.2/tests/test_dialect.py +0 -94
  263. {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/.github/workflows/publish-to-pypi.yml +0 -0
  264. {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/.gitignore +0 -0
  265. {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/LICENSE.md +0 -0
  266. {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/src/sqlalchemydqlite/py.typed +0 -0
  267. {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/__init__.py +0 -0
  268. {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/integration/__init__.py +0 -0
  269. {sqlalchemy_dqlite-0.1.2 → sqlalchemy_dqlite-0.2.0}/tests/integration/conftest.py +0 -0
  270. {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