execsql2 2.22.5__tar.gz → 2.22.6__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 (378) hide show
  1. {execsql2-2.22.5 → execsql2-2.22.6}/CHANGELOG.md +10 -0
  2. {execsql2-2.22.5 → execsql2-2.22.6}/PKG-INFO +2 -2
  3. {execsql2-2.22.5 → execsql2-2.22.6}/README.md +1 -1
  4. {execsql2-2.22.5 → execsql2-2.22.6}/docs/about/divergence.md +50 -47
  5. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/formatter.md +6 -4
  6. {execsql2-2.22.5 → execsql2-2.22.6}/pyproject.toml +2 -2
  7. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/format.py +101 -12
  8. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_format.py +271 -0
  9. {execsql2-2.22.5 → execsql2-2.22.6}/uv.lock +1 -1
  10. {execsql2-2.22.5 → execsql2-2.22.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {execsql2-2.22.5 → execsql2-2.22.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {execsql2-2.22.5 → execsql2-2.22.6}/.github/dependabot.yml +0 -0
  13. {execsql2-2.22.5 → execsql2-2.22.6}/.github/workflows/ci-cd.yml +0 -0
  14. {execsql2-2.22.5 → execsql2-2.22.6}/.gitignore +0 -0
  15. {execsql2-2.22.5 → execsql2-2.22.6}/.pre-commit-config.yaml +0 -0
  16. {execsql2-2.22.5 → execsql2-2.22.6}/.pre-commit-hooks.yaml +0 -0
  17. {execsql2-2.22.5 → execsql2-2.22.6}/.python-version +0 -0
  18. {execsql2-2.22.5 → execsql2-2.22.6}/.readthedocs.yaml +0 -0
  19. {execsql2-2.22.5 → execsql2-2.22.6}/CONTRIBUTING.md +0 -0
  20. {execsql2-2.22.5 → execsql2-2.22.6}/LICENSE.txt +0 -0
  21. {execsql2-2.22.5 → execsql2-2.22.6}/NOTICE +0 -0
  22. {execsql2-2.22.5 → execsql2-2.22.6}/SECURITY.md +0 -0
  23. {execsql2-2.22.5 → execsql2-2.22.6}/docs/about/contributors.md +0 -0
  24. {execsql2-2.22.5 → execsql2-2.22.6}/docs/about/copyright.md +0 -0
  25. {execsql2-2.22.5 → execsql2-2.22.6}/docs/api/cli.md +0 -0
  26. {execsql2-2.22.5 → execsql2-2.22.6}/docs/api/db.md +0 -0
  27. {execsql2-2.22.5 → execsql2-2.22.6}/docs/api/exporters.md +0 -0
  28. {execsql2-2.22.5 → execsql2-2.22.6}/docs/api/importers.md +0 -0
  29. {execsql2-2.22.5 → execsql2-2.22.6}/docs/api/index.md +0 -0
  30. {execsql2-2.22.5 → execsql2-2.22.6}/docs/api/metacommands.md +0 -0
  31. {execsql2-2.22.5 → execsql2-2.22.6}/docs/dev/adding_db_adapters.md +0 -0
  32. {execsql2-2.22.5 → execsql2-2.22.6}/docs/dev/adding_exporters.md +0 -0
  33. {execsql2-2.22.5 → execsql2-2.22.6}/docs/dev/adding_importers.md +0 -0
  34. {execsql2-2.22.5 → execsql2-2.22.6}/docs/dev/adding_metacommands.md +0 -0
  35. {execsql2-2.22.5 → execsql2-2.22.6}/docs/dev/architecture.md +0 -0
  36. {execsql2-2.22.5 → execsql2-2.22.6}/docs/dev/releasing.md +0 -0
  37. {execsql2-2.22.5 → execsql2-2.22.6}/docs/getting-started/installation.md +0 -0
  38. {execsql2-2.22.5 → execsql2-2.22.6}/docs/getting-started/requirements.md +0 -0
  39. {execsql2-2.22.5 → execsql2-2.22.6}/docs/getting-started/syntax.md +0 -0
  40. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/debugging.md +0 -0
  41. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/documentation.md +0 -0
  42. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/encoding.md +0 -0
  43. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/examples.md +0 -0
  44. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/logging.md +0 -0
  45. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/sql_syntax.md +0 -0
  46. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/usage.md +0 -0
  47. {execsql2-2.22.5 → execsql2-2.22.6}/docs/guides/using_scripts.md +0 -0
  48. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/Compare_planets.png +0 -0
  49. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/actions.png +0 -0
  50. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/actions2.png +0 -0
  51. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/checkboxes.png +0 -0
  52. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/connect.b64 +0 -0
  53. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/connect.png +0 -0
  54. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/create_conf.png +0 -0
  55. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/data_error1_screenshot.jpg +0 -0
  56. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/entry_form.png +0 -0
  57. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/execsql_console.png +0 -0
  58. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/execsql_logo_01.png +0 -0
  59. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/fatals.png +0 -0
  60. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/logo_small.png +0 -0
  61. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/pause_terminal.png +0 -0
  62. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/pause_terminal_sm.b64 +0 -0
  63. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/pause_terminal_sm.png +0 -0
  64. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/prompt_compare.png +0 -0
  65. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/set_build_commands.jpg +0 -0
  66. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/unit_conversions.b64 +0 -0
  67. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/unit_conversions_029.png +0 -0
  68. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/unmatched.png +0 -0
  69. {execsql2-2.22.5 → execsql2-2.22.6}/docs/images/vim_execsql_highlight.png +0 -0
  70. {execsql2-2.22.5 → execsql2-2.22.6}/docs/index.md +0 -0
  71. {execsql2-2.22.5 → execsql2-2.22.6}/docs/reference/configuration.md +0 -0
  72. {execsql2-2.22.5 → execsql2-2.22.6}/docs/reference/metacommands.md +0 -0
  73. {execsql2-2.22.5 → execsql2-2.22.6}/docs/reference/security.md +0 -0
  74. {execsql2-2.22.5 → execsql2-2.22.6}/docs/reference/substitution_vars.md +0 -0
  75. {execsql2-2.22.5 → execsql2-2.22.6}/extras/plugin-template/README.md +0 -0
  76. {execsql2-2.22.5 → execsql2-2.22.6}/extras/plugin-template/pyproject.toml +0 -0
  77. {execsql2-2.22.5 → execsql2-2.22.6}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  78. {execsql2-2.22.5 → execsql2-2.22.6}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  79. {execsql2-2.22.5 → execsql2-2.22.6}/extras/vscode-execsql/README.md +0 -0
  80. {execsql2-2.22.5 → execsql2-2.22.6}/extras/vscode-execsql/package.json +0 -0
  81. {execsql2-2.22.5 → execsql2-2.22.6}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  82. {execsql2-2.22.5 → execsql2-2.22.6}/justfile +0 -0
  83. {execsql2-2.22.5 → execsql2-2.22.6}/scripts/benchmarks/README.md +0 -0
  84. {execsql2-2.22.5 → execsql2-2.22.6}/scripts/benchmarks/results/.gitignore +0 -0
  85. {execsql2-2.22.5 → execsql2-2.22.6}/scripts/benchmarks/run_benchmarks.py +0 -0
  86. {execsql2-2.22.5 → execsql2-2.22.6}/scripts/generate_vscode_grammar.py +0 -0
  87. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/__init__.py +0 -0
  88. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/__main__.py +0 -0
  89. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/api.py +0 -0
  90. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/cli/__init__.py +0 -0
  91. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/cli/dsn.py +0 -0
  92. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/cli/help.py +0 -0
  93. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/cli/lint.py +0 -0
  94. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/cli/run.py +0 -0
  95. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/config.py +0 -0
  96. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/data/__init__.py +0 -0
  97. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/data/execsql.conf.template +0 -0
  98. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/__init__.py +0 -0
  99. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/access.py +0 -0
  100. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/base.py +0 -0
  101. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/dsn.py +0 -0
  102. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/duckdb.py +0 -0
  103. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/factory.py +0 -0
  104. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/firebird.py +0 -0
  105. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/mysql.py +0 -0
  106. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/oracle.py +0 -0
  107. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/postgres.py +0 -0
  108. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/sqlite.py +0 -0
  109. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/db/sqlserver.py +0 -0
  110. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/debug/__init__.py +0 -0
  111. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/debug/repl.py +0 -0
  112. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exceptions.py +0 -0
  113. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/__init__.py +0 -0
  114. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/base.py +0 -0
  115. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/delimited.py +0 -0
  116. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/duckdb.py +0 -0
  117. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/feather.py +0 -0
  118. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/html.py +0 -0
  119. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/json.py +0 -0
  120. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/latex.py +0 -0
  121. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/markdown.py +0 -0
  122. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/ods.py +0 -0
  123. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/parquet.py +0 -0
  124. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/pretty.py +0 -0
  125. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/protocol.py +0 -0
  126. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/raw.py +0 -0
  127. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/sqlite.py +0 -0
  128. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/templates.py +0 -0
  129. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/values.py +0 -0
  130. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/xls.py +0 -0
  131. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/xlsx.py +0 -0
  132. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/xml.py +0 -0
  133. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/yaml.py +0 -0
  134. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/exporters/zip.py +0 -0
  135. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/gui/__init__.py +0 -0
  136. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/gui/base.py +0 -0
  137. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/gui/console.py +0 -0
  138. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/gui/desktop.py +0 -0
  139. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/gui/tui.py +0 -0
  140. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/importers/__init__.py +0 -0
  141. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/importers/base.py +0 -0
  142. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/importers/csv.py +0 -0
  143. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/importers/feather.py +0 -0
  144. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/importers/json.py +0 -0
  145. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/importers/ods.py +0 -0
  146. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/importers/xls.py +0 -0
  147. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/__init__.py +0 -0
  148. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/conditions.py +0 -0
  149. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/connect.py +0 -0
  150. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/control.py +0 -0
  151. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/data.py +0 -0
  152. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/debug.py +0 -0
  153. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/dispatch.py +0 -0
  154. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/io.py +0 -0
  155. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/io_export.py +0 -0
  156. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/io_fileops.py +0 -0
  157. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/io_import.py +0 -0
  158. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/io_write.py +0 -0
  159. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/prompt.py +0 -0
  160. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/script_ext.py +0 -0
  161. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/system.py +0 -0
  162. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/metacommands/upsert.py +0 -0
  163. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/models.py +0 -0
  164. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/parser.py +0 -0
  165. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/plugins.py +0 -0
  166. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/py.typed +0 -0
  167. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/script/__init__.py +0 -0
  168. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/script/ast.py +0 -0
  169. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/script/control.py +0 -0
  170. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/script/engine.py +0 -0
  171. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/script/executor.py +0 -0
  172. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/script/parser.py +0 -0
  173. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/script/variables.py +0 -0
  174. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/state.py +0 -0
  175. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/state.pyi +0 -0
  176. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/types.py +0 -0
  177. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/__init__.py +0 -0
  178. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/auth.py +0 -0
  179. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/crypto.py +0 -0
  180. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/datetime.py +0 -0
  181. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/errors.py +0 -0
  182. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/fileio.py +0 -0
  183. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/gui.py +0 -0
  184. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/mail.py +0 -0
  185. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/numeric.py +0 -0
  186. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/regex.py +0 -0
  187. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/strings.py +0 -0
  188. {execsql2-2.22.5 → execsql2-2.22.6}/src/execsql/utils/timer.py +0 -0
  189. {execsql2-2.22.5 → execsql2-2.22.6}/templates/README.md +0 -0
  190. {execsql2-2.22.5 → execsql2-2.22.6}/templates/config_settings.sqlite +0 -0
  191. {execsql2-2.22.5 → execsql2-2.22.6}/templates/example_config_prompt.sql +0 -0
  192. {execsql2-2.22.5 → execsql2-2.22.6}/templates/make_config_db.sql +0 -0
  193. {execsql2-2.22.5 → execsql2-2.22.6}/templates/md_compare.sql +0 -0
  194. {execsql2-2.22.5 → execsql2-2.22.6}/templates/md_glossary.sql +0 -0
  195. {execsql2-2.22.5 → execsql2-2.22.6}/templates/md_upsert.sql +0 -0
  196. {execsql2-2.22.5 → execsql2-2.22.6}/templates/pg_compare.sql +0 -0
  197. {execsql2-2.22.5 → execsql2-2.22.6}/templates/pg_glossary.sql +0 -0
  198. {execsql2-2.22.5 → execsql2-2.22.6}/templates/pg_upsert.sql +0 -0
  199. {execsql2-2.22.5 → execsql2-2.22.6}/templates/script_template.sql +0 -0
  200. {execsql2-2.22.5 → execsql2-2.22.6}/templates/ss_compare.sql +0 -0
  201. {execsql2-2.22.5 → execsql2-2.22.6}/templates/ss_glossary.sql +0 -0
  202. {execsql2-2.22.5 → execsql2-2.22.6}/templates/ss_upsert.sql +0 -0
  203. {execsql2-2.22.5 → execsql2-2.22.6}/tests/__init__.py +0 -0
  204. {execsql2-2.22.5 → execsql2-2.22.6}/tests/cli/__init__.py +0 -0
  205. {execsql2-2.22.5 → execsql2-2.22.6}/tests/cli/test_cli.py +0 -0
  206. {execsql2-2.22.5 → execsql2-2.22.6}/tests/cli/test_cli_e2e.py +0 -0
  207. {execsql2-2.22.5 → execsql2-2.22.6}/tests/cli/test_cli_run.py +0 -0
  208. {execsql2-2.22.5 → execsql2-2.22.6}/tests/cli/test_lint.py +0 -0
  209. {execsql2-2.22.5 → execsql2-2.22.6}/tests/cli/test_ping.py +0 -0
  210. {execsql2-2.22.5 → execsql2-2.22.6}/tests/cli/test_profile.py +0 -0
  211. {execsql2-2.22.5 → execsql2-2.22.6}/tests/conftest.py +0 -0
  212. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/__init__.py +0 -0
  213. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_access_windows.py +0 -0
  214. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_base.py +0 -0
  215. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_db_adapters_mocked.py +0 -0
  216. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  217. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_dsn.py +0 -0
  218. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_duckdb.py +0 -0
  219. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_factory.py +0 -0
  220. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_mysql_case_folding.py +0 -0
  221. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_mysql_inprocess.py +0 -0
  222. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_postgres.py +0 -0
  223. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_postgres_inprocess.py +0 -0
  224. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_sqlite.py +0 -0
  225. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_sqlite_extra.py +0 -0
  226. {execsql2-2.22.5 → execsql2-2.22.6}/tests/db/test_sqlserver_inprocess.py +0 -0
  227. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/__init__.py +0 -0
  228. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_base.py +0 -0
  229. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_db.py +0 -0
  230. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_delimited.py +0 -0
  231. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_duckdb_exporter.py +0 -0
  232. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_exporters.py +0 -0
  233. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_feather.py +0 -0
  234. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_html_extended.py +0 -0
  235. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_html_latex.py +0 -0
  236. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_json.py +0 -0
  237. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_json_extended.py +0 -0
  238. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_latex_extended.py +0 -0
  239. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_markdown.py +0 -0
  240. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_ods.py +0 -0
  241. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_ods_export.py +0 -0
  242. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_parquet.py +0 -0
  243. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_pretty_extended.py +0 -0
  244. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_raw_extended.py +0 -0
  245. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_sqlite_exporter.py +0 -0
  246. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_templates.py +0 -0
  247. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_templates_extended.py +0 -0
  248. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_values_extended.py +0 -0
  249. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_xls_xlsx.py +0 -0
  250. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_xlsx.py +0 -0
  251. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_xml.py +0 -0
  252. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_yaml.py +0 -0
  253. {execsql2-2.22.5 → execsql2-2.22.6}/tests/exporters/test_zip.py +0 -0
  254. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/__init__.py +0 -0
  255. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_backends.py +0 -0
  256. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_backends_extended.py +0 -0
  257. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_compare_stats.py +0 -0
  258. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_compute_row_diffs.py +0 -0
  259. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_desktop_dialogs.py +0 -0
  260. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_format_table.py +0 -0
  261. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_tui_pilot.py +0 -0
  262. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_tui_pilot_complex.py +0 -0
  263. {execsql2-2.22.5 → execsql2-2.22.6}/tests/gui/test_utils_gui_extended.py +0 -0
  264. {execsql2-2.22.5 → execsql2-2.22.6}/tests/importers/__init__.py +0 -0
  265. {execsql2-2.22.5 → execsql2-2.22.6}/tests/importers/test_base_extended.py +0 -0
  266. {execsql2-2.22.5 → execsql2-2.22.6}/tests/importers/test_csv_edge_cases.py +0 -0
  267. {execsql2-2.22.5 → execsql2-2.22.6}/tests/importers/test_csv_importer.py +0 -0
  268. {execsql2-2.22.5 → execsql2-2.22.6}/tests/importers/test_feather_importer.py +0 -0
  269. {execsql2-2.22.5 → execsql2-2.22.6}/tests/importers/test_json_importer.py +0 -0
  270. {execsql2-2.22.5 → execsql2-2.22.6}/tests/importers/test_ods_importer.py +0 -0
  271. {execsql2-2.22.5 → execsql2-2.22.6}/tests/importers/test_xls_importer.py +0 -0
  272. {execsql2-2.22.5 → execsql2-2.22.6}/tests/integration/__init__.py +0 -0
  273. {execsql2-2.22.5 → execsql2-2.22.6}/tests/integration/conftest.py +0 -0
  274. {execsql2-2.22.5 → execsql2-2.22.6}/tests/integration/test_dsn.py +0 -0
  275. {execsql2-2.22.5 → execsql2-2.22.6}/tests/integration/test_duckdb.py +0 -0
  276. {execsql2-2.22.5 → execsql2-2.22.6}/tests/integration/test_mysql.py +0 -0
  277. {execsql2-2.22.5 → execsql2-2.22.6}/tests/integration/test_postgres.py +0 -0
  278. {execsql2-2.22.5 → execsql2-2.22.6}/tests/integration/test_sqlite.py +0 -0
  279. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/__init__.py +0 -0
  280. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_assert.py +0 -0
  281. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_breakpoint.py +0 -0
  282. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_conditions_extra.py +0 -0
  283. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_connect.py +0 -0
  284. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_io_export.py +0 -0
  285. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_io_import.py +0 -0
  286. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands.py +0 -0
  287. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_connect.py +0 -0
  288. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_data.py +0 -0
  289. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_extended.py +0 -0
  290. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  291. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_io.py +0 -0
  292. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  293. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  294. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_system.py +0 -0
  295. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  296. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_pg_upsert.py +0 -0
  297. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_prompt.py +0 -0
  298. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_row_count.py +0 -0
  299. {execsql2-2.22.5 → execsql2-2.22.6}/tests/metacommands/test_show_scripts.py +0 -0
  300. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/__init__.py +0 -0
  301. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  302. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  303. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  304. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/audit_smoke.sql +0 -0
  305. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/config_runtime.sql +0 -0
  306. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/control_flow.sql +0 -0
  307. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  308. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/error_handling.sql +0 -0
  309. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  310. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/import_options.sql +0 -0
  311. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/includes/helper.sql +0 -0
  312. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/includes.sql +0 -0
  313. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/io_formats.sql +0 -0
  314. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  315. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/multi_db.sql +0 -0
  316. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  317. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  318. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/predicates.sql +0 -0
  319. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/scripts.sql +0 -0
  320. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/smoke.sql +0 -0
  321. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  322. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  323. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  324. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/template_export.sql +0 -0
  325. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/timer.sql +0 -0
  326. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/transactions.sql +0 -0
  327. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/write_create_table.sql +0 -0
  328. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  329. {execsql2-2.22.5 → execsql2-2.22.6}/tests/scripts/test_sql_scripts.py +0 -0
  330. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/__init__.py +0 -0
  331. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/test_env_detection.py +0 -0
  332. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/test_expansion_bomb.py +0 -0
  333. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/test_import_regex_hardening.py +0 -0
  334. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/test_log_redaction.py +0 -0
  335. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/test_odbc_injection.py +0 -0
  336. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/test_path_containment.py +0 -0
  337. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/test_substitution_injection.py +0 -0
  338. {execsql2-2.22.5 → execsql2-2.22.6}/tests/security/test_zip_bomb.py +0 -0
  339. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_api.py +0 -0
  340. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_api_extended.py +0 -0
  341. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_ast.py +0 -0
  342. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_ast_parser.py +0 -0
  343. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_bundled_templates.py +0 -0
  344. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_config.py +0 -0
  345. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_config_data.py +0 -0
  346. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_config_extended.py +0 -0
  347. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_debug_repl.py +0 -0
  348. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_engine.py +0 -0
  349. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_error_messages.py +0 -0
  350. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_exceptions.py +0 -0
  351. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_executor.py +0 -0
  352. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_executor_inprocess.py +0 -0
  353. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_mail.py +0 -0
  354. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_models.py +0 -0
  355. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_package.py +0 -0
  356. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_parser.py +0 -0
  357. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_parser_params.py +0 -0
  358. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_plugins.py +0 -0
  359. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_registry.py +0 -0
  360. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_script.py +0 -0
  361. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_state.py +0 -0
  362. {execsql2-2.22.5 → execsql2-2.22.6}/tests/test_types.py +0 -0
  363. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/__init__.py +0 -0
  364. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_auth.py +0 -0
  365. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_auth_extra.py +0 -0
  366. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_crypto.py +0 -0
  367. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_datetime.py +0 -0
  368. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_encodedfile_context.py +0 -0
  369. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_errors.py +0 -0
  370. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_errors_extra.py +0 -0
  371. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_fileio.py +0 -0
  372. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_fileio_extra.py +0 -0
  373. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_numeric.py +0 -0
  374. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_regex.py +0 -0
  375. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_strings.py +0 -0
  376. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_timer.py +0 -0
  377. {execsql2-2.22.5 → execsql2-2.22.6}/tests/utils/test_timer_extra.py +0 -0
  378. {execsql2-2.22.5 → execsql2-2.22.6}/zensical.toml +0 -0
@@ -13,6 +13,16 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.22.6] - 2026-08-28
17
+
18
+ ### Fixed
19
+
20
+ - `execsql-format` no longer indents inside a multi-line string literal. A `$$…$$` body or a multi-line `'…'` string inside an `IF`, `LOOP`, or `BEGIN SQL` block had the block's indentation added to each of its lines, changing the value the database stores.
21
+ - `execsql-format --leading-comma` no longer moves a comma into or out of a multi-line string literal, which rewrote the stored value.
22
+ - `execsql-format` now reaches a stable result in one pass. A statement sqlglot could not parse gained a space of indentation on every run and never converged, so `--check` could not pass on an already-formatted file; two adjacent comments on one statement also cost an extra pass.
23
+
24
+ ______________________________________________________________________
25
+
16
26
  ## [2.22.5] - 2026-08-27
17
27
 
18
28
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.22.5
3
+ Version: 2.22.6
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.5
387
+ rev: v2.22.6
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.5
256
+ rev: v2.22.6
257
257
  hooks:
258
258
  - id: execsql-format
259
259
  ```
@@ -272,53 +272,56 @@ 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. |
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. |
322
325
 
323
326
  ______________________________________________________________________
324
327
 
@@ -93,12 +93,14 @@ execsql substitution variables (`!!varname!!`, `!{varname}!`) are replaced with
93
93
 
94
94
  #### String literal preservation { #string_literals }
95
95
 
96
- Formatting never changes what a string literal contains. Two mechanisms enforce this:
96
+ Formatting never changes what a string literal contains. Four mechanisms enforce this:
97
97
 
98
98
  - PostgreSQL escape strings (`E'...'`) are held out of the sqlglot round trip entirely and restored byte-for-byte, keeping both their backslash escapes and their `E` prefix. This matters because sqlglot consumes backslash escapes inside an escape string without re-emitting them, which would turn `E'\\s+'` (a regex matching whitespace) into `e'\s+'` (a regex matching the letter `s`).
99
- - Every other literal — ordinary `'...'` strings and dollar-quoted bodies (`$$...$$`, `$tag$...$tag$`) — is compared before and after formatting. If any literal fails to come back unchanged, the statement is left unformatted rather than rewritten.
99
+ - Indentation is never applied inside a multi-line literal — a dollar-quoted body (`$$...$$`, `$tag$...$tag$`) or an ordinary `'...'` string that spans lines. Leading whitespace on a continuation line is part of the stored value, not layout. The line that *opens* the literal is ordinary SQL and is indented with the statement around it, but every line from there to the closing delimiter is emitted exactly as written. Such a literal therefore keeps its original whitespace regardless of `--indent` or how deeply the statement is nested — which means its body will not line up visually with the statement that contains it. That is unavoidable: indenting it would change the string.
100
+ - Commas are never moved into or out of a multi-line literal. `--leading-comma`, and the trailing-comma normalization that precedes it, both skip every line such a literal touches, including the line that opens it. A comma inside a multi-line string is data, so repositioning it would change the value rather than the layout.
101
+ - Literals that do pass through sqlglot — ordinary single-line `'...'` strings — are compared before and after. If any fails to come back unchanged, the statement is left unformatted rather than rewritten.
100
102
 
101
- The second check is deliberately conservative: a statement may occasionally be left alone when the rewrite would in fact have been harmless (for example, sqlglot normalizes `interval '1 day'` to `INTERVAL '1 DAY'`, which changes the literal's text but not its meaning). Losing formatting on a statement is recoverable; silently changing what a query does is not.
103
+ The last check is deliberately conservative: a statement may occasionally be left alone when the rewrite would in fact have been harmless (for example, sqlglot normalizes `interval '1 day'` to `INTERVAL '1 DAY'`, which changes the literal's text but not its meaning). Losing formatting on a statement is recoverable; silently changing what a query does is not.
102
104
 
103
105
  #### Fallback behavior
104
106
 
@@ -169,7 +171,7 @@ select id,name,created_at from users where active = true order by name;
169
171
  ```yaml
170
172
  repos:
171
173
  - repo: https://github.com/geocoug/execsql
172
- rev: v2.22.5
174
+ rev: v2.22.6
173
175
  hooks:
174
176
  - id: execsql-format
175
177
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.22.5"
7
+ version = "2.22.6"
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.5"
181
+ current_version = "2.22.6"
182
182
  commit = true
183
183
  tag = true
184
184
  tag_name = "v{new_version}"
@@ -278,6 +278,64 @@ def _sql_literal_texts(sql: str) -> list[str]:
278
278
  return [sql[start:end] for start, end, _ in _iter_sql_literals(sql)]
279
279
 
280
280
 
281
+ def _literal_line_starts(lines: list[str]) -> list[int]:
282
+ """Return the character offset at which each of *lines* starts once joined."""
283
+ starts: list[int] = []
284
+ offset = 0
285
+ for line in lines:
286
+ starts.append(offset)
287
+ offset += len(line) + 1 # +1 for the newline joined between lines
288
+ return starts
289
+
290
+
291
+ def _literal_interior_lines(lines: list[str]) -> set[int]:
292
+ """Return indices of *lines* that begin inside a multi-line string literal.
293
+
294
+ The line that *opens* a literal is ordinary SQL up to the delimiter, so it
295
+ is indented with the statement as usual. Every later line of that literal
296
+ — through and including the one holding the closing delimiter — begins
297
+ inside the string, which makes its leading whitespace part of the stored
298
+ value rather than layout. Re-indenting those lines changes what the
299
+ database receives, so callers must emit them verbatim.
300
+
301
+ This covers every literal kind the scanner reports, not just dollar-quoted
302
+ bodies: an ordinary ``'…'`` string spanning lines carries its newlines and
303
+ indentation exactly the same way.
304
+ """
305
+ if len(lines) < 2:
306
+ return set()
307
+ text = "\n".join(lines)
308
+ starts = _literal_line_starts(lines)
309
+
310
+ interior: set[int] = set()
311
+ for start, end, _kind in _iter_sql_literals(text):
312
+ interior.update(i for i, line_start in enumerate(starts) if start < line_start < end)
313
+ return interior
314
+
315
+
316
+ def _literal_spanning_lines(lines: list[str]) -> set[int]:
317
+ """Return indices of every line that any multi-line literal touches.
318
+
319
+ Unlike :func:`_literal_interior_lines` this also includes the line that
320
+ *opens* the literal. Passes that move text between lines — rather than
321
+ only adjusting leading whitespace — must leave the whole span alone, since
322
+ the opening line holds literal content after its delimiter.
323
+ """
324
+ if len(lines) < 2:
325
+ return set()
326
+ text = "\n".join(lines)
327
+ starts = _literal_line_starts(lines)
328
+
329
+ spanning: set[int] = set()
330
+ for start, end, _kind in _iter_sql_literals(text):
331
+ touched = [i for i, line_start in enumerate(starts) if start < line_start < end]
332
+ if not touched:
333
+ continue # single-line literal — nothing spans
334
+ spanning.update(touched)
335
+ spanning.add(min(touched) - 1) # the line that opens the literal
336
+ return spanning
337
+
338
+
281
339
  # Placeholder standing in for a masked E'...' literal. It is a plain literal
282
340
  # so that sqlglot sees a string where a string was, and lays the statement out
283
341
  # the same way it would have for the real one.
@@ -503,6 +561,14 @@ def _has_mid_statement_comments(lines: list[str]) -> bool:
503
561
 
504
562
  _CMT_MARKER = "EXECSQL_CMTMARKER_"
505
563
  _CMT_MARKER_RE = re.compile(rf"/\*\s*({re.escape(_CMT_MARKER)}\d+)\s*\*/")
564
+ # A whole run of adjacent markers plus the whitespace injected around them.
565
+ # Markers are written as ``/* marker */ line`` and several consecutive source
566
+ # comments attach several markers to one line, so removing them one at a time
567
+ # leaves a separator space behind for each. When sqlglot moves the run into
568
+ # the middle of an expression that residue shows up as ``AND RTRIM(`` and is
569
+ # only normalised on the following pass, which costs an extra pass to
570
+ # converge. Matching the run as a unit removes it cleanly. See issue #35.
571
+ _CMT_MARKER_RUN_RE = re.compile(rf"\s*(?:/\*\s*{re.escape(_CMT_MARKER)}\d+\s*\*/\s*)+")
506
572
 
507
573
 
508
574
  def _format_preserving_comments(
@@ -570,12 +636,16 @@ def _format_preserving_comments(
570
636
  markers_here = _CMT_MARKER_RE.findall(fline)
571
637
  if markers_here:
572
638
  # Strip markers to get the underlying SQL line and its indent
573
- cleaned = _CMT_MARKER_RE.sub("", fline).strip()
574
- # Determine indent: use the SQL line's indent from sqlglot
575
- line_indent = ""
576
- if cleaned:
577
- raw_cleaned = _CMT_MARKER_RE.sub("", fline)
578
- line_indent = raw_cleaned[: len(raw_cleaned) - len(raw_cleaned.lstrip())]
639
+ cleaned = _CMT_MARKER_RUN_RE.sub(" ", fline).strip()
640
+ # Indent comes from the line's own leading whitespace, never from
641
+ # what is left after removing the marker. Markers are injected as
642
+ # ``/* marker */ line`` — with a separating space — so measuring
643
+ # after removal counts that space as indentation. Whenever
644
+ # sqlglot leaves the line untouched (an unparsable statement falls
645
+ # back verbatim) the extra space is re-measured and re-added on the
646
+ # next run, growing the indent by one on every pass and never
647
+ # converging. See issue #35.
648
+ line_indent = fline[: len(fline) - len(fline.lstrip())]
579
649
  for m in markers_here:
580
650
  if m in comment_store:
581
651
  orig = comment_store[m]
@@ -671,12 +741,24 @@ def format_sql_block(
671
741
  if not non_empty:
672
742
  return [""] * len(lines)
673
743
 
674
- base = min(len(line) - len(line.lstrip()) for line in non_empty)
744
+ # Lines that begin inside a multi-line literal carry content, not layout —
745
+ # dedenting or indenting them changes the value the database stores.
746
+ # Exclude them from the indent arithmetic and emit them verbatim.
747
+ interior = _literal_interior_lines(lines)
748
+ if interior:
749
+ # format_file already withholds dollar-quoted blocks from sqlglot; keep
750
+ # that true here so the line indices below stay aligned with the output.
751
+ use_sql = False
752
+
753
+ layout_lines = [line for i, line in enumerate(lines) if line.strip() and i not in interior]
754
+ base = min((len(line) - len(line.lstrip()) for line in layout_lines), default=0)
675
755
  target_prefix = " " * (depth * indent)
676
- rebased = [line[base:] if line.strip() else "" for line in lines]
756
+ rebased = [line if i in interior else (line[base:] if line.strip() else "") for i, line in enumerate(lines)]
677
757
 
678
758
  if not use_sql:
679
- return [target_prefix + line if line.strip() else "" for line in rebased]
759
+ return [
760
+ line if i in interior else (target_prefix + line if line.strip() else "") for i, line in enumerate(rebased)
761
+ ]
680
762
 
681
763
  # When comments appear mid-statement, use the marker-based approach
682
764
  # which preserves both comments AND sqlglot formatting. When all
@@ -747,6 +829,9 @@ def _normalize_to_trailing_comma(text: str) -> str:
747
829
  every invocation. Comments between the two SQL lines stay in place.
748
830
  """
749
831
  lines = text.split("\n")
832
+ # Symmetric with _apply_leading_comma: a leading comma inside a multi-line
833
+ # literal is data, not layout, so the whole span is left alone.
834
+ protected = _literal_spanning_lines(lines)
750
835
 
751
836
  def find_prev_sql_line(idx: int) -> int:
752
837
  k = idx - 1
@@ -756,7 +841,7 @@ def _normalize_to_trailing_comma(text: str) -> str:
756
841
 
757
842
  for i, line in enumerate(lines):
758
843
  stripped = line.lstrip()
759
- if not stripped.startswith(",") or _is_comment_only(line):
844
+ if not stripped.startswith(",") or _is_comment_only(line) or i in protected:
760
845
  continue
761
846
  # Don't try to rewrite leading-comma inside a comment-only line.
762
847
  # Move the comma onto the previous SQL line as a trailing `,`.
@@ -792,15 +877,19 @@ def _apply_leading_comma(text: str) -> str:
792
877
  TestIdempotency).
793
878
  """
794
879
  lines = text.split("\n")
880
+ # A comma inside a multi-line string literal is data. Moving it to the next
881
+ # line would rewrite the stored value, so leave every line such a literal
882
+ # touches — including the one that opens it — exactly as it is.
883
+ protected = _literal_spanning_lines(lines)
795
884
  i = 0
796
885
  n = len(lines)
797
886
  while i < n:
798
887
  rstripped = lines[i].rstrip()
799
- if rstripped.endswith(",") and not _is_comment_only(lines[i]):
888
+ if rstripped.endswith(",") and not _is_comment_only(lines[i]) and i not in protected:
800
889
  j = i + 1
801
890
  while j < n and (not lines[j].strip() or _is_comment_only(lines[j])):
802
891
  j += 1
803
- if j < n:
892
+ if j < n and j not in protected:
804
893
  stripped = lines[i].rstrip()
805
894
  comma_idx = stripped.rfind(",")
806
895
  lines[i] = stripped[:comma_idx] + stripped[comma_idx + 1 :]