execsql2 2.15.8__tar.gz → 2.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. {execsql2-2.15.8 → execsql2-2.16.0}/.gitignore +2 -0
  2. execsql2-2.16.0/AUDIT.md +324 -0
  3. {execsql2-2.15.8 → execsql2-2.16.0}/CHANGELOG.md +92 -0
  4. {execsql2-2.15.8 → execsql2-2.16.0}/PKG-INFO +59 -1
  5. {execsql2-2.15.8 → execsql2-2.16.0}/README.md +58 -0
  6. {execsql2-2.15.8 → execsql2-2.16.0}/docs/about/divergence.md +48 -4
  7. {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/index.md +21 -1
  8. {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/adding_metacommands.md +11 -3
  9. {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/architecture.md +31 -0
  10. {execsql2-2.15.8 → execsql2-2.16.0}/docs/getting-started/syntax.md +16 -0
  11. {execsql2-2.15.8 → execsql2-2.16.0}/docs/reference/configuration.md +6 -0
  12. {execsql2-2.15.8 → execsql2-2.16.0}/docs/reference/substitution_vars.md +3 -0
  13. execsql2-2.16.0/extras/plugin-template/README.md +71 -0
  14. execsql2-2.16.0/extras/plugin-template/pyproject.toml +22 -0
  15. execsql2-2.16.0/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +96 -0
  16. execsql2-2.16.0/extras/plugin-template/tests/test_plugin.py.example +110 -0
  17. {execsql2-2.15.8 → execsql2-2.16.0}/justfile +45 -10
  18. {execsql2-2.15.8 → execsql2-2.16.0}/pyproject.toml +5 -3
  19. execsql2-2.16.0/src/execsql/__init__.py +21 -0
  20. execsql2-2.16.0/src/execsql/api.py +580 -0
  21. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/__init__.py +123 -0
  22. execsql2-2.16.0/src/execsql/cli/lint_ast.py +439 -0
  23. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/run.py +113 -102
  24. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/config.py +29 -4
  25. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/access.py +1 -0
  26. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/base.py +4 -1
  27. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/dsn.py +3 -2
  28. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/duckdb.py +1 -1
  29. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/factory.py +3 -0
  30. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/firebird.py +2 -1
  31. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/mysql.py +2 -1
  32. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/oracle.py +2 -1
  33. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/postgres.py +2 -1
  34. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/sqlite.py +1 -1
  35. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/sqlserver.py +3 -2
  36. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/debug/repl.py +27 -10
  37. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/base.py +6 -4
  38. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/delimited.py +11 -3
  39. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/pretty.py +9 -12
  40. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/tui.py +59 -2
  41. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/__init__.py +3 -0
  42. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/conditions.py +20 -2
  43. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/connect.py +1 -1
  44. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/control.py +8 -14
  45. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/debug.py +6 -4
  46. execsql2-2.16.0/src/execsql/metacommands/io_export.py +325 -0
  47. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/io_fileops.py +7 -13
  48. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/io_write.py +1 -1
  49. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/script_ext.py +8 -5
  50. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/upsert.py +40 -0
  51. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/models.py +8 -12
  52. execsql2-2.16.0/src/execsql/plugins.py +414 -0
  53. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/script/__init__.py +36 -12
  54. execsql2-2.16.0/src/execsql/script/ast.py +562 -0
  55. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/script/engine.py +59 -368
  56. execsql2-2.16.0/src/execsql/script/executor.py +833 -0
  57. execsql2-2.16.0/src/execsql/script/parser.py +663 -0
  58. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/script/variables.py +11 -0
  59. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/state.py +55 -2
  60. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/crypto.py +14 -10
  61. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/errors.py +31 -8
  62. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/gui.py +139 -17
  63. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/mail.py +15 -12
  64. {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_cli.py +214 -0
  65. {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_cli_run.py +18 -151
  66. {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_profile.py +4 -4
  67. {execsql2-2.15.8 → execsql2-2.16.0}/tests/conftest.py +16 -1
  68. execsql2-2.16.0/tests/db/test_db_adapters_mocked.py +546 -0
  69. execsql2-2.16.0/tests/db/test_dsn.py +365 -0
  70. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_base.py +15 -13
  71. {execsql2-2.15.8 → execsql2-2.16.0}/tests/gui/test_backends.py +16 -5
  72. {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_csv_edge_cases.py +97 -1
  73. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_breakpoint.py +7 -0
  74. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands.py +4 -3
  75. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_extended.py +8 -37
  76. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_fileops_extra.py +4 -20
  77. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_io.py +5 -21
  78. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_script_ext.py +5 -11
  79. execsql2-2.16.0/tests/scripts/fixtures/control_flow.sql +178 -0
  80. execsql2-2.16.0/tests/scripts/fixtures/io_roundtrip.sql +135 -0
  81. execsql2-2.16.0/tests/scripts/fixtures/parse_only/parse_tree.sql +541 -0
  82. execsql2-2.16.0/tests/scripts/fixtures/smoke.sql +138 -0
  83. execsql2-2.16.0/tests/scripts/test_sql_scripts.py +67 -0
  84. execsql2-2.16.0/tests/test_api.py +303 -0
  85. execsql2-2.16.0/tests/test_ast.py +552 -0
  86. execsql2-2.16.0/tests/test_ast_parser.py +829 -0
  87. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_config_data.py +61 -0
  88. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_debug_repl.py +7 -0
  89. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_engine.py +15 -630
  90. execsql2-2.16.0/tests/test_executor.py +939 -0
  91. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_format.py +183 -0
  92. execsql2-2.16.0/tests/test_parser.py +814 -0
  93. execsql2-2.16.0/tests/test_plugins.py +213 -0
  94. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_script.py +8 -0
  95. execsql2-2.16.0/tests/utils/__init__.py +0 -0
  96. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_errors_extra.py +4 -4
  97. {execsql2-2.15.8 → execsql2-2.16.0}/uv.lock +1 -1
  98. execsql2-2.15.8/src/execsql/__init__.py +0 -16
  99. execsql2-2.15.8/src/execsql/metacommands/io_export.py +0 -523
  100. execsql2-2.15.8/tests/test_parser.py +0 -446
  101. {execsql2-2.15.8 → execsql2-2.16.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  102. {execsql2-2.15.8 → execsql2-2.16.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  103. {execsql2-2.15.8 → execsql2-2.16.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  104. {execsql2-2.15.8 → execsql2-2.16.0}/.github/workflows/ci-cd.yml +0 -0
  105. {execsql2-2.15.8 → execsql2-2.16.0}/.pre-commit-config.yaml +0 -0
  106. {execsql2-2.15.8 → execsql2-2.16.0}/.pre-commit-hooks.yaml +0 -0
  107. {execsql2-2.15.8 → execsql2-2.16.0}/.python-version +0 -0
  108. {execsql2-2.15.8 → execsql2-2.16.0}/.readthedocs.yaml +0 -0
  109. {execsql2-2.15.8 → execsql2-2.16.0}/CONTRIBUTING.md +0 -0
  110. {execsql2-2.15.8 → execsql2-2.16.0}/LICENSE.txt +0 -0
  111. {execsql2-2.15.8 → execsql2-2.16.0}/NOTICE +0 -0
  112. {execsql2-2.15.8 → execsql2-2.16.0}/SECURITY.md +0 -0
  113. {execsql2-2.15.8 → execsql2-2.16.0}/docs/about/contributors.md +0 -0
  114. {execsql2-2.15.8 → execsql2-2.16.0}/docs/about/copyright.md +0 -0
  115. {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/cli.md +0 -0
  116. {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/db.md +0 -0
  117. {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/exporters.md +0 -0
  118. {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/importers.md +0 -0
  119. {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/metacommands.md +0 -0
  120. {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/adding_db_adapters.md +0 -0
  121. {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/adding_exporters.md +0 -0
  122. {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/adding_importers.md +0 -0
  123. {execsql2-2.15.8 → execsql2-2.16.0}/docs/getting-started/installation.md +0 -0
  124. {execsql2-2.15.8 → execsql2-2.16.0}/docs/getting-started/requirements.md +0 -0
  125. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/debugging.md +0 -0
  126. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/documentation.md +0 -0
  127. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/encoding.md +0 -0
  128. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/examples.md +0 -0
  129. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/formatter.md +0 -0
  130. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/logging.md +0 -0
  131. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/sql_syntax.md +0 -0
  132. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/usage.md +0 -0
  133. {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/using_scripts.md +0 -0
  134. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/Compare_planets.png +0 -0
  135. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/actions.png +0 -0
  136. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/actions2.png +0 -0
  137. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/checkboxes.png +0 -0
  138. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/connect.b64 +0 -0
  139. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/connect.png +0 -0
  140. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/create_conf.png +0 -0
  141. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/data_error1_screenshot.jpg +0 -0
  142. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/entry_form.png +0 -0
  143. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/execsql_console.png +0 -0
  144. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/execsql_logo_01.png +0 -0
  145. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/fatals.png +0 -0
  146. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/logo_small.png +0 -0
  147. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/pause_terminal.png +0 -0
  148. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/pause_terminal_sm.b64 +0 -0
  149. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/pause_terminal_sm.png +0 -0
  150. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/prompt_compare.png +0 -0
  151. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/set_build_commands.jpg +0 -0
  152. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/unit_conversions.b64 +0 -0
  153. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/unit_conversions_029.png +0 -0
  154. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/unmatched.png +0 -0
  155. {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/vim_execsql_highlight.png +0 -0
  156. {execsql2-2.15.8 → execsql2-2.16.0}/docs/index.md +0 -0
  157. {execsql2-2.15.8 → execsql2-2.16.0}/docs/reference/metacommands.md +0 -0
  158. {execsql2-2.15.8 → execsql2-2.16.0}/docs/reference/security.md +0 -0
  159. {execsql2-2.15.8 → execsql2-2.16.0}/extras/vscode-execsql/README.md +0 -0
  160. {execsql2-2.15.8 → execsql2-2.16.0}/extras/vscode-execsql/package.json +0 -0
  161. {execsql2-2.15.8 → execsql2-2.16.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  162. {execsql2-2.15.8 → execsql2-2.16.0}/scripts/generate_vscode_grammar.py +0 -0
  163. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/__main__.py +0 -0
  164. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/dsn.py +0 -0
  165. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/help.py +0 -0
  166. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/lint.py +0 -0
  167. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/__init__.py +0 -0
  168. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/debug/__init__.py +0 -0
  169. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exceptions.py +0 -0
  170. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/__init__.py +0 -0
  171. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/duckdb.py +0 -0
  172. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/feather.py +0 -0
  173. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/html.py +0 -0
  174. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/json.py +0 -0
  175. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/latex.py +0 -0
  176. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/markdown.py +0 -0
  177. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/ods.py +0 -0
  178. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/parquet.py +0 -0
  179. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/protocol.py +0 -0
  180. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/raw.py +0 -0
  181. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/sqlite.py +0 -0
  182. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/templates.py +0 -0
  183. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/values.py +0 -0
  184. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/xls.py +0 -0
  185. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/xlsx.py +0 -0
  186. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/xml.py +0 -0
  187. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/yaml.py +0 -0
  188. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/zip.py +0 -0
  189. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/format.py +0 -0
  190. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/__init__.py +0 -0
  191. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/base.py +0 -0
  192. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/console.py +0 -0
  193. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/desktop.py +0 -0
  194. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/__init__.py +0 -0
  195. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/base.py +0 -0
  196. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/csv.py +0 -0
  197. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/feather.py +0 -0
  198. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/json.py +0 -0
  199. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/ods.py +0 -0
  200. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/xls.py +0 -0
  201. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/data.py +0 -0
  202. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/dispatch.py +0 -0
  203. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/io.py +0 -0
  204. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/io_import.py +0 -0
  205. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/prompt.py +0 -0
  206. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/system.py +0 -0
  207. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/parser.py +0 -0
  208. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/py.typed +0 -0
  209. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/script/control.py +0 -0
  210. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/types.py +0 -0
  211. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/__init__.py +0 -0
  212. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/auth.py +0 -0
  213. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/datetime.py +0 -0
  214. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/fileio.py +0 -0
  215. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/numeric.py +0 -0
  216. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/regex.py +0 -0
  217. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/strings.py +0 -0
  218. {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/timer.py +0 -0
  219. {execsql2-2.15.8 → execsql2-2.16.0}/templates/README.md +0 -0
  220. {execsql2-2.15.8 → execsql2-2.16.0}/templates/config_settings.sqlite +0 -0
  221. {execsql2-2.15.8 → execsql2-2.16.0}/templates/example_config_prompt.sql +0 -0
  222. {execsql2-2.15.8 → execsql2-2.16.0}/templates/execsql.conf +0 -0
  223. {execsql2-2.15.8 → execsql2-2.16.0}/templates/make_config_db.sql +0 -0
  224. {execsql2-2.15.8 → execsql2-2.16.0}/templates/md_compare.sql +0 -0
  225. {execsql2-2.15.8 → execsql2-2.16.0}/templates/md_glossary.sql +0 -0
  226. {execsql2-2.15.8 → execsql2-2.16.0}/templates/md_upsert.sql +0 -0
  227. {execsql2-2.15.8 → execsql2-2.16.0}/templates/pg_compare.sql +0 -0
  228. {execsql2-2.15.8 → execsql2-2.16.0}/templates/pg_glossary.sql +0 -0
  229. {execsql2-2.15.8 → execsql2-2.16.0}/templates/pg_upsert.sql +0 -0
  230. {execsql2-2.15.8 → execsql2-2.16.0}/templates/script_template.sql +0 -0
  231. {execsql2-2.15.8 → execsql2-2.16.0}/templates/ss_compare.sql +0 -0
  232. {execsql2-2.15.8 → execsql2-2.16.0}/templates/ss_glossary.sql +0 -0
  233. {execsql2-2.15.8 → execsql2-2.16.0}/templates/ss_upsert.sql +0 -0
  234. {execsql2-2.15.8 → execsql2-2.16.0}/tests/__init__.py +0 -0
  235. {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/__init__.py +0 -0
  236. {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_cli_e2e.py +0 -0
  237. {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_lint.py +0 -0
  238. {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_ping.py +0 -0
  239. {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/__init__.py +0 -0
  240. {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_base.py +0 -0
  241. {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_duckdb.py +0 -0
  242. {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_factory.py +0 -0
  243. {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_postgres.py +0 -0
  244. {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_sqlite.py +0 -0
  245. {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_sqlite_extra.py +0 -0
  246. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/__init__.py +0 -0
  247. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_db.py +0 -0
  248. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_delimited.py +0 -0
  249. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  250. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_exporters.py +0 -0
  251. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_feather.py +0 -0
  252. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_html_extended.py +0 -0
  253. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_html_latex.py +0 -0
  254. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_json.py +0 -0
  255. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_json_extended.py +0 -0
  256. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_latex_extended.py +0 -0
  257. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_markdown.py +0 -0
  258. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_ods.py +0 -0
  259. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_parquet.py +0 -0
  260. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_pretty_extended.py +0 -0
  261. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_raw_extended.py +0 -0
  262. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  263. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_templates.py +0 -0
  264. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_templates_extended.py +0 -0
  265. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_values_extended.py +0 -0
  266. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_xls_xlsx.py +0 -0
  267. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_xlsx.py +0 -0
  268. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_xml.py +0 -0
  269. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_yaml.py +0 -0
  270. {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_zip.py +0 -0
  271. {execsql2-2.15.8 → execsql2-2.16.0}/tests/gui/__init__.py +0 -0
  272. {execsql2-2.15.8 → execsql2-2.16.0}/tests/gui/test_compare_stats.py +0 -0
  273. {execsql2-2.15.8 → execsql2-2.16.0}/tests/gui/test_compute_row_diffs.py +0 -0
  274. {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/__init__.py +0 -0
  275. {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_base_extended.py +0 -0
  276. {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_csv_importer.py +0 -0
  277. {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_feather_importer.py +0 -0
  278. {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_json_importer.py +0 -0
  279. {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_ods_importer.py +0 -0
  280. {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_xls_importer.py +0 -0
  281. {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/__init__.py +0 -0
  282. {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/conftest.py +0 -0
  283. {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_dsn.py +0 -0
  284. {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_duckdb.py +0 -0
  285. {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_mysql.py +0 -0
  286. {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_postgres.py +0 -0
  287. {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_sqlite.py +0 -0
  288. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/__init__.py +0 -0
  289. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_assert.py +0 -0
  290. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_connect.py +0 -0
  291. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_io_export.py +0 -0
  292. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_io_import.py +0 -0
  293. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  294. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_data.py +0 -0
  295. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  296. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_system.py +0 -0
  297. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  298. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_pg_upsert.py +0 -0
  299. {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_row_count.py +0 -0
  300. {execsql2-2.15.8/tests/utils → execsql2-2.16.0/tests/scripts}/__init__.py +0 -0
  301. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_config.py +0 -0
  302. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_config_extended.py +0 -0
  303. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_error_messages.py +0 -0
  304. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_exceptions.py +0 -0
  305. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_mail.py +0 -0
  306. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_models.py +0 -0
  307. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_package.py +0 -0
  308. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_registry.py +0 -0
  309. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_state.py +0 -0
  310. {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_types.py +0 -0
  311. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_auth.py +0 -0
  312. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_auth_extra.py +0 -0
  313. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_crypto.py +0 -0
  314. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_datetime.py +0 -0
  315. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_errors.py +0 -0
  316. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_fileio.py +0 -0
  317. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_fileio_extra.py +0 -0
  318. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_numeric.py +0 -0
  319. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_regex.py +0 -0
  320. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_strings.py +0 -0
  321. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_timer.py +0 -0
  322. {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_timer_extra.py +0 -0
  323. {execsql2-2.15.8 → execsql2-2.16.0}/zensical.toml +0 -0
@@ -35,6 +35,7 @@ htmlcov/
35
35
  execsql.log
36
36
  !scripts/generate_vscode_grammar.py
37
37
  scripts/
38
+ !tests/scripts
38
39
 
39
40
  # Distribution
40
41
  *.whl
@@ -47,3 +48,4 @@ _execsql/
47
48
  .claude/
48
49
  ANALYSIS.md
49
50
  FINDINGS.md
51
+ CLAUDE.md
@@ -0,0 +1,324 @@
1
+ # Codebase Audit — execsql2
2
+
3
+ **Original audit:** 2026-04-13
4
+ **Last updated:** 2026-04-29
5
+
6
+ Full audit covered 42 source files and 12 test files. 25 findings were identified (F1-F25)
7
+ plus 29 additional findings across security, performance, test gaps, edge cases, code quality,
8
+ and divergences. Of those, **25 findings have been resolved** (10 bugs, 3 perf fixes, 2 edge
9
+ cases, 1 security fix, 8 doc fixes, 1 CI fix, and the F-series fixes below).
10
+
11
+ **Resolved F-series:** F1 (substitute_all tuple unpack), F2 (bytes-to-stdout), F5 (export
12
+ dispatch duplication), F7 (commandliststack bounds), F8 (exec_cmd injection), F10 (enc_password
13
+ deprecation warning), F12 (env var filtering), F13 (bumpversion --no-verify), F14 (dead
14
+ \_DEFAULT_CTX), F17 (duplicate tuple entries), F20 (password persistence), F24 (Comment AST node
15
+ implemented).
16
+
17
+ **User-excluded:** F11 (--allow-shell flag) — not adding.
18
+
19
+ ______________________________________________________________________
20
+
21
+ ## Architecture & Code Quality
22
+
23
+ ### F3. [HIGH] Global mutable state blocks threading and library API
24
+
25
+ `src/execsql/state.py:399-432` — `_ctx` is a plain module-level global. All 200+ metacommand
26
+ handlers, SQL execution, variable substitution, and the IF-stack go through `_state.foo` which
27
+ resolves to `_ctx.foo`. Two threads executing scripts simultaneously will corrupt each other's
28
+ state. Blocks the planned PARALLEL metacommand and safe `from execsql import run` usage.
29
+
30
+ **Fix:** Replace `_ctx` with `contextvars.ContextVar` or `threading.local()`.
31
+
32
+ ### F4. [HIGH] `_run()` is a 460-line god-function with 36 parameters
33
+
34
+ `src/execsql/cli/run.py:193-660` — Performs config parsing, CLI argument merging, DSN parsing,
35
+ database connection, GUI init, script loading, dry-run, AST execution, legacy execution,
36
+ profiling, and cleanup in a single function. Estimated cyclomatic complexity ~85-90. Untestable
37
+ as a unit.
38
+
39
+ **Fix:** Extract into phases: `_setup_config()`, `_connect_db()`, `_load_script()`,
40
+ `_execute()`, `_teardown()`.
41
+
42
+ ### F15. [MEDIUM] Legacy parser is a 150-line function with 6 levels of nesting
43
+
44
+ `src/execsql/script/engine.py:864-1012` — The production parser tracks 6 state variables with
45
+ deepest nesting at 6 levels. The AST parser is the planned replacement; accelerating that
46
+ migration is the real fix.
47
+
48
+ ### F19. [MEDIUM] 51 near-duplicate regex patterns in conditions.py
49
+
50
+ `src/execsql/metacommands/conditions.py:426-627` — `CONTAINS`, `STARTS_WITH`, `ENDS_WITH` each
51
+ register 17 quoting variants as separate regex patterns. Adding a new string predicate requires
52
+ copying and modifying 17 patterns.
53
+
54
+ **Fix:** Consolidate into a single pattern per predicate with optional quoting alternation groups.
55
+
56
+ ### CQ-1. [MEDIUM] 111 bare `except Exception` clauses across 38 files
57
+
58
+ Many suppress errors silently (`pass`). Notable: `db/base.py` (14), `utils/fileio.py` (9),
59
+ `db/access.py` (10). Each should be reviewed for more specific exception types.
60
+
61
+ ### CQ-4. [MEDIUM] Inconsistent `_cursor()` context manager vs bare `cursor()`
62
+
63
+ The `Database` base class provides `_cursor()` for cleanup, but many adapter methods still use
64
+ `curs = self.cursor()` without cleanup. Standardize to `with self._cursor() as curs:`.
65
+
66
+ ### F22. [LOW] Eager import of `AccessDatabase` in connect.py
67
+
68
+ `src/execsql/metacommands/connect.py:1-2` — Imports `AccessDatabase` (depends on pyodbc,
69
+ Windows-only) at module load. Moving to lazy import was reverted because test patch targets
70
+ broke. Marginal benefit.
71
+
72
+ ### F23. [LOW] `conftest.py` `minimal_conf` fixture doesn't cover all attributes
73
+
74
+ `tests/conftest.py:33-56` — `SimpleNamespace` with ~16 attributes vs `ConfigData.__init__`'s
75
+ ~50+. Tests hitting unset attributes get `AttributeError`.
76
+
77
+ ### F25. [LOW] `format.py` SQL formatter hardcodes PostgreSQL dialect
78
+
79
+ `src/execsql/format.py:155,160` — `sqlglot.parse(read="postgres")` regardless of target DB.
80
+
81
+ **Fix:** Accept a `--dialect` flag or infer from `--type`.
82
+
83
+ ### CQ-2. [LOW] `JsonDatatype` class uses class-level attribute assignment anti-pattern
84
+
85
+ `src/execsql/models.py:308-324` — `JsonDatatype.integer` assigned twice (lines 317 and 322).
86
+ Could be an enum or dict.
87
+
88
+ ### CQ-3. [LOW] `Encrypt.ky` is a mutable class variable
89
+
90
+ `src/execsql/utils/crypto.py:48-57` — Dict never mutated after definition but declared as
91
+ mutable class attribute. Frozen dict or class constant would be cleaner.
92
+
93
+ ### CQ-5. [LOW] `state.py` version parsing catches bare `Exception`
94
+
95
+ `src/execsql/state.py:148` — Should be `except (ValueError, IndexError)`.
96
+
97
+ ### CQ-6. [LOW] `ConfigData.export_output_dir` is dynamically added
98
+
99
+ `src/execsql/cli/run.py:365` — Attribute doesn't exist in `ConfigData.__init__()`. Should be
100
+ declared with default `None`.
101
+
102
+ ______________________________________________________________________
103
+
104
+ ## Security
105
+
106
+ ### SEC-1. [MEDIUM] SHELL metacommand passes user input to subprocess
107
+
108
+ `src/execsql/metacommands/system.py:38-56` — `shlex.split()` tokenizes but doesn't sanitize.
109
+ Substitution variables from `PROMPT ENTRY`, `SUB_INI`, or env vars could inject arguments.
110
+ Partially mitigated: `subprocess.call` with list arg doesn't invoke a shell. User excluded
111
+ `--allow-shell` flag (F11).
112
+
113
+ ### SEC-3. [LOW] `Encrypt` class provides no real security
114
+
115
+ `src/execsql/utils/crypto.py` — Hardcoded XOR keys in source. Any `enc_password` can be
116
+ trivially decoded. Documented as obfuscation-only. Deprecation warning now emitted (F10 fix).
117
+
118
+ ### SEC-4. [LOW] `x_include` path traversal
119
+
120
+ `src/execsql/metacommands/io_fileops.py:26-38` — `INCLUDE` accepts arbitrary file paths from
121
+ scripts. If variables are populated from external sources, arbitrary files could be included.
122
+
123
+ ______________________________________________________________________
124
+
125
+ ## Performance
126
+
127
+ ### F18. [MEDIUM] `SubVarSet._substitute_nested` is O(V) per call
128
+
129
+ `src/execsql/script/variables.py:262-296` — Fallback iterates over every defined variable with
130
+ case-insensitive substring search. With N tokens and V variables, worst case is O(N * V).
131
+
132
+ **Fix:** Index variables by first few characters for faster fallback lookup.
133
+
134
+ ### PERF-4. [LOW] `substitute_vars` creates merged `SubVarSet` per statement
135
+
136
+ `src/execsql/script/engine.py:778-783` — When `localvars` is not None, creates a new
137
+ `SubVarSet` with recompiled regex every statement. Could cache when local vars haven't changed.
138
+
139
+ ### PERF-5. [LOW] `set_dynamic_system_vars()` called per-statement
140
+
141
+ `src/execsql/script/engine.py:738-761` — 7 `add_substitution` calls per statement for values
142
+ that only change on CONFIG/AUTOCOMMIT metacommands. A dirty-flag approach would avoid overhead.
143
+
144
+ ### PERF-6. [LOW] `date_fmts` deque is shared module-level mutable
145
+
146
+ `src/execsql/types.py:184-204` — Currently harmless but would be a race condition under future
147
+ parallelism.
148
+
149
+ ### PERF-7. [LOW] Pretty-print materializes entire result set
150
+
151
+ `src/execsql/exporters/pretty.py:47-49` — `list(rows)` forces the entire streaming generator
152
+ into memory to compute column widths. A 1M-row result set could blow up memory.
153
+
154
+ ______________________________________________________________________
155
+
156
+ ## Test Gaps
157
+
158
+ ### F6. [HIGH] Two parsers with no equivalence tests
159
+
160
+ `src/execsql/script/engine.py:864-1012` (legacy) and `src/execsql/script/parser.py` (AST) —
161
+ Two independent parsers for the same grammar with no shared test verifying equivalent results.
162
+ The AST parser creates nested block nodes; the legacy parser treats IF/LOOP/BATCH as flat.
163
+
164
+ **Fix:** Add parametric tests running a script corpus through both parsers and asserting
165
+ structural equivalence.
166
+
167
+ ### F9. [HIGH] Core SQL execution unit tests are theater
168
+
169
+ `tests/test_engine.py:319-412` — `SqlStmt.run()` never tested with a real DB.
170
+ `MetacommandStmt.run()` patches the entire dispatch table. `CommandList` patches
171
+ `run_and_increment`. Error-recovery paths (`WriteSpec.write()`, `MailSpec.send()`) still have
172
+ zero integration test coverage.
173
+
174
+ **Fix:** Unit tests for `SqlStmt.run()` with real in-memory SQLite. Integration tests for
175
+ `ON ERROR_HALT WRITE` and `ON ERROR_HALT EMAIL` end-to-end.
176
+
177
+ ### F16. [MEDIUM] Coverage omissions exclude ~7.8K lines
178
+
179
+ `pyproject.toml:204-222` — Excludes gui/desktop.py, gui/tui.py, metacommands/prompt.py,
180
+ script/executor.py, all 7 DB adapters, all LSP modules. The 90% gate applies to ~27K of ~35K
181
+ lines.
182
+
183
+ ### TEST-1. [MEDIUM] No tests for `debug/repl.py`
184
+
185
+ REPL commands (`.vars`, `.set`, `.where`, `.stack`, ad-hoc SQL) have no dedicated tests.
186
+
187
+ ### TEST-5. [MEDIUM] Limited importer edge case coverage
188
+
189
+ CSV importer doesn't cover: inconsistent column counts, encoding errors, BOM markers, empty
190
+ files, header-only files.
191
+
192
+ ### TEST-2. [LOW] No tests for `gui/desktop.py` or `gui/tui.py` (excluded from coverage)
193
+
194
+ ### TEST-3. [LOW] No tests for `metacommands/prompt.py` (excluded from coverage)
195
+
196
+ ### TEST-4. [LOW] No tests for `db/dsn.py` (excluded from coverage)
197
+
198
+ ### TEST-6. [LOW] No `format.py` edge case tests
199
+
200
+ Doesn't cover: nested block comments, metacommands inside block comments, SQL strings
201
+ containing `-- !x!` patterns, very long lines.
202
+
203
+ ### TEST-7. [LOW] No property-based tests for parsers
204
+
205
+ `CondParser` and `NumericParser` handle arbitrary user input. Hypothesis-based testing would
206
+ catch edge cases.
207
+
208
+ ### TEST-8. [LOW] DB adapters have zero test coverage
209
+
210
+ `db/access.py`, `db/firebird.py`, `db/oracle.py`, `db/sqlserver.py` — all excluded from
211
+ coverage.
212
+
213
+ ______________________________________________________________________
214
+
215
+ ## Edge Cases & Robustness
216
+
217
+ ### EDGE-2. [LOW] `SubVarSet.substitute_all()` has no cycle depth limit
218
+
219
+ `src/execsql/script/variables.py:254-265` — `substitute_vars()` in engine.py has a 100-iteration
220
+ guard, but `substitute_all()` called directly (e.g., from config loading) has no guard.
221
+
222
+ ### EDGE-3. [LOW] `ScriptFile.__repr__` uses `super().filename` incorrectly
223
+
224
+ `src/execsql/script/engine.py:635` — Should be `self.filename`.
225
+
226
+ ### EDGE-5. [LOW] Block comment parsing doesn't handle nested comments
227
+
228
+ `src/execsql/script/engine.py:871-881` — Simple `in_block_cmt` flag; `/* outer /* inner */ still comment */` exits at first `*/`.
229
+
230
+ ### EDGE-6. [LOW] `SourceString.match_str()` parameter shadows builtin `str`
231
+
232
+ `src/execsql/parser.py:60` — Suppressed by ruff A002.
233
+
234
+ ### EDGE-7. [LOW] `_import_loop` may access unbound `line` variable
235
+
236
+ `src/execsql/db/base.py:565` — If `StopIteration` raised on first row, `line` would be
237
+ unbound. Guarded in practice by `len(b) > 0`.
238
+
239
+ ______________________________________________________________________
240
+
241
+ ## Divergences from Monolith
242
+
243
+ ### DIV-1. [LOW] `ScriptCmd` resolves `source_dir` at construction time
244
+
245
+ `src/execsql/script/engine.py:400-408` — Monolith resolved per-statement. New behavior is
246
+ arguably better. Not documented in `divergence.md`.
247
+
248
+ ### DIV-2. [MEDIUM] `$CURRENT_DATABASE`/`$CURRENT_DBMS` set differently
249
+
250
+ Static (per-connect) vs dynamic (per-statement) split means these aren't updated on `USE`.
251
+
252
+ ### DIV-3. [LOW] `DT_Long` maps to "hugeint" in SQLite
253
+
254
+ `src/execsql/types.py:742` — SQLite doesn't have "hugeint"; gets TEXT affinity.
255
+
256
+ ### DIV-4. [LOW] `DT_DuckDB` character types all map to TEXT
257
+
258
+ `src/execsql/types.py:761-763` — Loses VARCHAR(N) length constraints.
259
+
260
+ ### DIV-5. [LOW] Keyring stores password silently in GUI mode
261
+
262
+ `src/execsql/utils/auth.py:192-193` — Auto-stores without asking. Divergence doc mentions
263
+ keyring but not auto-store behavior.
264
+
265
+ ______________________________________________________________________
266
+
267
+ ## Residual Risks
268
+
269
+ 1. **Thread safety (F3)** is the largest residual risk. `from execsql import run` cannot be
270
+ used from multiple threads. Must be addressed before the library API is promoted.
271
+
272
+ 1. **Env var filter (F12 fix)** is a behavioral change — scripts relying on
273
+ `!!&AWS_SECRET_ACCESS_KEY!!` will silently get empty strings. No opt-out mechanism.
274
+
275
+ 1. **exec_cmd quoting (F8 fix)** — `quote_identifier("schema.myproc")` produces
276
+ `"schema.myproc"` (single identifier), not `"schema"."myproc"`. No worse than before but
277
+ could break schema-qualified function calls.
278
+
279
+ 1. **WriteSpec/MailSpec** error-recovery paths are now correct but still have zero dedicated
280
+ integration tests.
281
+
282
+ 1. **Bumpversion hook removal (F13 fix)** — if pre-commit hooks reject bump-generated content,
283
+ bumps will fail. Watch on next version bump.
284
+
285
+ ______________________________________________________________________
286
+
287
+ ## Feature Ideas
288
+
289
+ ### Quick wins
290
+
291
+ - `$TIMER_SECONDS` — numeric companion to `$TIMER` timedelta string
292
+ - `$CURRENT_DATE` — clean `YYYY-MM-DD` string (vs `$DATE_TAG`'s `YYYYMMDD`)
293
+ - `CONTINUE` in loops — only `BREAK` exists; users nest IF blocks as workaround
294
+
295
+ ### Medium features
296
+
297
+ - `FOR <var> IN <query|list>` loop — avoids `SUBDATA` + `WHILE` + string manipulation
298
+ - `RETRY N [BACKOFF s]` — transient DB error handling without verbose manual patterns
299
+ - Native webhook/HTTP notifications — `ON ERROR_HALT WEBHOOK` instead of `SYSTEM_CMD curl`
300
+ - `IMPORT FROM URL` — HTTP/REST import without temp file management
301
+ - Entry form validation enforcement — `validation_regex` fields exist but aren't enforced
302
+
303
+ ### Strategic features
304
+
305
+ - Textual TUI console — `CONSOLE ON` is a stub in the Textual backend
306
+ - Persistent state across runs — `~/.execsql/state.db` for run history, watermarks, checkpoints
307
+ - Thread-safe RuntimeContext (F3) — enables PARALLEL blocks, concurrent library API, easier testing
308
+ - AST migration completion (F6/F15) — foundational for LSP and long-term maintainability
309
+ - Plugin system via entry points — custom metacommands, community ecosystem
310
+ - LSP enhancements — autocomplete, hover docs, jump-to-definition, inline diagnostics
311
+
312
+ ______________________________________________________________________
313
+
314
+ ## Summary
315
+
316
+ | Category | High | Medium | Low | Total |
317
+ | ---------------------- | ----- | ------ | ------ | ------ |
318
+ | Architecture & Quality | 2 | 2 | 6 | 10 |
319
+ | Security | 0 | 1 | 2 | 3 |
320
+ | Performance | 0 | 1 | 4 | 5 |
321
+ | Test Gaps | 2 | 3 | 6 | 11 |
322
+ | Edge Cases | 0 | 0 | 5 | 5 |
323
+ | Divergences | 0 | 1 | 4 | 5 |
324
+ | **Total** | **4** | **8** | **27** | **39** |
@@ -13,6 +13,98 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.16.0] - 2026-04-29
17
+
18
+ ### Added
19
+
20
+ - `--parse-tree` CLI flag: parse a script into an Abstract Syntax Tree and print a visual tree structure showing block nesting (IF/LOOP/BATCH/SCRIPT), source line ranges, compound conditions (ANDIF/ORIF), and all metacommands. Requires no database connection or configuration.
21
+ - AST parser module (`execsql.script.parser`) with `parse_script()` and `parse_string()` entry points. Produces a structured `Script` tree with typed nodes for all block constructs (IfBlock, LoopBlock, BatchBlock, ScriptBlock, SqlBlock, IncludeDirective).
22
+ - AST node definitions (`execsql.script.ast`) with `format_tree()` for human-readable tree output.
23
+ - AST-based execution engine is now the default (and only) engine. Scripts are parsed into a tree of typed nodes, then walked for execution. INCLUDE'd files are parsed and executed natively with circular-include detection. Control flow (IF/LOOP/BATCH) is driven by tree structure.
24
+ - `active_context()` context manager in `execsql.state` for installing an isolated `RuntimeContext` as the active global context within a `with` block.
25
+ - Plugin system (`execsql.plugins`) for extending execsql with custom metacommands, export formats, and import formats via Python entry points. Entry point groups: `execsql.metacommands`, `execsql.exporters`, `execsql.importers`. Plugins are discovered automatically at startup.
26
+ - `--list-plugins` CLI flag to show all discovered plugins and exit.
27
+ - Python library API: `from execsql import run` for programmatic script execution from notebooks, pipelines, and applications. Returns a `ScriptResult` with success/failure, command count, timing, errors, and final variable state. Supports DSN connection strings, pre-existing connections, substitution variables, and error control. Full RuntimeContext isolation between calls.
28
+ - AST `Comment` node: the parser now preserves SQL comments in the tree. Consecutive single-line `--` comments are grouped into one node; `/* */` block comments are captured as single nodes. The `--parse-tree` output includes `<CMT>` tagged comment nodes.
29
+ - `--parse-tree` visual improvements: color-coded type tags (`<SQL>`, `<CMD>`, `<CMT>`, `<IF>`, `<LOOP>`, etc.), dimmed line numbers, and content truncation for cleaner output.
30
+ - Deprecation warning emitted when `enc_password` is used in config files, advising users to switch to keyring or environment variables.
31
+ - Sensitive environment variables (`*SECRET*`, `*TOKEN*`, `*PASSWORD*`, etc.) are now filtered from automatic substitution variable exposure.
32
+
33
+ ### Changed
34
+
35
+ - **Execution engine replaced.** The legacy flat command-list engine has been replaced by the AST-based executor. Scripts are now parsed into a tree of typed nodes and executed by walking the tree. INCLUDE'd files are parsed and executed natively with circular-include detection. All metacommands, SQL, and control flow work identically. This change is transparent to users.
36
+ - **BREAK outside LOOP is now an error.** `BREAK` outside a loop block now raises an error (exit 1) instead of being silently ignored. This catches script bugs that were previously unreported.
37
+ - `--lint` now uses the AST parser for structural validation. Unmatched IF/LOOP/BATCH/SCRIPT blocks are caught at parse time with precise source line ranges. No database connection or runtime state initialization is required. All prior lint checks (variable analysis, INCLUDE file existence, EXECUTE SCRIPT resolution, SUB_INI reading) are preserved.
38
+ - Export format dispatch logic (`EXPORT` and `EXPORT QUERY` metacommands) refactored from duplicated ~180-line if/elif chains into shared `_dispatch_format()` function, eliminating code duplication and fixing missing zip-compatibility checks for `EXPORT QUERY`.
39
+ - `MailSpec.send()` refactored: extracted `_expand()` helper to replace 12 repetitive substitution lines.
40
+
41
+ ### Fixed
42
+
43
+ - **[Critical]** `WriteSpec.write()` and `MailSpec.send()` error-recovery paths crashed because `SubVarSet.substitute_all()` returns `(str, bool)` but callers treated the return as a plain string. All 14 call sites now unpack the tuple correctly.
44
+ - **[Critical]** Error-recovery fallback in `WriteSpec.write()` and `io_write` called `.encode()` producing bytes passed to `sys.stdout.write()` which expects `str`. Removed the `.encode()` calls.
45
+ - `WriteSpec.write()` no longer crashes with `IndexError` when `commandliststack` is empty during early initialization errors.
46
+ - SQL injection vector in `exec_cmd()` across all 8 database adapters — stored procedure/function/view names are now quoted with `quote_identifier()`.
47
+ - `DSN` and `SQL Server` adapters no longer encode SQL strings to bytes before execution.
48
+ - Duplicate tuple entries in export format checks (`"txt-and"` and `"text-and"` each appeared twice).
49
+ - Database adapters now clear `self.password` after successful connection, reducing credential exposure window.
50
+ - Removed unused `_DEFAULT_CTX = RuntimeContext()` allocation in `state.py`.
51
+ - Version bump commits no longer skip pre-commit hooks (`--no-verify` removed from bumpversion config).
52
+ - `SubVarSet.substitute_all()` now enforces a 100-iteration depth limit to prevent infinite loops from cyclic variable references. The per-statement guard in the executor already had this protection, but direct callers (e.g. config loading) did not.
53
+ - `ConfigData.export_output_dir` is now declared in `__init__` with a default of `None` instead of being dynamically added in the CLI entry point.
54
+ - `Encrypt.ky` key table is now an immutable `MappingProxyType` instead of a mutable class-level dict.
55
+ - `JsonDatatype` attributes are now declared as class variables in the class body instead of assigned externally after class definition.
56
+ - `minimal_conf` test fixture expanded with commonly needed attributes (`import_encoding`, `script_encoding`, `export_output_dir`, `write_prefix`, `write_suffix`, `fold_col_hdrs`, `trim_col_hdrs`, etc.) to reduce ad-hoc attribute additions in individual tests.
57
+
58
+ ### Removed
59
+
60
+ - `--ast` / `--no-ast` CLI flag — the AST executor is now the only execution engine; no opt-out.
61
+ - Legacy flat command-list execution engine (`_parse_script_lines`, `read_sqlfile`, `read_sqlstring`, `runscripts`, `ScriptFile`, `CommandListWhileLoop`, `CommandListUntilLoop`, `ScriptExecSpec.execute()`).
62
+ - Legacy `_execute_script_direct()` function and `_execute_include_legacy()` fallback path.
63
+
64
+ ______________________________________________________________________
65
+
66
+ ## [2.15.11] - 2026-04-27
67
+
68
+ ### Fixed
69
+
70
+ - `PAUSE` console-mode fallback now checks `sys.platform` before attempting POSIX terminal imports, preventing hangs on Windows when stdin reports as a TTY.
71
+
72
+ ______________________________________________________________________
73
+
74
+ ## [2.15.10] - 2026-04-27
75
+
76
+ ### Added
77
+
78
+ - `--config FILE` CLI flag to specify an explicit configuration file. The file is loaded after the implicit search paths (system, user, script-dir, working-dir) so its values take precedence, while CLI arguments still override everything.
79
+ - `$HOSTNAME` system substitution variable — the network name of the machine running execsql, useful for log messages and environment detection.
80
+
81
+ ### Fixed
82
+
83
+ - Config file chaining no longer mutates a list during iteration; uses a deque for safe, predictable processing order.
84
+ - REPL `_use_color()` result is now cached instead of re-checking environment variables and TTY status on every colorized output.
85
+ - `DatabasePool.closeall()` no longer calls `self.__init__()` to reset state; fields are reset directly to avoid the re-initialization anti-pattern.
86
+ - `PAUSE` console mode no longer crashes on Windows CI due to unconditional `import termios`; POSIX-only imports are now guarded by the TTY fallback check.
87
+ - `HAS_ROWS()`, `ROW_COUNT_GT()`, `ROW_COUNT_GTE()`, `ROW_COUNT_EQ()`, and `ROW_COUNT_LT()` condition predicates now quote table names with standard SQL identifier quoting, preventing potential SQL injection when table names originate from substitution variables.
88
+ - Corrected `__init__.py` module docstring that incorrectly described the CLI entry point as `execsql2` (the command is `execsql`).
89
+ - Added note to configuration reference clarifying that `--output-dir` is a CLI-only option with no equivalent configuration file setting.
90
+
91
+ ______________________________________________________________________
92
+
93
+ ## [2.15.9] - 2026-04-27
94
+
95
+ ### Added
96
+
97
+ - Textual TUI now displays a progress bar and remaining-time countdown for `PROMPT PAUSE` and `PAUSE` dialogs when the `CONTINUE AFTER` or `HALT AFTER` keywords specify a timed duration (matching existing Tkinter behavior).
98
+
99
+ ### Fixed
100
+
101
+ - `PAUSE` metacommand in console mode (no `-v`) now responds to single keypresses (Enter to continue, Esc to quit) instead of requiring Enter after every key. Uses raw-mode terminal reading on POSIX and `msvcrt` polling on Windows.
102
+ - `PAUSE` with `CONTINUE AFTER`/`HALT AFTER` in console mode now displays a live SIGALRM-driven progress bar showing time remaining, matching the documented behavior and terminal screenshot.
103
+ - `PAUSE` progress bar output no longer bleeds into subsequent script output — the progress line is cleared before returning.
104
+ - Fixed double minutes-to-seconds conversion in the console `PAUSE` path that caused a 1-minute pause to sleep for 60 minutes.
105
+
106
+ ______________________________________________________________________
107
+
16
108
  ## [2.15.8] - 2026-04-20
17
109
 
18
110
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.15.8
3
+ Version: 2.16.0
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
@@ -237,9 +237,12 @@ execsql script.sql # read connection from config file
237
237
  | `--output-dir DIR` | Default base directory for EXPORT output files |
238
238
  | `--dry-run` | Parse the script and report commands without executing |
239
239
  | `--lint` | Static analysis: check structure and warn on issues (no DB) |
240
+ | `--parse-tree` | Print the script's AST structure and exit (no DB) |
241
+ | `--list-plugins` | List discovered plugins and exit |
240
242
  | `--ping` | Test database connectivity and exit |
241
243
  | `--profile` | Show per-statement timing summary after execution |
242
244
  | `--progress` | Show a progress bar for long-running IMPORT operations |
245
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
243
246
  | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
244
247
  | `--dump-keywords` | Print metacommand keywords as JSON and exit |
245
248
  | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
@@ -259,6 +262,61 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
259
262
  - Display query results in a GUI dialog; optionally prompt the user to select a row, enter a value, or submit a form.
260
263
  - Write status messages or tabular output to the console or a file during execution.
261
264
  - Automatically log each run, recording databases used, scripts executed, and user responses.
265
+ - Extend with custom metacommands, exporters, and importers via the plugin system.
266
+
267
+ # Library API
268
+
269
+ execsql can be used as a Python library for programmatic script execution:
270
+
271
+ ```python
272
+ from execsql import run
273
+
274
+ # Execute a script file
275
+ result = run(script="pipeline.sql", dsn="postgresql://user:pass@host/db")
276
+
277
+ # Execute inline SQL
278
+ result = run(
279
+ sql="CREATE TABLE t (id INT);\nINSERT INTO t VALUES (1);",
280
+ dsn="sqlite:///my.db",
281
+ new_db=True,
282
+ )
283
+
284
+ # With substitution variables
285
+ result = run(
286
+ script="etl.sql",
287
+ dsn="sqlite:///data.db",
288
+ variables={"SCHEMA": "public", "DATE": "2026-01-01"},
289
+ )
290
+
291
+ # Check results
292
+ print(result.success) # True
293
+ print(result.commands_run) # 2
294
+ print(result.elapsed) # 0.003 (seconds)
295
+ print(result.variables) # {"SCHEMA": "public", ...}
296
+ ```
297
+
298
+ Error handling:
299
+
300
+ ```python
301
+ result = run(sql="SELECT * FROM nonexistent;", dsn="sqlite:///:memory:")
302
+ if not result.success:
303
+ for err in result.errors:
304
+ print(f"{err.source}:{err.line}: {err.message}")
305
+
306
+ # Or raise on failure
307
+ result.raise_on_error() # raises ExecSqlError
308
+ ```
309
+
310
+ Use a pre-existing database connection instead of a DSN:
311
+
312
+ ```python
313
+ from execsql.db.factory import db_SQLite
314
+ conn = db_SQLite("my.db", new_db=True)
315
+ result = run(sql="SELECT 1;", connection=conn)
316
+ # run() does NOT close this connection — you manage its lifecycle
317
+ ```
318
+
319
+ Each call to `run()` uses an isolated `RuntimeContext`, so multiple calls do not share state.
262
320
 
263
321
  # An Illustration
264
322
 
@@ -115,9 +115,12 @@ execsql script.sql # read connection from config file
115
115
  | `--output-dir DIR` | Default base directory for EXPORT output files |
116
116
  | `--dry-run` | Parse the script and report commands without executing |
117
117
  | `--lint` | Static analysis: check structure and warn on issues (no DB) |
118
+ | `--parse-tree` | Print the script's AST structure and exit (no DB) |
119
+ | `--list-plugins` | List discovered plugins and exit |
118
120
  | `--ping` | Test database connectivity and exit |
119
121
  | `--profile` | Show per-statement timing summary after execution |
120
122
  | `--progress` | Show a progress bar for long-running IMPORT operations |
123
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
121
124
  | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
122
125
  | `--dump-keywords` | Print metacommand keywords as JSON and exit |
123
126
  | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
@@ -137,6 +140,61 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
137
140
  - Display query results in a GUI dialog; optionally prompt the user to select a row, enter a value, or submit a form.
138
141
  - Write status messages or tabular output to the console or a file during execution.
139
142
  - Automatically log each run, recording databases used, scripts executed, and user responses.
143
+ - Extend with custom metacommands, exporters, and importers via the plugin system.
144
+
145
+ # Library API
146
+
147
+ execsql can be used as a Python library for programmatic script execution:
148
+
149
+ ```python
150
+ from execsql import run
151
+
152
+ # Execute a script file
153
+ result = run(script="pipeline.sql", dsn="postgresql://user:pass@host/db")
154
+
155
+ # Execute inline SQL
156
+ result = run(
157
+ sql="CREATE TABLE t (id INT);\nINSERT INTO t VALUES (1);",
158
+ dsn="sqlite:///my.db",
159
+ new_db=True,
160
+ )
161
+
162
+ # With substitution variables
163
+ result = run(
164
+ script="etl.sql",
165
+ dsn="sqlite:///data.db",
166
+ variables={"SCHEMA": "public", "DATE": "2026-01-01"},
167
+ )
168
+
169
+ # Check results
170
+ print(result.success) # True
171
+ print(result.commands_run) # 2
172
+ print(result.elapsed) # 0.003 (seconds)
173
+ print(result.variables) # {"SCHEMA": "public", ...}
174
+ ```
175
+
176
+ Error handling:
177
+
178
+ ```python
179
+ result = run(sql="SELECT * FROM nonexistent;", dsn="sqlite:///:memory:")
180
+ if not result.success:
181
+ for err in result.errors:
182
+ print(f"{err.source}:{err.line}: {err.message}")
183
+
184
+ # Or raise on failure
185
+ result.raise_on_error() # raises ExecSqlError
186
+ ```
187
+
188
+ Use a pre-existing database connection instead of a DSN:
189
+
190
+ ```python
191
+ from execsql.db.factory import db_SQLite
192
+ conn = db_SQLite("my.db", new_db=True)
193
+ result = run(sql="SELECT 1;", connection=conn)
194
+ # run() does NOT close this connection — you manage its lifecycle
195
+ ```
196
+
197
+ Each call to `run()` uses an isolated `RuntimeContext`, so multiple calls do not share state.
140
198
 
141
199
  # An Illustration
142
200