execsql2 2.22.7__tar.gz → 2.22.9__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 (379) hide show
  1. {execsql2-2.22.7 → execsql2-2.22.9}/CHANGELOG.md +18 -0
  2. {execsql2-2.22.7 → execsql2-2.22.9}/PKG-INFO +2 -2
  3. {execsql2-2.22.7 → execsql2-2.22.9}/README.md +1 -1
  4. {execsql2-2.22.7 → execsql2-2.22.9}/docs/about/divergence.md +54 -51
  5. {execsql2-2.22.7 → execsql2-2.22.9}/docs/dev/architecture.md +14 -0
  6. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/formatter.md +5 -1
  7. {execsql2-2.22.7 → execsql2-2.22.9}/docs/reference/configuration.md +1 -1
  8. {execsql2-2.22.7 → execsql2-2.22.9}/pyproject.toml +6 -2
  9. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/data/execsql.conf.template +1 -0
  10. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/format.py +164 -33
  11. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/mail.py +9 -5
  12. execsql2-2.22.9/tests/data/formatter_shapes/comment_anchoring.sql +55 -0
  13. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_format.py +142 -3
  14. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_mail.py +190 -23
  15. {execsql2-2.22.7 → execsql2-2.22.9}/uv.lock +1 -1
  16. {execsql2-2.22.7 → execsql2-2.22.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {execsql2-2.22.7 → execsql2-2.22.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {execsql2-2.22.7 → execsql2-2.22.9}/.github/dependabot.yml +0 -0
  19. {execsql2-2.22.7 → execsql2-2.22.9}/.github/workflows/ci-cd.yml +0 -0
  20. {execsql2-2.22.7 → execsql2-2.22.9}/.gitignore +0 -0
  21. {execsql2-2.22.7 → execsql2-2.22.9}/.pre-commit-config.yaml +0 -0
  22. {execsql2-2.22.7 → execsql2-2.22.9}/.pre-commit-hooks.yaml +0 -0
  23. {execsql2-2.22.7 → execsql2-2.22.9}/.python-version +0 -0
  24. {execsql2-2.22.7 → execsql2-2.22.9}/.readthedocs.yaml +0 -0
  25. {execsql2-2.22.7 → execsql2-2.22.9}/CONTRIBUTING.md +0 -0
  26. {execsql2-2.22.7 → execsql2-2.22.9}/LICENSE.txt +0 -0
  27. {execsql2-2.22.7 → execsql2-2.22.9}/NOTICE +0 -0
  28. {execsql2-2.22.7 → execsql2-2.22.9}/SECURITY.md +0 -0
  29. {execsql2-2.22.7 → execsql2-2.22.9}/docs/about/contributors.md +0 -0
  30. {execsql2-2.22.7 → execsql2-2.22.9}/docs/about/copyright.md +0 -0
  31. {execsql2-2.22.7 → execsql2-2.22.9}/docs/api/cli.md +0 -0
  32. {execsql2-2.22.7 → execsql2-2.22.9}/docs/api/db.md +0 -0
  33. {execsql2-2.22.7 → execsql2-2.22.9}/docs/api/exporters.md +0 -0
  34. {execsql2-2.22.7 → execsql2-2.22.9}/docs/api/importers.md +0 -0
  35. {execsql2-2.22.7 → execsql2-2.22.9}/docs/api/index.md +0 -0
  36. {execsql2-2.22.7 → execsql2-2.22.9}/docs/api/metacommands.md +0 -0
  37. {execsql2-2.22.7 → execsql2-2.22.9}/docs/dev/adding_db_adapters.md +0 -0
  38. {execsql2-2.22.7 → execsql2-2.22.9}/docs/dev/adding_exporters.md +0 -0
  39. {execsql2-2.22.7 → execsql2-2.22.9}/docs/dev/adding_importers.md +0 -0
  40. {execsql2-2.22.7 → execsql2-2.22.9}/docs/dev/adding_metacommands.md +0 -0
  41. {execsql2-2.22.7 → execsql2-2.22.9}/docs/dev/releasing.md +0 -0
  42. {execsql2-2.22.7 → execsql2-2.22.9}/docs/getting-started/installation.md +0 -0
  43. {execsql2-2.22.7 → execsql2-2.22.9}/docs/getting-started/requirements.md +0 -0
  44. {execsql2-2.22.7 → execsql2-2.22.9}/docs/getting-started/syntax.md +0 -0
  45. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/debugging.md +0 -0
  46. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/documentation.md +0 -0
  47. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/encoding.md +0 -0
  48. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/examples.md +0 -0
  49. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/logging.md +0 -0
  50. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/sql_syntax.md +0 -0
  51. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/usage.md +0 -0
  52. {execsql2-2.22.7 → execsql2-2.22.9}/docs/guides/using_scripts.md +0 -0
  53. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/Compare_planets.png +0 -0
  54. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/actions.png +0 -0
  55. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/actions2.png +0 -0
  56. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/checkboxes.png +0 -0
  57. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/connect.b64 +0 -0
  58. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/connect.png +0 -0
  59. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/create_conf.png +0 -0
  60. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/data_error1_screenshot.jpg +0 -0
  61. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/entry_form.png +0 -0
  62. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/execsql_console.png +0 -0
  63. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/execsql_logo_01.png +0 -0
  64. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/fatals.png +0 -0
  65. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/logo_small.png +0 -0
  66. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/pause_terminal.png +0 -0
  67. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/pause_terminal_sm.b64 +0 -0
  68. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/pause_terminal_sm.png +0 -0
  69. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/prompt_compare.png +0 -0
  70. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/set_build_commands.jpg +0 -0
  71. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/unit_conversions.b64 +0 -0
  72. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/unit_conversions_029.png +0 -0
  73. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/unmatched.png +0 -0
  74. {execsql2-2.22.7 → execsql2-2.22.9}/docs/images/vim_execsql_highlight.png +0 -0
  75. {execsql2-2.22.7 → execsql2-2.22.9}/docs/index.md +0 -0
  76. {execsql2-2.22.7 → execsql2-2.22.9}/docs/reference/metacommands.md +0 -0
  77. {execsql2-2.22.7 → execsql2-2.22.9}/docs/reference/security.md +0 -0
  78. {execsql2-2.22.7 → execsql2-2.22.9}/docs/reference/substitution_vars.md +0 -0
  79. {execsql2-2.22.7 → execsql2-2.22.9}/extras/plugin-template/README.md +0 -0
  80. {execsql2-2.22.7 → execsql2-2.22.9}/extras/plugin-template/pyproject.toml +0 -0
  81. {execsql2-2.22.7 → execsql2-2.22.9}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  82. {execsql2-2.22.7 → execsql2-2.22.9}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  83. {execsql2-2.22.7 → execsql2-2.22.9}/extras/vscode-execsql/README.md +0 -0
  84. {execsql2-2.22.7 → execsql2-2.22.9}/extras/vscode-execsql/package.json +0 -0
  85. {execsql2-2.22.7 → execsql2-2.22.9}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  86. {execsql2-2.22.7 → execsql2-2.22.9}/justfile +0 -0
  87. {execsql2-2.22.7 → execsql2-2.22.9}/scripts/benchmarks/README.md +0 -0
  88. {execsql2-2.22.7 → execsql2-2.22.9}/scripts/benchmarks/results/.gitignore +0 -0
  89. {execsql2-2.22.7 → execsql2-2.22.9}/scripts/benchmarks/run_benchmarks.py +0 -0
  90. {execsql2-2.22.7 → execsql2-2.22.9}/scripts/generate_vscode_grammar.py +0 -0
  91. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/__init__.py +0 -0
  92. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/__main__.py +0 -0
  93. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/api.py +0 -0
  94. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/cli/__init__.py +0 -0
  95. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/cli/dsn.py +0 -0
  96. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/cli/help.py +0 -0
  97. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/cli/lint.py +0 -0
  98. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/cli/run.py +0 -0
  99. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/config.py +0 -0
  100. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/data/__init__.py +0 -0
  101. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/__init__.py +0 -0
  102. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/access.py +0 -0
  103. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/base.py +0 -0
  104. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/dsn.py +0 -0
  105. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/duckdb.py +0 -0
  106. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/factory.py +0 -0
  107. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/firebird.py +0 -0
  108. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/mysql.py +0 -0
  109. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/oracle.py +0 -0
  110. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/postgres.py +0 -0
  111. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/sqlite.py +0 -0
  112. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/db/sqlserver.py +0 -0
  113. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/debug/__init__.py +0 -0
  114. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/debug/repl.py +0 -0
  115. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exceptions.py +0 -0
  116. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/__init__.py +0 -0
  117. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/base.py +0 -0
  118. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/delimited.py +0 -0
  119. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/duckdb.py +0 -0
  120. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/feather.py +0 -0
  121. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/html.py +0 -0
  122. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/json.py +0 -0
  123. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/latex.py +0 -0
  124. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/markdown.py +0 -0
  125. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/ods.py +0 -0
  126. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/parquet.py +0 -0
  127. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/pretty.py +0 -0
  128. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/protocol.py +0 -0
  129. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/raw.py +0 -0
  130. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/sqlite.py +0 -0
  131. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/templates.py +0 -0
  132. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/values.py +0 -0
  133. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/xls.py +0 -0
  134. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/xlsx.py +0 -0
  135. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/xml.py +0 -0
  136. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/yaml.py +0 -0
  137. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/exporters/zip.py +0 -0
  138. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/gui/__init__.py +0 -0
  139. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/gui/base.py +0 -0
  140. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/gui/console.py +0 -0
  141. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/gui/desktop.py +0 -0
  142. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/gui/tui.py +0 -0
  143. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/importers/__init__.py +0 -0
  144. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/importers/base.py +0 -0
  145. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/importers/csv.py +0 -0
  146. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/importers/feather.py +0 -0
  147. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/importers/json.py +0 -0
  148. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/importers/ods.py +0 -0
  149. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/importers/xls.py +0 -0
  150. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/__init__.py +0 -0
  151. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/conditions.py +0 -0
  152. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/connect.py +0 -0
  153. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/control.py +0 -0
  154. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/data.py +0 -0
  155. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/debug.py +0 -0
  156. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/dispatch.py +0 -0
  157. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/io.py +0 -0
  158. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/io_export.py +0 -0
  159. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/io_fileops.py +0 -0
  160. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/io_import.py +0 -0
  161. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/io_write.py +0 -0
  162. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/prompt.py +0 -0
  163. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/script_ext.py +0 -0
  164. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/system.py +0 -0
  165. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/metacommands/upsert.py +0 -0
  166. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/models.py +0 -0
  167. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/parser.py +0 -0
  168. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/plugins.py +0 -0
  169. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/py.typed +0 -0
  170. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/script/__init__.py +0 -0
  171. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/script/ast.py +0 -0
  172. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/script/control.py +0 -0
  173. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/script/engine.py +0 -0
  174. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/script/executor.py +0 -0
  175. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/script/parser.py +0 -0
  176. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/script/variables.py +0 -0
  177. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/state.py +0 -0
  178. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/state.pyi +0 -0
  179. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/types.py +0 -0
  180. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/__init__.py +0 -0
  181. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/auth.py +0 -0
  182. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/crypto.py +0 -0
  183. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/datetime.py +0 -0
  184. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/errors.py +0 -0
  185. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/fileio.py +0 -0
  186. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/gui.py +0 -0
  187. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/numeric.py +0 -0
  188. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/regex.py +0 -0
  189. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/strings.py +0 -0
  190. {execsql2-2.22.7 → execsql2-2.22.9}/src/execsql/utils/timer.py +0 -0
  191. {execsql2-2.22.7 → execsql2-2.22.9}/templates/README.md +0 -0
  192. {execsql2-2.22.7 → execsql2-2.22.9}/templates/config_settings.sqlite +0 -0
  193. {execsql2-2.22.7 → execsql2-2.22.9}/templates/example_config_prompt.sql +0 -0
  194. {execsql2-2.22.7 → execsql2-2.22.9}/templates/make_config_db.sql +0 -0
  195. {execsql2-2.22.7 → execsql2-2.22.9}/templates/md_compare.sql +0 -0
  196. {execsql2-2.22.7 → execsql2-2.22.9}/templates/md_glossary.sql +0 -0
  197. {execsql2-2.22.7 → execsql2-2.22.9}/templates/md_upsert.sql +0 -0
  198. {execsql2-2.22.7 → execsql2-2.22.9}/templates/pg_compare.sql +0 -0
  199. {execsql2-2.22.7 → execsql2-2.22.9}/templates/pg_glossary.sql +0 -0
  200. {execsql2-2.22.7 → execsql2-2.22.9}/templates/pg_upsert.sql +0 -0
  201. {execsql2-2.22.7 → execsql2-2.22.9}/templates/script_template.sql +0 -0
  202. {execsql2-2.22.7 → execsql2-2.22.9}/templates/ss_compare.sql +0 -0
  203. {execsql2-2.22.7 → execsql2-2.22.9}/templates/ss_glossary.sql +0 -0
  204. {execsql2-2.22.7 → execsql2-2.22.9}/templates/ss_upsert.sql +0 -0
  205. {execsql2-2.22.7 → execsql2-2.22.9}/tests/__init__.py +0 -0
  206. {execsql2-2.22.7 → execsql2-2.22.9}/tests/cli/__init__.py +0 -0
  207. {execsql2-2.22.7 → execsql2-2.22.9}/tests/cli/test_cli.py +0 -0
  208. {execsql2-2.22.7 → execsql2-2.22.9}/tests/cli/test_cli_e2e.py +0 -0
  209. {execsql2-2.22.7 → execsql2-2.22.9}/tests/cli/test_cli_run.py +0 -0
  210. {execsql2-2.22.7 → execsql2-2.22.9}/tests/cli/test_lint.py +0 -0
  211. {execsql2-2.22.7 → execsql2-2.22.9}/tests/cli/test_ping.py +0 -0
  212. {execsql2-2.22.7 → execsql2-2.22.9}/tests/cli/test_profile.py +0 -0
  213. {execsql2-2.22.7 → execsql2-2.22.9}/tests/conftest.py +0 -0
  214. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/__init__.py +0 -0
  215. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_access_windows.py +0 -0
  216. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_base.py +0 -0
  217. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_db_adapters_mocked.py +0 -0
  218. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  219. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_dsn.py +0 -0
  220. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_duckdb.py +0 -0
  221. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_factory.py +0 -0
  222. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_mysql_case_folding.py +0 -0
  223. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_mysql_inprocess.py +0 -0
  224. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_postgres.py +0 -0
  225. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_postgres_inprocess.py +0 -0
  226. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_sqlite.py +0 -0
  227. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_sqlite_extra.py +0 -0
  228. {execsql2-2.22.7 → execsql2-2.22.9}/tests/db/test_sqlserver_inprocess.py +0 -0
  229. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/__init__.py +0 -0
  230. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_base.py +0 -0
  231. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_db.py +0 -0
  232. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_delimited.py +0 -0
  233. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_duckdb_exporter.py +0 -0
  234. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_exporters.py +0 -0
  235. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_feather.py +0 -0
  236. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_html_extended.py +0 -0
  237. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_html_latex.py +0 -0
  238. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_json.py +0 -0
  239. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_json_extended.py +0 -0
  240. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_latex_extended.py +0 -0
  241. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_markdown.py +0 -0
  242. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_ods.py +0 -0
  243. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_ods_export.py +0 -0
  244. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_parquet.py +0 -0
  245. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_pretty_extended.py +0 -0
  246. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_raw_extended.py +0 -0
  247. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_sqlite_exporter.py +0 -0
  248. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_templates.py +0 -0
  249. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_templates_extended.py +0 -0
  250. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_values_extended.py +0 -0
  251. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_xls_xlsx.py +0 -0
  252. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_xlsx.py +0 -0
  253. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_xml.py +0 -0
  254. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_yaml.py +0 -0
  255. {execsql2-2.22.7 → execsql2-2.22.9}/tests/exporters/test_zip.py +0 -0
  256. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/__init__.py +0 -0
  257. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_backends.py +0 -0
  258. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_backends_extended.py +0 -0
  259. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_compare_stats.py +0 -0
  260. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_compute_row_diffs.py +0 -0
  261. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_desktop_dialogs.py +0 -0
  262. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_format_table.py +0 -0
  263. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_tui_pilot.py +0 -0
  264. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_tui_pilot_complex.py +0 -0
  265. {execsql2-2.22.7 → execsql2-2.22.9}/tests/gui/test_utils_gui_extended.py +0 -0
  266. {execsql2-2.22.7 → execsql2-2.22.9}/tests/importers/__init__.py +0 -0
  267. {execsql2-2.22.7 → execsql2-2.22.9}/tests/importers/test_base_extended.py +0 -0
  268. {execsql2-2.22.7 → execsql2-2.22.9}/tests/importers/test_csv_edge_cases.py +0 -0
  269. {execsql2-2.22.7 → execsql2-2.22.9}/tests/importers/test_csv_importer.py +0 -0
  270. {execsql2-2.22.7 → execsql2-2.22.9}/tests/importers/test_feather_importer.py +0 -0
  271. {execsql2-2.22.7 → execsql2-2.22.9}/tests/importers/test_json_importer.py +0 -0
  272. {execsql2-2.22.7 → execsql2-2.22.9}/tests/importers/test_ods_importer.py +0 -0
  273. {execsql2-2.22.7 → execsql2-2.22.9}/tests/importers/test_xls_importer.py +0 -0
  274. {execsql2-2.22.7 → execsql2-2.22.9}/tests/integration/__init__.py +0 -0
  275. {execsql2-2.22.7 → execsql2-2.22.9}/tests/integration/conftest.py +0 -0
  276. {execsql2-2.22.7 → execsql2-2.22.9}/tests/integration/test_dsn.py +0 -0
  277. {execsql2-2.22.7 → execsql2-2.22.9}/tests/integration/test_duckdb.py +0 -0
  278. {execsql2-2.22.7 → execsql2-2.22.9}/tests/integration/test_mysql.py +0 -0
  279. {execsql2-2.22.7 → execsql2-2.22.9}/tests/integration/test_postgres.py +0 -0
  280. {execsql2-2.22.7 → execsql2-2.22.9}/tests/integration/test_sqlite.py +0 -0
  281. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/__init__.py +0 -0
  282. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_assert.py +0 -0
  283. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_breakpoint.py +0 -0
  284. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_conditions_extra.py +0 -0
  285. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_connect.py +0 -0
  286. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_io_export.py +0 -0
  287. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_io_import.py +0 -0
  288. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands.py +0 -0
  289. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_connect.py +0 -0
  290. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_data.py +0 -0
  291. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_extended.py +0 -0
  292. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  293. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_io.py +0 -0
  294. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  295. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  296. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_system.py +0 -0
  297. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  298. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_pg_upsert.py +0 -0
  299. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_prompt.py +0 -0
  300. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_row_count.py +0 -0
  301. {execsql2-2.22.7 → execsql2-2.22.9}/tests/metacommands/test_show_scripts.py +0 -0
  302. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/__init__.py +0 -0
  303. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  304. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  305. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  306. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/audit_smoke.sql +0 -0
  307. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/config_runtime.sql +0 -0
  308. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/control_flow.sql +0 -0
  309. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  310. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/error_handling.sql +0 -0
  311. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  312. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/import_options.sql +0 -0
  313. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/includes/helper.sql +0 -0
  314. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/includes.sql +0 -0
  315. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/io_formats.sql +0 -0
  316. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  317. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/multi_db.sql +0 -0
  318. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  319. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  320. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/predicates.sql +0 -0
  321. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/scripts.sql +0 -0
  322. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/smoke.sql +0 -0
  323. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  324. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  325. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  326. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/template_export.sql +0 -0
  327. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/timer.sql +0 -0
  328. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/transactions.sql +0 -0
  329. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/write_create_table.sql +0 -0
  330. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  331. {execsql2-2.22.7 → execsql2-2.22.9}/tests/scripts/test_sql_scripts.py +0 -0
  332. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/__init__.py +0 -0
  333. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/test_env_detection.py +0 -0
  334. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/test_expansion_bomb.py +0 -0
  335. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/test_import_regex_hardening.py +0 -0
  336. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/test_log_redaction.py +0 -0
  337. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/test_odbc_injection.py +0 -0
  338. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/test_path_containment.py +0 -0
  339. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/test_substitution_injection.py +0 -0
  340. {execsql2-2.22.7 → execsql2-2.22.9}/tests/security/test_zip_bomb.py +0 -0
  341. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_api.py +0 -0
  342. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_api_extended.py +0 -0
  343. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_ast.py +0 -0
  344. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_ast_parser.py +0 -0
  345. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_bundled_templates.py +0 -0
  346. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_config.py +0 -0
  347. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_config_data.py +0 -0
  348. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_config_extended.py +0 -0
  349. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_debug_repl.py +0 -0
  350. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_engine.py +0 -0
  351. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_error_messages.py +0 -0
  352. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_exceptions.py +0 -0
  353. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_executor.py +0 -0
  354. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_executor_inprocess.py +0 -0
  355. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_models.py +0 -0
  356. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_package.py +0 -0
  357. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_parser.py +0 -0
  358. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_parser_params.py +0 -0
  359. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_plugins.py +0 -0
  360. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_registry.py +0 -0
  361. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_script.py +0 -0
  362. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_state.py +0 -0
  363. {execsql2-2.22.7 → execsql2-2.22.9}/tests/test_types.py +0 -0
  364. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/__init__.py +0 -0
  365. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_auth.py +0 -0
  366. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_auth_extra.py +0 -0
  367. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_crypto.py +0 -0
  368. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_datetime.py +0 -0
  369. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_encodedfile_context.py +0 -0
  370. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_errors.py +0 -0
  371. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_errors_extra.py +0 -0
  372. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_fileio.py +0 -0
  373. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_fileio_extra.py +0 -0
  374. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_numeric.py +0 -0
  375. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_regex.py +0 -0
  376. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_strings.py +0 -0
  377. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_timer.py +0 -0
  378. {execsql2-2.22.7 → execsql2-2.22.9}/tests/utils/test_timer_extra.py +0 -0
  379. {execsql2-2.22.7 → execsql2-2.22.9}/zensical.toml +0 -0
@@ -13,6 +13,24 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.22.9] - 2026-09-24
17
+
18
+ ### Fixed
19
+
20
+ - `EMAIL`, and the `ON ERROR_HALT EMAIL` and `ON CANCEL_HALT EMAIL` hooks, failed with `AttributeError: 'SMTP' object has no attribute 'ehlo_or_hello_if_needed'` instead of sending. Every send was affected, on every server and TLS setting.
21
+ - An `[email]` `username` with no `password` or `enc_password` now reports a configuration error instead of failing with a `TypeError` from `smtplib`.
22
+
23
+ ______________________________________________________________________
24
+
25
+ ## [2.22.8] - 2026-09-17
26
+
27
+ ### Fixed
28
+
29
+ - `execsql-format` no longer leaves a space before a `;` when a comment precedes the closing statement line. `1 = 1;` came back as `1 = 1 ;` and was corrected only on the next run, so `--check` failed on a file the formatter had just written.
30
+ - `execsql-format` no longer moves a comment into the middle of the expression it describes. A comment above a select item containing a parenthesized composite-field access — `COALESCE((a.meas_value).undetected, FALSE)` — was relocated between the operand and the closing paren, and the file then reformatted differently on the next run, so `pre-commit` could not pass. Comments around `CASE` branches were relocated the same way.
31
+
32
+ ______________________________________________________________________
33
+
16
34
  ## [2.22.7] - 2026-09-17
17
35
 
18
36
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.22.7
3
+ Version: 2.22.9
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -384,7 +384,7 @@ execsql-format --no-sql --in-place scripts/
384
384
  ```yaml
385
385
  repos:
386
386
  - repo: https://github.com/geocoug/execsql
387
- rev: v2.22.7
387
+ rev: v2.22.9
388
388
  hooks:
389
389
  - id: execsql-format
390
390
  ```
@@ -253,7 +253,7 @@ execsql-format --no-sql --in-place scripts/
253
253
  ```yaml
254
254
  repos:
255
255
  - repo: https://github.com/geocoug/execsql
256
- rev: v2.22.7
256
+ rev: v2.22.9
257
257
  hooks:
258
258
  - id: execsql-format
259
259
  ```
@@ -272,57 +272,60 @@ These are behavioral changes driven by security or correctness issues in the ups
272
272
 
273
273
  ### Bug Fixes
274
274
 
275
- | Area | Fix |
276
- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
277
- | Oracle default port | Corrected from `5432` (PostgreSQL) to `1521`. |
278
- | MySQL `LOAD DATA INFILE` encoding | Python encoding names are now mapped to MySQL charset names. |
279
- | `dt_cast` type converters | Base `Database` class auto-populates 8 type converters that were previously left empty after the refactor. |
280
- | `FileWriter` CPU busy-loop | Uses blocking `queue.get(timeout=0.1)` instead of `get_nowait()` in a tight loop. |
281
- | Substitution variable cycles | 100-iteration limit prevents infinite loops on cyclic variable references. |
282
- | Script location in error messages | `ErrInfo.script_file` and `script_line_no` are now populated via `stamp_errinfo()` so error output includes "Line N of script foo.sql" context — restoring behavior present in the monolith. |
283
- | `$ERROR_MESSAGE` not updated | `$ERROR_MESSAGE` is now set on every error path: `exit_now()`, non-halting SQL errors, and non-halting metacommand errors. Previously it was initialized to `""` and never changed. |
284
- | Metacommand error message lost | When `halt_on_metacommand_err` is `ON`, the original handler `ErrInfo` is now re-raised; the generic "Unknown metacommand" message no longer replaces the specific error from the handler. |
285
- | Empty script name in error msg | `_execute_script_direct()` and `_execute_script_textual_console()` no longer append "in script , line 0" to uncaught-exception messages when `current_script_line()` returns an empty string. |
286
- | `PROMPT COMPARE` diff comparison | Diff engine uses native Python equality instead of string comparison — numeric types, Decimals, and booleans compare correctly. `None` is distinguished from empty string. Columns are matched by name (not position), key columns are excluded from comparison, and duplicate PKs keep the first row. |
287
- | `win_config_file` broken | Checked `os.name == "windows"` which Python never returns (correct value is `"nt"`). Fixed to `os.name == "nt"`. |
288
- | ELSEIF + ANDIF/ORIF | ANDIF/ORIF after an ELSEIF were silently attached to the parent IF condition instead of the ELSEIF clause. The compound condition was never evaluated for the ELSEIF branch. Fixed in the AST parser and executor. |
289
- | Cursor leak in `select_rowsource()` | Cursor was not closed on query execution failure. Row generators in EXPORT and COPY paths were not explicitly closed on error, relying on garbage collection for cleanup. |
290
- | `NumericParser` right-associative | Arithmetic operators were parsed right-to-left. `10 - 3 - 2` evaluated as `9` instead of `5`. Fixed to left-associative parsing. |
291
- | Empty-column check precedence | `DataTable` and `Database.populate_table()` had an operator precedence bug in the extra-column emptiness check — a redundant `and conf.del_empty_cols` caused incorrect short-circuit evaluation. |
292
- | SQLite import string processing | `SQLiteDatabase.populate_table()` applied `trim_strings`, `replace_newlines`, and `empty_strings` after copying row data, so processing never reached the INSERT. Fixed to process before extraction. |
293
- | `$CURRENT_DATABASE`/`$CURRENT_DBMS` stale after USE | These variables were only set at startup and on CONNECT, not refreshed when `USE` switched the active database. Now set in `set_static_system_vars()` so they update on any connection change. |
294
- | `DT_Text.data_type_name` wrong | Was `"character"` (same as `DT_Character`), making error messages indistinguishable. Corrected to `"text"`. |
295
- | `DT_Varchar` non-string data unchecked | `_from_data()` only enforced the 255-char limit for `str` inputs; non-string values passed through without conversion or length check. |
296
- | `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. |
297
- | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. |
298
- | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. |
299
- | `DT_Timestamp` claims time-only values | `dateutil.parser.parse()` silently fills in today's date for bare time strings like `"13:15:45"`, so `DT_Timestamp` matched before `DT_Time` in the inference order. `parse_datetime()` now rejects time-only strings. |
300
- | `CounterVars.substitute` skipped pos 0–1 | `re.I` was passed as the positional `pos` argument, skipping the first 2 characters of every string during counter variable expansion. Counter variables at the very start of a line were never matched. |
301
- | `exec_cmd` (SQLite/DuckDB) always raised TypeError | Passed bytes to `curs.execute()` via `.encode()`, which Python 3 `sqlite3`/`duckdb` reject. `EXECUTE PROCEDURE` metacommand was non-functional on these backends. |
302
- | MySQL `LOAD DATA INFILE` injection | File path, delimiter, and quotechar were interpolated without escaping. Now single-quotes are escaped consistent with the PostgreSQL COPY path. |
303
- | Missing server/database name at connect | Connecting to a server-based database without a server or database name now stops with a clear fatal error ("Configured database requires a server name, but none is provided.") instead of crashing inside the driver with a `TypeError`. |
304
- | Config file chain infinite loop | The `config_file` directive could chain config files without limit. A circular reference (via symlinks or different relative paths) caused an infinite loop at startup. Now capped at 20 files. |
305
- | Cursor leaks in database adapters | ~15 methods across all adapters used `curs = self.cursor()` / `curs.close()` without `try/finally`. If the query raised, the cursor leaked. Converted to `with self._cursor() as curs:`. |
306
- | JSON export malformed on special column names | Column names containing `"` or `\` produced invalid JSON. Now uses `json.dumps()` for all field names. |
307
- | Temp file creation TOCTOU race | `TempFileMgr.new_temp_fn()` discarded the `NamedTemporaryFile` handle, creating a race window. Now uses `tempfile.mkstemp()` for secure creation. |
308
- | `shlex.split` on Windows incorrect mode | Called without `posix=False` on Windows, mishandling backslash-heavy paths in SHELL commands. |
309
- | AST executor `~`/`+` variable scoping broken | The AST executor passed `localvars` through function parameters but never pushed `CommandList` frames onto `commandliststack`. Legacy metacommand handlers (`x_sub`, `x_rm_sub`, `xf_sub_defined`, `SUB_LOCAL`, prompt handlers, REPL) access `commandliststack[-1]` for `~` local and `+` outer-scope variables. This caused `~` vars to be invisible to SQL, `SUB_DEFINED(~var)` to always return false, and the REPL `.vars`/`.stack` to show empty state. Fixed by pushing/popping `CommandList` frames in `execute()` and `_execute_script_native()`. |
310
- | Firebird adapter import | Adapter imported `fdb` (the unmaintained legacy driver) while the `[firebird]` extra declared `firebird-driver`. `pip install execsql2[firebird]` followed by a Firebird connect raised `ModuleNotFoundError: No module named 'fdb'`. Adapter now imports `firebird.driver`, matching the declared dependency. |
311
- | Connect timeouts on network adapters | MySQL, SQL Server / Access / DSN (pyodbc), Firebird, and SMTP all now apply a 30 s connect timeout matching the existing PostgreSQL default. A silently-dropped network peer (firewall rule, dead VM) used to hang scripts indefinitely; now the connect fails fast. |
312
- | AST parser INCLUDE quoted paths broken | The AST parser captured the full INCLUDE target including surrounding quotes (`"path"`), but the legacy dispatch regex stripped them. Quoted INCLUDE paths failed with "File does not exist" even when the file was present. Fixed by stripping matched quote pairs in the parser. |
313
- | AST parser `BEGIN SCRIPT name(params)` rejected | The regex required whitespace between the script name and parameter list. `BEGIN SCRIPT foo(a,b)` (no space before `(`) silently failed to match, causing the matching `END SCRIPT` to raise "Unmatched END SCRIPT metacommand." Fixed by allowing optional whitespace before the parameter expression. |
314
- | AST executor forward SCRIPT references broken | The legacy engine registered all `BEGIN SCRIPT` blocks at parse time (two-pass), so `EXECUTE SCRIPT foo` could appear before the `BEGIN SCRIPT foo` definition. The AST executor walked the tree in a single pass, so forward references failed with "There is no SCRIPT named foo." Fixed by adding a pre-registration scan of all SCRIPT blocks before execution begins. |
315
- | DB-export type inference overrides source types | `DataTable`/`Column` inferred column types from the *string shape* of values — correct for CSV import (everything is `str`) but wrong for DB-query exports where the driver returns typed Python objects. A text column with values like `"2026-06-30 10:07:20 PM"` was inferred as `TIMESTAMP`, causing DuckDB bind errors. SQLite, DuckDB, HDF5, and JSON-schema exports now pass `infer_strings=False` to `DataTable`: string values are classified as text types only, while native Python objects (`Decimal`, `datetime`, etc.) still run through the full accumulator set for accurate schema inference. |
316
- | `EXPORT FORMAT SQLITE` rejects `decimal.Decimal` | SQLite's Python adapter does not accept `Decimal` in `executemany`, raising `ProgrammingError`. SQLite exports now convert `Decimal` values to their exact string representation before binding; SQLite's NUMERIC affinity stores them without loss of precision. |
317
- | Unsupported Python objects crash native exports | `EXPORT FORMAT SQLITE` and `EXPORT FORMAT DUCKDB` raised bind errors when the DB driver returned user-defined Python objects (e.g. PostGIS geometry). Both exporters now stringify any object whose type is not in the adapter's safe-binding set, preserving values as their `str()` form instead of crashing. |
318
- | `HALT` with a message hangs unattended runs | The message-bearing `HALT` patterns are registered after the general one and so take priority, meaning `x_halt`'s `gui_level > 1` guard was never reached and every `HALT MESSAGE` opened a modal dialog — blocking cron and CI runs indefinitely at `gui_level` 0, contrary to the documented behavior. The guard now lives in `x_halt_msg`: without a GUI console and below `gui_level` 2, the message and any `DISPLAY` rowset are written to the error output and the script exits with the requested status. An empty `DISPLAY` table renders as headings plus a zero row count rather than nothing. |
319
- | Textual `HALT` dialog ignored `button_list` | The Textual `MsgScreen` hardcoded a Cancel/Continue pair and dropped the caller's `column_headers`/`rowset`, so a `HALT` asking for a single OK button offered a Cancel that exited with status 2 instead of the requested `EXIT_STATUS`, and `HALT ... DISPLAY` showed no table. `MsgScreen` now honors `button_list` and `no_cancel` (which also suppresses <kbd>Esc</kbd>) and renders the rowset, matching the Tkinter backend. |
320
- | `execsql-format` corrupts `E'...'` literals | sqlglot's Postgres tokenizer consumes backslash escapes inside an escape string but its generator never re-emits them (tobymao/sqlglot#8191), so `E'\\s+'` became `e'\s+'` — a regex matching the letter `s` rather than whitespace — losing one backslash per pass until `E'\\\\'` degraded to the unparsable `e'\''`. Escape strings are now masked out of the sqlglot round trip and restored byte-for-byte, independently of the installed sqlglot version. |
321
- | Formatter could not detect literal-level content loss | The content-loss guard deleted every non-alphanumeric character before comparing *lengths*, so any rewrite confined to punctuation was invisible to it at any threshold. A literal round-trip check now requires every string literal in the input to survive verbatim in the output, falling back to the unformatted text otherwise. |
322
- | Formatter indents inside multi-line literals | Block indentation was applied to every line of a multi-line literal (`$$…$$` body or a `'…'` string spanning lines), so a literal inside an `IF`, `LOOP`, or `BEGIN SQL` block gained the block's leading whitespace on each continuation line — changing the value stored by the database. Lines that begin inside a dollar-quoted literal are now emitted verbatim and excluded from the indent arithmetic. Neither the literal round-trip guard (which covers only the sqlglot round trip) nor the existing idempotency test (which compares pass 1 to pass 2, while the indent is applied once and then stable) could detect it. |
323
- | Comma passes rewrite multi-line literals | `--leading-comma` and the trailing-comma normalization that precedes it moved a comma across a line break inside a multi-line literal, changing the stored value. Both now skip every line such a literal touches. |
324
- | Formatter never converged on unparsable statements | Comments are hidden from sqlglot as `/* marker */ line`. Removing the marker left the injected separator space behind, which was then measured as indentation — so a statement sqlglot could not parse (and therefore returned verbatim) gained one space on every run and never reached a fixed point, leaving `--check` permanently failing. Adjacent comments compounded it, leaving residue mid-expression that took a second pass to normalise. Marker runs are now removed together with the whitespace they introduced, and indentation is read from the line itself. |
325
- | Formatter dropped clauses from unparsable SQL | The sqlglot parse ran at `ErrorLevel.IGNORE`, which answers a statement it cannot parse with a partial AST rather than an error — the tokens it failed on are absent, and regenerating from that AST wrote back SQL the author never had. `INSERT ... CROSS JOIN LATERAL (VALUES ...) AS g(a, b) ON CONFLICT (a) DO NOTHING` came back without its `DO NOTHING`, a syntax error that `--in-place` reported as `reformatted`. Every guard passed it: one statement for one semicolon, 88% of the input's alphanumeric characters, and no string literals to round-trip. The guards are quantitative and the loss is qualitative — a dropped `DISTINCT`, `CASCADE`, `DESC`, or `NOT` is smaller still — so the parse now runs at `ErrorLevel.RAISE` and SQL that does not fully parse is emitted exactly as written. |
275
+ | Area | Fix |
276
+ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
277
+ | Oracle default port | Corrected from `5432` (PostgreSQL) to `1521`. |
278
+ | MySQL `LOAD DATA INFILE` encoding | Python encoding names are now mapped to MySQL charset names. |
279
+ | `dt_cast` type converters | Base `Database` class auto-populates 8 type converters that were previously left empty after the refactor. |
280
+ | `FileWriter` CPU busy-loop | Uses blocking `queue.get(timeout=0.1)` instead of `get_nowait()` in a tight loop. |
281
+ | Substitution variable cycles | 100-iteration limit prevents infinite loops on cyclic variable references. |
282
+ | Script location in error messages | `ErrInfo.script_file` and `script_line_no` are now populated via `stamp_errinfo()` so error output includes "Line N of script foo.sql" context — restoring behavior present in the monolith. |
283
+ | `$ERROR_MESSAGE` not updated | `$ERROR_MESSAGE` is now set on every error path: `exit_now()`, non-halting SQL errors, and non-halting metacommand errors. Previously it was initialized to `""` and never changed. |
284
+ | Metacommand error message lost | When `halt_on_metacommand_err` is `ON`, the original handler `ErrInfo` is now re-raised; the generic "Unknown metacommand" message no longer replaces the specific error from the handler. |
285
+ | Empty script name in error msg | `_execute_script_direct()` and `_execute_script_textual_console()` no longer append "in script , line 0" to uncaught-exception messages when `current_script_line()` returns an empty string. |
286
+ | `PROMPT COMPARE` diff comparison | Diff engine uses native Python equality instead of string comparison — numeric types, Decimals, and booleans compare correctly. `None` is distinguished from empty string. Columns are matched by name (not position), key columns are excluded from comparison, and duplicate PKs keep the first row. |
287
+ | `win_config_file` broken | Checked `os.name == "windows"` which Python never returns (correct value is `"nt"`). Fixed to `os.name == "nt"`. |
288
+ | ELSEIF + ANDIF/ORIF | ANDIF/ORIF after an ELSEIF were silently attached to the parent IF condition instead of the ELSEIF clause. The compound condition was never evaluated for the ELSEIF branch. Fixed in the AST parser and executor. |
289
+ | Cursor leak in `select_rowsource()` | Cursor was not closed on query execution failure. Row generators in EXPORT and COPY paths were not explicitly closed on error, relying on garbage collection for cleanup. |
290
+ | `NumericParser` right-associative | Arithmetic operators were parsed right-to-left. `10 - 3 - 2` evaluated as `9` instead of `5`. Fixed to left-associative parsing. |
291
+ | Empty-column check precedence | `DataTable` and `Database.populate_table()` had an operator precedence bug in the extra-column emptiness check — a redundant `and conf.del_empty_cols` caused incorrect short-circuit evaluation. |
292
+ | SQLite import string processing | `SQLiteDatabase.populate_table()` applied `trim_strings`, `replace_newlines`, and `empty_strings` after copying row data, so processing never reached the INSERT. Fixed to process before extraction. |
293
+ | `$CURRENT_DATABASE`/`$CURRENT_DBMS` stale after USE | These variables were only set at startup and on CONNECT, not refreshed when `USE` switched the active database. Now set in `set_static_system_vars()` so they update on any connection change. |
294
+ | `DT_Text.data_type_name` wrong | Was `"character"` (same as `DT_Character`), making error messages indistinguishable. Corrected to `"text"`. |
295
+ | `DT_Varchar` non-string data unchecked | `_from_data()` only enforced the 255-char limit for `str` inputs; non-string values passed through without conversion or length check. |
296
+ | `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. |
297
+ | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. |
298
+ | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. |
299
+ | `DT_Timestamp` claims time-only values | `dateutil.parser.parse()` silently fills in today's date for bare time strings like `"13:15:45"`, so `DT_Timestamp` matched before `DT_Time` in the inference order. `parse_datetime()` now rejects time-only strings. |
300
+ | `CounterVars.substitute` skipped pos 0–1 | `re.I` was passed as the positional `pos` argument, skipping the first 2 characters of every string during counter variable expansion. Counter variables at the very start of a line were never matched. |
301
+ | `exec_cmd` (SQLite/DuckDB) always raised TypeError | Passed bytes to `curs.execute()` via `.encode()`, which Python 3 `sqlite3`/`duckdb` reject. `EXECUTE PROCEDURE` metacommand was non-functional on these backends. |
302
+ | MySQL `LOAD DATA INFILE` injection | File path, delimiter, and quotechar were interpolated without escaping. Now single-quotes are escaped consistent with the PostgreSQL COPY path. |
303
+ | Missing server/database name at connect | Connecting to a server-based database without a server or database name now stops with a clear fatal error ("Configured database requires a server name, but none is provided.") instead of crashing inside the driver with a `TypeError`. |
304
+ | Config file chain infinite loop | The `config_file` directive could chain config files without limit. A circular reference (via symlinks or different relative paths) caused an infinite loop at startup. Now capped at 20 files. |
305
+ | Cursor leaks in database adapters | ~15 methods across all adapters used `curs = self.cursor()` / `curs.close()` without `try/finally`. If the query raised, the cursor leaked. Converted to `with self._cursor() as curs:`. |
306
+ | JSON export malformed on special column names | Column names containing `"` or `\` produced invalid JSON. Now uses `json.dumps()` for all field names. |
307
+ | Temp file creation TOCTOU race | `TempFileMgr.new_temp_fn()` discarded the `NamedTemporaryFile` handle, creating a race window. Now uses `tempfile.mkstemp()` for secure creation. |
308
+ | `shlex.split` on Windows incorrect mode | Called without `posix=False` on Windows, mishandling backslash-heavy paths in SHELL commands. |
309
+ | AST executor `~`/`+` variable scoping broken | The AST executor passed `localvars` through function parameters but never pushed `CommandList` frames onto `commandliststack`. Legacy metacommand handlers (`x_sub`, `x_rm_sub`, `xf_sub_defined`, `SUB_LOCAL`, prompt handlers, REPL) access `commandliststack[-1]` for `~` local and `+` outer-scope variables. This caused `~` vars to be invisible to SQL, `SUB_DEFINED(~var)` to always return false, and the REPL `.vars`/`.stack` to show empty state. Fixed by pushing/popping `CommandList` frames in `execute()` and `_execute_script_native()`. |
310
+ | Firebird adapter import | Adapter imported `fdb` (the unmaintained legacy driver) while the `[firebird]` extra declared `firebird-driver`. `pip install execsql2[firebird]` followed by a Firebird connect raised `ModuleNotFoundError: No module named 'fdb'`. Adapter now imports `firebird.driver`, matching the declared dependency. |
311
+ | Connect timeouts on network adapters | MySQL, SQL Server / Access / DSN (pyodbc), Firebird, and SMTP all now apply a 30 s connect timeout matching the existing PostgreSQL default. A silently-dropped network peer (firewall rule, dead VM) used to hang scripts indefinitely; now the connect fails fast. |
312
+ | AST parser INCLUDE quoted paths broken | The AST parser captured the full INCLUDE target including surrounding quotes (`"path"`), but the legacy dispatch regex stripped them. Quoted INCLUDE paths failed with "File does not exist" even when the file was present. Fixed by stripping matched quote pairs in the parser. |
313
+ | AST parser `BEGIN SCRIPT name(params)` rejected | The regex required whitespace between the script name and parameter list. `BEGIN SCRIPT foo(a,b)` (no space before `(`) silently failed to match, causing the matching `END SCRIPT` to raise "Unmatched END SCRIPT metacommand." Fixed by allowing optional whitespace before the parameter expression. |
314
+ | AST executor forward SCRIPT references broken | The legacy engine registered all `BEGIN SCRIPT` blocks at parse time (two-pass), so `EXECUTE SCRIPT foo` could appear before the `BEGIN SCRIPT foo` definition. The AST executor walked the tree in a single pass, so forward references failed with "There is no SCRIPT named foo." Fixed by adding a pre-registration scan of all SCRIPT blocks before execution begins. |
315
+ | DB-export type inference overrides source types | `DataTable`/`Column` inferred column types from the *string shape* of values — correct for CSV import (everything is `str`) but wrong for DB-query exports where the driver returns typed Python objects. A text column with values like `"2026-06-30 10:07:20 PM"` was inferred as `TIMESTAMP`, causing DuckDB bind errors. SQLite, DuckDB, HDF5, and JSON-schema exports now pass `infer_strings=False` to `DataTable`: string values are classified as text types only, while native Python objects (`Decimal`, `datetime`, etc.) still run through the full accumulator set for accurate schema inference. |
316
+ | `EXPORT FORMAT SQLITE` rejects `decimal.Decimal` | SQLite's Python adapter does not accept `Decimal` in `executemany`, raising `ProgrammingError`. SQLite exports now convert `Decimal` values to their exact string representation before binding; SQLite's NUMERIC affinity stores them without loss of precision. |
317
+ | Unsupported Python objects crash native exports | `EXPORT FORMAT SQLITE` and `EXPORT FORMAT DUCKDB` raised bind errors when the DB driver returned user-defined Python objects (e.g. PostGIS geometry). Both exporters now stringify any object whose type is not in the adapter's safe-binding set, preserving values as their `str()` form instead of crashing. |
318
+ | `HALT` with a message hangs unattended runs | The message-bearing `HALT` patterns are registered after the general one and so take priority, meaning `x_halt`'s `gui_level > 1` guard was never reached and every `HALT MESSAGE` opened a modal dialog — blocking cron and CI runs indefinitely at `gui_level` 0, contrary to the documented behavior. The guard now lives in `x_halt_msg`: without a GUI console and below `gui_level` 2, the message and any `DISPLAY` rowset are written to the error output and the script exits with the requested status. An empty `DISPLAY` table renders as headings plus a zero row count rather than nothing. |
319
+ | Textual `HALT` dialog ignored `button_list` | The Textual `MsgScreen` hardcoded a Cancel/Continue pair and dropped the caller's `column_headers`/`rowset`, so a `HALT` asking for a single OK button offered a Cancel that exited with status 2 instead of the requested `EXIT_STATUS`, and `HALT ... DISPLAY` showed no table. `MsgScreen` now honors `button_list` and `no_cancel` (which also suppresses <kbd>Esc</kbd>) and renders the rowset, matching the Tkinter backend. |
320
+ | `execsql-format` corrupts `E'...'` literals | sqlglot's Postgres tokenizer consumes backslash escapes inside an escape string but its generator never re-emits them (tobymao/sqlglot#8191), so `E'\\s+'` became `e'\s+'` — a regex matching the letter `s` rather than whitespace — losing one backslash per pass until `E'\\\\'` degraded to the unparsable `e'\''`. Escape strings are now masked out of the sqlglot round trip and restored byte-for-byte, independently of the installed sqlglot version. |
321
+ | Formatter could not detect literal-level content loss | The content-loss guard deleted every non-alphanumeric character before comparing *lengths*, so any rewrite confined to punctuation was invisible to it at any threshold. A literal round-trip check now requires every string literal in the input to survive verbatim in the output, falling back to the unformatted text otherwise. |
322
+ | Formatter indents inside multi-line literals | Block indentation was applied to every line of a multi-line literal (`$$…$$` body or a `'…'` string spanning lines), so a literal inside an `IF`, `LOOP`, or `BEGIN SQL` block gained the block's leading whitespace on each continuation line — changing the value stored by the database. Lines that begin inside a dollar-quoted literal are now emitted verbatim and excluded from the indent arithmetic. Neither the literal round-trip guard (which covers only the sqlglot round trip) nor the existing idempotency test (which compares pass 1 to pass 2, while the indent is applied once and then stable) could detect it. |
323
+ | Comma passes rewrite multi-line literals | `--leading-comma` and the trailing-comma normalization that precedes it moved a comma across a line break inside a multi-line literal, changing the stored value. Both now skip every line such a literal touches. |
324
+ | Formatter never converged on unparsable statements | Comments are hidden from sqlglot as `/* marker */ line`. Removing the marker left the injected separator space behind, which was then measured as indentation — so a statement sqlglot could not parse (and therefore returned verbatim) gained one space on every run and never reached a fixed point, leaving `--check` permanently failing. Adjacent comments compounded it, leaving residue mid-expression that took a second pass to normalise. Marker runs are now removed together with the whitespace they introduced, and indentation is read from the line itself. |
325
+ | Formatter dropped clauses from unparsable SQL | The sqlglot parse ran at `ErrorLevel.IGNORE`, which answers a statement it cannot parse with a partial AST rather than an error — the tokens it failed on are absent, and regenerating from that AST wrote back SQL the author never had. `INSERT ... CROSS JOIN LATERAL (VALUES ...) AS g(a, b) ON CONFLICT (a) DO NOTHING` came back without its `DO NOTHING`, a syntax error that `--in-place` reported as `reformatted`. Every guard passed it: one statement for one semicolon, 88% of the input's alphanumeric characters, and no string literals to round-trip. The guards are quantitative and the loss is qualitative — a dropped `DISTINCT`, `CASCADE`, `DESC`, or `NOT` is smaller still — so the parse now runs at `ErrorLevel.RAISE` and SQL that does not fully parse is emitted exactly as written. |
326
+ | Formatter moved a comment into the middle of an expression | Comments are hidden from sqlglot as `/* marker */ line`, and sqlglot attaches the marker to an AST node. When it then reflows that node across several lines the marker surfaces deep inside the expression — `COALESCE((a.meas_value).undetected, FALSE)` carried it onto the `).undetected, FALSE)` continuation line — so restoring the comment above the marker split the expression around it, and the resulting layout reformatted differently on the next run. Comments whose markers sqlglot dropped entirely (inside a `CASE`) were re-inserted by token matching, which picked continuation lines the same way. Both paths now place the comment above the line where its expression opens, found by walking back to the parenthesis depth the comment sat at in the input. |
327
+ | Formatter left a space before punctuation where a comment had been | Marker runs are replaced by the one space that keeps their neighbours apart, so `AND /* m */ RTRIM(` does not become `ANDRTRIM(`. Where sqlglot placed the run against a `;`, `,`, `)`, or `.` that space was residue rather than a separator: `1 = 1 /* m */;` came back as `1 = 1 ;`, normalised only on the next run, so `--check` failed on output the formatter had just produced. The run is now removed without a space at the ends of a line and against punctuation that binds to the token before it. |
328
+ | `EMAIL` SMTP login without a password | Upstream calls `smtplib.login()` with only a username when `[email] password` and `enc_password` are both unset, which raises `TypeError` from the stdlib — `login()` has no single-argument form. execsql2 reports a configuration error naming the missing password instead. |
326
329
 
327
330
  ______________________________________________________________________
328
331
 
@@ -353,6 +353,20 @@ At runtime, scripts can modify configuration via `CONFIG` metacommands (e.g., `C
353
353
 
354
354
  ______________________________________________________________________
355
355
 
356
+ ## Formatter Corpus
357
+
358
+ `tests/test_format.py` checks four invariants against a corpus of real `.sql` files rather than hand-written snippets alone: no string literal is lost, no comment changes parenthesis depth, formatting is idempotent, and nothing raises. The corpus is every `.sql` file in the repository, which means `tests/data/formatter_shapes/` is the place to add a construct that has broken the formatter — files dropped anywhere in the repo are picked up automatically.
359
+
360
+ Every formatter bug so far was reported from a production script and reproduced from a construct the project's own scripts do not contain, so the in-repo corpus can only catch a class of bug after someone has already hit it. To run the same checks over a private library of real SQL:
361
+
362
+ ```sh
363
+ EXECSQL_FORMAT_CORPUS=~/sql-library uv run pytest tests/test_format.py -k corpus
364
+ ```
365
+
366
+ Several directories may be given, separated by `:` (`;` on Windows). Unset — which is what CI sees — the in-repo corpus is used alone. Nothing is written to the corpus directories; the files are read and formatted in memory.
367
+
368
+ ______________________________________________________________________
369
+
356
370
  ## Further Reading
357
371
 
358
372
  - [Adding Metacommands](adding_metacommands.md) -- Step-by-step guide for new metacommands
@@ -85,6 +85,10 @@ Comments interleaved within SQL statements (e.g. `--` comments between SELECT co
85
85
  1. Markers are restored to their original `--` comment style and position.
86
86
  1. Comments that sqlglot's AST drops (e.g. inside CASE WHEN) are detected and re-inserted at the best matching position.
87
87
 
88
+ A comment written on its own line stays on its own line, above the expression it preceded — even when sqlglot reflows that expression across several lines. sqlglot attaches the marker to an AST node, and reformatting can carry it onto a continuation line deep inside the expression; the formatter walks back to the line where the expression opens, matching the parenthesis depth the comment sat at in the source. Without that, a comment above `COALESCE((a.meas_value).undetected, FALSE)` landed between the operand and its closing paren.
89
+
90
+ A comment that genuinely sits inside parentheses — between the columns of an `INSERT` list, or inside a `WHERE (...)` group — stays inside them.
91
+
88
92
  Block comments (`/* */`) that contain `-- !x!` metacommand markers (e.g. commented-out code blocks) are recognized and passed through without metacommand processing.
89
93
 
90
94
  #### Variable preservation
@@ -184,7 +188,7 @@ select id,name,created_at from users where active = true order by name;
184
188
  ```yaml
185
189
  repos:
186
190
  - repo: https://github.com/geocoug/execsql
187
- rev: v2.22.7
191
+ rev: v2.22.9
188
192
  hooks:
189
193
  - id: execsql-format
190
194
  ```
@@ -266,7 +266,7 @@ The section and property names that may be used in a configuration file are list
266
266
 
267
267
  `username`
268
268
 
269
- : The name of the user if the SMTP server requires login authentication.
269
+ : The name of the user if the SMTP server requires login authentication. If `username` is set, either `password` or `enc_password` must also be set; execsql reports an error rather than attempting an incomplete login.
270
270
 
271
271
  `password`
272
272
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.22.7"
7
+ version = "2.22.9"
8
8
  description = "Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = { file = "LICENSE.txt" }
@@ -178,7 +178,7 @@ skip-magic-trailing-comma = false
178
178
  line-ending = "auto"
179
179
 
180
180
  [tool.bumpversion]
181
- current_version = "2.22.7"
181
+ current_version = "2.22.9"
182
182
  commit = true
183
183
  tag = true
184
184
  tag_name = "v{new_version}"
@@ -256,6 +256,10 @@ fo = "fo"
256
256
  odf = "odf"
257
257
  # Latin-1 test data: b"caf\xe9" decodes to "café"
258
258
  caf = "caf"
259
+ # SMTP verb: EHLO-or-HELO, not the English word. Correcting it to "hello"
260
+ # renames smtplib.ehlo_or_helo_if_needed() into a method that does not exist,
261
+ # which is exactly how every EMAIL send broke in 2.22.9.
262
+ helo = "helo"
259
263
 
260
264
  [tool.tox]
261
265
  required = ["tox-uv"]
@@ -242,6 +242,7 @@
242
242
  # Settings for the EMAIL metacommand.
243
243
 
244
244
  # SMTP server connection.
245
+ # If username is set, password (or enc_password, below) is required too.
245
246
  #host=
246
247
  #port=
247
248
  #username=