execsql2 2.15.11__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 (321) hide show
  1. execsql2-2.16.0/AUDIT.md +324 -0
  2. {execsql2-2.15.11 → execsql2-2.16.0}/CHANGELOG.md +50 -0
  3. {execsql2-2.15.11 → execsql2-2.16.0}/PKG-INFO +58 -1
  4. {execsql2-2.15.11 → execsql2-2.16.0}/README.md +57 -0
  5. {execsql2-2.15.11 → execsql2-2.16.0}/docs/about/divergence.md +46 -4
  6. {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/index.md +21 -1
  7. {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/adding_metacommands.md +11 -3
  8. {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/architecture.md +31 -0
  9. {execsql2-2.15.11 → execsql2-2.16.0}/docs/getting-started/syntax.md +16 -0
  10. execsql2-2.16.0/extras/plugin-template/README.md +71 -0
  11. execsql2-2.16.0/extras/plugin-template/pyproject.toml +22 -0
  12. execsql2-2.16.0/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +96 -0
  13. execsql2-2.16.0/extras/plugin-template/tests/test_plugin.py.example +110 -0
  14. {execsql2-2.15.11 → execsql2-2.16.0}/pyproject.toml +5 -3
  15. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/__init__.py +4 -0
  16. execsql2-2.16.0/src/execsql/api.py +580 -0
  17. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/__init__.py +106 -0
  18. execsql2-2.16.0/src/execsql/cli/lint_ast.py +439 -0
  19. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/run.py +109 -101
  20. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/config.py +9 -0
  21. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/access.py +1 -0
  22. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/dsn.py +3 -2
  23. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/duckdb.py +1 -1
  24. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/factory.py +3 -0
  25. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/firebird.py +2 -1
  26. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/mysql.py +2 -1
  27. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/oracle.py +2 -1
  28. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/postgres.py +2 -1
  29. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/sqlite.py +1 -1
  30. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/sqlserver.py +3 -2
  31. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/base.py +6 -4
  32. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/delimited.py +11 -3
  33. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/pretty.py +9 -12
  34. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/__init__.py +3 -0
  35. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/connect.py +1 -1
  36. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/control.py +8 -14
  37. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/debug.py +6 -4
  38. execsql2-2.16.0/src/execsql/metacommands/io_export.py +325 -0
  39. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/io_fileops.py +7 -13
  40. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/io_write.py +1 -1
  41. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/script_ext.py +8 -5
  42. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/upsert.py +40 -0
  43. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/models.py +8 -12
  44. execsql2-2.16.0/src/execsql/plugins.py +414 -0
  45. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/script/__init__.py +36 -12
  46. execsql2-2.16.0/src/execsql/script/ast.py +562 -0
  47. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/script/engine.py +59 -368
  48. execsql2-2.16.0/src/execsql/script/executor.py +833 -0
  49. execsql2-2.16.0/src/execsql/script/parser.py +663 -0
  50. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/script/variables.py +11 -0
  51. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/state.py +55 -2
  52. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/crypto.py +14 -10
  53. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/errors.py +31 -8
  54. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/mail.py +15 -12
  55. {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_cli.py +163 -0
  56. {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_cli_run.py +18 -151
  57. {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_profile.py +4 -4
  58. {execsql2-2.15.11 → execsql2-2.16.0}/tests/conftest.py +16 -1
  59. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_db_adapters_mocked.py +3 -4
  60. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_dsn.py +5 -6
  61. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_base.py +15 -13
  62. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands.py +4 -3
  63. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_extended.py +8 -37
  64. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_fileops_extra.py +4 -20
  65. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_io.py +5 -21
  66. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_script_ext.py +5 -11
  67. {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/fixtures/control_flow.sql +5 -0
  68. execsql2-2.16.0/tests/scripts/fixtures/parse_only/parse_tree.sql +541 -0
  69. execsql2-2.16.0/tests/test_api.py +303 -0
  70. execsql2-2.16.0/tests/test_ast.py +552 -0
  71. execsql2-2.16.0/tests/test_ast_parser.py +829 -0
  72. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_engine.py +15 -630
  73. execsql2-2.16.0/tests/test_executor.py +939 -0
  74. execsql2-2.16.0/tests/test_plugins.py +213 -0
  75. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_script.py +8 -0
  76. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_errors_extra.py +4 -4
  77. {execsql2-2.15.11 → execsql2-2.16.0}/uv.lock +1 -1
  78. execsql2-2.15.11/src/execsql/metacommands/io_export.py +0 -523
  79. {execsql2-2.15.11 → execsql2-2.16.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  80. {execsql2-2.15.11 → execsql2-2.16.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  81. {execsql2-2.15.11 → execsql2-2.16.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  82. {execsql2-2.15.11 → execsql2-2.16.0}/.github/workflows/ci-cd.yml +0 -0
  83. {execsql2-2.15.11 → execsql2-2.16.0}/.gitignore +0 -0
  84. {execsql2-2.15.11 → execsql2-2.16.0}/.pre-commit-config.yaml +0 -0
  85. {execsql2-2.15.11 → execsql2-2.16.0}/.pre-commit-hooks.yaml +0 -0
  86. {execsql2-2.15.11 → execsql2-2.16.0}/.python-version +0 -0
  87. {execsql2-2.15.11 → execsql2-2.16.0}/.readthedocs.yaml +0 -0
  88. {execsql2-2.15.11 → execsql2-2.16.0}/CONTRIBUTING.md +0 -0
  89. {execsql2-2.15.11 → execsql2-2.16.0}/LICENSE.txt +0 -0
  90. {execsql2-2.15.11 → execsql2-2.16.0}/NOTICE +0 -0
  91. {execsql2-2.15.11 → execsql2-2.16.0}/SECURITY.md +0 -0
  92. {execsql2-2.15.11 → execsql2-2.16.0}/docs/about/contributors.md +0 -0
  93. {execsql2-2.15.11 → execsql2-2.16.0}/docs/about/copyright.md +0 -0
  94. {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/cli.md +0 -0
  95. {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/db.md +0 -0
  96. {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/exporters.md +0 -0
  97. {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/importers.md +0 -0
  98. {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/metacommands.md +0 -0
  99. {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/adding_db_adapters.md +0 -0
  100. {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/adding_exporters.md +0 -0
  101. {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/adding_importers.md +0 -0
  102. {execsql2-2.15.11 → execsql2-2.16.0}/docs/getting-started/installation.md +0 -0
  103. {execsql2-2.15.11 → execsql2-2.16.0}/docs/getting-started/requirements.md +0 -0
  104. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/debugging.md +0 -0
  105. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/documentation.md +0 -0
  106. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/encoding.md +0 -0
  107. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/examples.md +0 -0
  108. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/formatter.md +0 -0
  109. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/logging.md +0 -0
  110. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/sql_syntax.md +0 -0
  111. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/usage.md +0 -0
  112. {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/using_scripts.md +0 -0
  113. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/Compare_planets.png +0 -0
  114. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/actions.png +0 -0
  115. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/actions2.png +0 -0
  116. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/checkboxes.png +0 -0
  117. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/connect.b64 +0 -0
  118. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/connect.png +0 -0
  119. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/create_conf.png +0 -0
  120. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/data_error1_screenshot.jpg +0 -0
  121. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/entry_form.png +0 -0
  122. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/execsql_console.png +0 -0
  123. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/execsql_logo_01.png +0 -0
  124. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/fatals.png +0 -0
  125. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/logo_small.png +0 -0
  126. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/pause_terminal.png +0 -0
  127. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/pause_terminal_sm.b64 +0 -0
  128. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/pause_terminal_sm.png +0 -0
  129. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/prompt_compare.png +0 -0
  130. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/set_build_commands.jpg +0 -0
  131. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/unit_conversions.b64 +0 -0
  132. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/unit_conversions_029.png +0 -0
  133. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/unmatched.png +0 -0
  134. {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/vim_execsql_highlight.png +0 -0
  135. {execsql2-2.15.11 → execsql2-2.16.0}/docs/index.md +0 -0
  136. {execsql2-2.15.11 → execsql2-2.16.0}/docs/reference/configuration.md +0 -0
  137. {execsql2-2.15.11 → execsql2-2.16.0}/docs/reference/metacommands.md +0 -0
  138. {execsql2-2.15.11 → execsql2-2.16.0}/docs/reference/security.md +0 -0
  139. {execsql2-2.15.11 → execsql2-2.16.0}/docs/reference/substitution_vars.md +0 -0
  140. {execsql2-2.15.11 → execsql2-2.16.0}/extras/vscode-execsql/README.md +0 -0
  141. {execsql2-2.15.11 → execsql2-2.16.0}/extras/vscode-execsql/package.json +0 -0
  142. {execsql2-2.15.11 → execsql2-2.16.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  143. {execsql2-2.15.11 → execsql2-2.16.0}/justfile +0 -0
  144. {execsql2-2.15.11 → execsql2-2.16.0}/scripts/generate_vscode_grammar.py +0 -0
  145. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/__main__.py +0 -0
  146. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/dsn.py +0 -0
  147. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/help.py +0 -0
  148. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/lint.py +0 -0
  149. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/__init__.py +0 -0
  150. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/base.py +0 -0
  151. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/debug/__init__.py +0 -0
  152. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/debug/repl.py +0 -0
  153. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exceptions.py +0 -0
  154. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/__init__.py +0 -0
  155. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/duckdb.py +0 -0
  156. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/feather.py +0 -0
  157. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/html.py +0 -0
  158. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/json.py +0 -0
  159. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/latex.py +0 -0
  160. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/markdown.py +0 -0
  161. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/ods.py +0 -0
  162. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/parquet.py +0 -0
  163. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/protocol.py +0 -0
  164. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/raw.py +0 -0
  165. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/sqlite.py +0 -0
  166. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/templates.py +0 -0
  167. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/values.py +0 -0
  168. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/xls.py +0 -0
  169. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/xlsx.py +0 -0
  170. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/xml.py +0 -0
  171. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/yaml.py +0 -0
  172. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/zip.py +0 -0
  173. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/format.py +0 -0
  174. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/__init__.py +0 -0
  175. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/base.py +0 -0
  176. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/console.py +0 -0
  177. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/desktop.py +0 -0
  178. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/tui.py +0 -0
  179. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/__init__.py +0 -0
  180. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/base.py +0 -0
  181. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/csv.py +0 -0
  182. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/feather.py +0 -0
  183. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/json.py +0 -0
  184. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/ods.py +0 -0
  185. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/xls.py +0 -0
  186. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/conditions.py +0 -0
  187. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/data.py +0 -0
  188. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/dispatch.py +0 -0
  189. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/io.py +0 -0
  190. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/io_import.py +0 -0
  191. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/prompt.py +0 -0
  192. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/system.py +0 -0
  193. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/parser.py +0 -0
  194. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/py.typed +0 -0
  195. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/script/control.py +0 -0
  196. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/types.py +0 -0
  197. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/__init__.py +0 -0
  198. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/auth.py +0 -0
  199. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/datetime.py +0 -0
  200. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/fileio.py +0 -0
  201. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/gui.py +0 -0
  202. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/numeric.py +0 -0
  203. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/regex.py +0 -0
  204. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/strings.py +0 -0
  205. {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/timer.py +0 -0
  206. {execsql2-2.15.11 → execsql2-2.16.0}/templates/README.md +0 -0
  207. {execsql2-2.15.11 → execsql2-2.16.0}/templates/config_settings.sqlite +0 -0
  208. {execsql2-2.15.11 → execsql2-2.16.0}/templates/example_config_prompt.sql +0 -0
  209. {execsql2-2.15.11 → execsql2-2.16.0}/templates/execsql.conf +0 -0
  210. {execsql2-2.15.11 → execsql2-2.16.0}/templates/make_config_db.sql +0 -0
  211. {execsql2-2.15.11 → execsql2-2.16.0}/templates/md_compare.sql +0 -0
  212. {execsql2-2.15.11 → execsql2-2.16.0}/templates/md_glossary.sql +0 -0
  213. {execsql2-2.15.11 → execsql2-2.16.0}/templates/md_upsert.sql +0 -0
  214. {execsql2-2.15.11 → execsql2-2.16.0}/templates/pg_compare.sql +0 -0
  215. {execsql2-2.15.11 → execsql2-2.16.0}/templates/pg_glossary.sql +0 -0
  216. {execsql2-2.15.11 → execsql2-2.16.0}/templates/pg_upsert.sql +0 -0
  217. {execsql2-2.15.11 → execsql2-2.16.0}/templates/script_template.sql +0 -0
  218. {execsql2-2.15.11 → execsql2-2.16.0}/templates/ss_compare.sql +0 -0
  219. {execsql2-2.15.11 → execsql2-2.16.0}/templates/ss_glossary.sql +0 -0
  220. {execsql2-2.15.11 → execsql2-2.16.0}/templates/ss_upsert.sql +0 -0
  221. {execsql2-2.15.11 → execsql2-2.16.0}/tests/__init__.py +0 -0
  222. {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/__init__.py +0 -0
  223. {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_cli_e2e.py +0 -0
  224. {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_lint.py +0 -0
  225. {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_ping.py +0 -0
  226. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/__init__.py +0 -0
  227. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_base.py +0 -0
  228. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_duckdb.py +0 -0
  229. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_factory.py +0 -0
  230. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_postgres.py +0 -0
  231. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_sqlite.py +0 -0
  232. {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_sqlite_extra.py +0 -0
  233. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/__init__.py +0 -0
  234. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_db.py +0 -0
  235. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_delimited.py +0 -0
  236. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  237. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_exporters.py +0 -0
  238. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_feather.py +0 -0
  239. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_html_extended.py +0 -0
  240. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_html_latex.py +0 -0
  241. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_json.py +0 -0
  242. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_json_extended.py +0 -0
  243. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_latex_extended.py +0 -0
  244. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_markdown.py +0 -0
  245. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_ods.py +0 -0
  246. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_parquet.py +0 -0
  247. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_pretty_extended.py +0 -0
  248. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_raw_extended.py +0 -0
  249. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  250. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_templates.py +0 -0
  251. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_templates_extended.py +0 -0
  252. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_values_extended.py +0 -0
  253. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_xls_xlsx.py +0 -0
  254. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_xlsx.py +0 -0
  255. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_xml.py +0 -0
  256. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_yaml.py +0 -0
  257. {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_zip.py +0 -0
  258. {execsql2-2.15.11 → execsql2-2.16.0}/tests/gui/__init__.py +0 -0
  259. {execsql2-2.15.11 → execsql2-2.16.0}/tests/gui/test_backends.py +0 -0
  260. {execsql2-2.15.11 → execsql2-2.16.0}/tests/gui/test_compare_stats.py +0 -0
  261. {execsql2-2.15.11 → execsql2-2.16.0}/tests/gui/test_compute_row_diffs.py +0 -0
  262. {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/__init__.py +0 -0
  263. {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_base_extended.py +0 -0
  264. {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_csv_edge_cases.py +0 -0
  265. {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_csv_importer.py +0 -0
  266. {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_feather_importer.py +0 -0
  267. {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_json_importer.py +0 -0
  268. {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_ods_importer.py +0 -0
  269. {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_xls_importer.py +0 -0
  270. {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/__init__.py +0 -0
  271. {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/conftest.py +0 -0
  272. {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_dsn.py +0 -0
  273. {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_duckdb.py +0 -0
  274. {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_mysql.py +0 -0
  275. {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_postgres.py +0 -0
  276. {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_sqlite.py +0 -0
  277. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/__init__.py +0 -0
  278. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_assert.py +0 -0
  279. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_breakpoint.py +0 -0
  280. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_connect.py +0 -0
  281. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_io_export.py +0 -0
  282. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_io_import.py +0 -0
  283. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  284. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_data.py +0 -0
  285. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  286. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_system.py +0 -0
  287. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  288. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_pg_upsert.py +0 -0
  289. {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_row_count.py +0 -0
  290. {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/__init__.py +0 -0
  291. {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  292. {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/fixtures/smoke.sql +0 -0
  293. {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/test_sql_scripts.py +0 -0
  294. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_config.py +0 -0
  295. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_config_data.py +0 -0
  296. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_config_extended.py +0 -0
  297. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_debug_repl.py +0 -0
  298. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_error_messages.py +0 -0
  299. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_exceptions.py +0 -0
  300. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_format.py +0 -0
  301. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_mail.py +0 -0
  302. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_models.py +0 -0
  303. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_package.py +0 -0
  304. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_parser.py +0 -0
  305. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_registry.py +0 -0
  306. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_state.py +0 -0
  307. {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_types.py +0 -0
  308. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/__init__.py +0 -0
  309. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_auth.py +0 -0
  310. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_auth_extra.py +0 -0
  311. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_crypto.py +0 -0
  312. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_datetime.py +0 -0
  313. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_errors.py +0 -0
  314. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_fileio.py +0 -0
  315. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_fileio_extra.py +0 -0
  316. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_numeric.py +0 -0
  317. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_regex.py +0 -0
  318. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_strings.py +0 -0
  319. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_timer.py +0 -0
  320. {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_timer_extra.py +0 -0
  321. {execsql2-2.15.11 → execsql2-2.16.0}/zensical.toml +0 -0
@@ -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,56 @@ ______________________________________________________________________
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
+
16
66
  ## [2.15.11] - 2026-04-27
17
67
 
18
68
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.15.11
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,6 +237,8 @@ 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 |
@@ -260,6 +262,61 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
260
262
  - Display query results in a GUI dialog; optionally prompt the user to select a row, enter a value, or submit a form.
261
263
  - Write status messages or tabular output to the console or a file during execution.
262
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.
263
320
 
264
321
  # An Illustration
265
322
 
@@ -115,6 +115,8 @@ 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 |
@@ -138,6 +140,61 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
138
140
  - Display query results in a GUI dialog; optionally prompt the user to select a row, enter a value, or submit a form.
139
141
  - Write status messages or tabular output to the console or a file during execution.
140
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.
141
198
 
142
199
  # An Illustration
143
200
 
@@ -29,6 +29,8 @@ ______________________________________________________________________
29
29
  | `--profile-limit N` | Number of top statements to display in the `--profile` summary (default: 20). Remaining statements are counted and noted in the output footer. |
30
30
  | `--ping` | Test database connectivity and exit. Connects using the supplied connection parameters, queries for the server version, and prints a one-line success message (exit 0) or the error (exit 1). No script file argument is required. |
31
31
  | `--lint` | Parse the script and perform static analysis without connecting to a database. Reports unmatched IF/ENDIF, LOOP/END LOOP, and BEGIN BATCH/END BATCH blocks (errors); potentially undefined `!!$VAR!!` references (warnings); missing INCLUDE file targets (warnings); and unknown `EXECUTE SCRIPT` targets (warnings). Variable analysis uses two passes so definition order does not matter. The linter descends into named script blocks reached via `EXECUTE SCRIPT` / `EXEC SCRIPT` / `RUN SCRIPT`, reads `SUB_INI` INI files at lint time, recognizes `SUB_EMPTY` / `SUB_ADD` / `SUB_APPEND` / `SUBDATA` as definitions, suppresses false warnings for `$COUNTER_N`, and auto-discovers built-in system variables from the installed source. Exits 0 if no errors, 1 if errors found. |
32
+ | `--parse-tree` | Parse the script into an Abstract Syntax Tree and print a visual tree showing block nesting (IF/LOOP/BATCH/SCRIPT), source line ranges, compound conditions (ANDIF/ORIF), and all metacommands. Does not connect to a database or execute anything. Useful for understanding script structure and verifying the parser handles a script correctly. |
33
+ | `--list-plugins` | List all discovered plugins (metacommands, exporters, importers) from Python entry points and exit. Plugins extend execsql via `execsql.metacommands`, `execsql.exporters`, and `execsql.importers` entry point groups. |
32
34
 
33
35
  ### Export Formats
34
36
 
@@ -94,10 +96,11 @@ New options in `execsql.conf`:
94
96
 
95
97
  ### Authentication
96
98
 
97
- | Feature | Description |
98
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
99
- | OS keyring integration | When the `keyring` package is installed, passwords are stored in and retrieved from the OS credential store (macOS Keychain, Windows Credential Manager, Linux SecretService). |
100
- | Keyring retry on auth failure | If a stored password is rejected, the stale entry is deleted, the user is re-prompted, and the new password is saved automatically. |
99
+ | Feature | Description |
100
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
101
+ | OS keyring integration | When the `keyring` package is installed, passwords are stored in and retrieved from the OS credential store (macOS Keychain, Windows Credential Manager, Linux SecretService). |
102
+ | Keyring retry on auth failure | If a stored password is rejected, the stale entry is deleted, the user is re-prompted, and the new password is saved automatically. |
103
+ | Keyring auto-store in GUI mode | When a password is entered via a GUI prompt (Tkinter or Textual), it is stored to the OS keyring automatically without an explicit confirmation prompt. CLI password prompts behave the same way. |
101
104
 
102
105
  ### Logging Enhancements
103
106
 
@@ -158,6 +161,34 @@ execsql2 adds a full interactive debugging system that has no equivalent in upst
158
161
  - **ANSI color output** — the REPL uses ANSI color on TTY outputs: bold yellow for section labels, cyan for filenames and variable names, dim for separators and `=` signs, red for error messages, bold for SQL column headers, and dim italic for `NULL` values. Color is suppressed when `NO_COLOR` or `EXECSQL_NO_COLOR` environment variables are set, or when the output stream is not a TTY.
159
162
  - **Readline support** — on platforms where `readline` is available (macOS, Linux), the REPL supports arrow-key history navigation and line editing.
160
163
 
164
+ ### Library API
165
+
166
+ execsql2 provides a Python library API for programmatic script execution — no CLI needed. The upstream execsql has no equivalent.
167
+
168
+ ```python
169
+ from execsql import run
170
+
171
+ result = run(
172
+ script="pipeline.sql",
173
+ dsn="postgresql://user:pass@host/db",
174
+ variables={"SCHEMA": "public"},
175
+ )
176
+
177
+ print(result.success) # True/False
178
+ print(result.commands_run) # number of statements executed
179
+ print(result.errors) # list of ScriptError objects
180
+ print(result.variables) # final substitution variable state
181
+ ```
182
+
183
+ **Key features:**
184
+
185
+ - **DSN or connection** — pass a DSN string (`dsn="sqlite:///my.db"`) or a pre-existing `Database` object (`connection=conn`).
186
+ - **Substitution variables** — pass a `variables` dict; keys are automatically `$`-prefixed.
187
+ - **Error control** — `halt_on_error=True` (default) stops on the first error; `halt_on_error=False` captures errors and continues.
188
+ - **Isolation** — each `run()` call uses an isolated `RuntimeContext`. Multiple calls do not share state.
189
+ - **Result object** — `ScriptResult` is a frozen dataclass with `success`, `commands_run`, `elapsed`, `errors`, and `variables`.
190
+ - **Exception convenience** — call `result.raise_on_error()` to raise `ExecSqlError` if the script failed.
191
+
161
192
  ______________________________________________________________________
162
193
 
163
194
  ## Changed Behavior
@@ -192,6 +223,17 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
192
223
  - **`Database` is an ABC** — `open_db()` and `exec_cmd()` are abstract methods. Subclasses that omit them raise `TypeError` at instantiation instead of at call time.
193
224
  - **Connection timeouts** — PostgreSQL and SQLite adapters accept a connection timeout parameter (default 30 seconds).
194
225
  - **DuckDB temporal types** — `TIMESTAMPTZ`, `TIMESTAMP`, `DATE`, `TIME` now map to native DuckDB types instead of `TEXT`.
226
+ - **SQLite `DT_Long` mapping** — `DT_Long` maps to `"hugeint"` in the SQLite type table. SQLite does not have a native `HUGEINT` type; the value receives `TEXT` affinity. In practice this is harmless because SQLite's type affinity system handles large integers transparently, but the mapping name differs from upstream.
227
+
228
+ ### Execution Engine
229
+
230
+ The legacy flat command-list engine (`_parse_script_lines` / `runscripts` / `CommandList.run_next()`) has been replaced by an AST-based execution engine. Scripts are parsed into a tree of typed nodes, then the tree is walked for execution. This change is transparent to users — all metacommands, SQL, and control flow work identically.
231
+
232
+ - **Native INCLUDE handling** — The AST executor parses INCLUDE'd files with the AST parser and executes them through the tree-walking executor. Control flow structures (IF/LOOP/BATCH/SCRIPT) in included files are fully tree-driven, with correct deferred variable handling and source-span error reporting.
233
+ - **Circular INCLUDE detection** — The executor tracks the full include chain and detects circular references (e.g. A includes B includes A), reporting the full chain in the error message. Upstream has no such detection.
234
+ - **BREAK outside LOOP is an error** — `BREAK` outside a loop block now raises an error (exit 1) instead of being silently ignored. This catches script bugs that upstream would not report.
235
+ - **Instance-scoped script registry** — Named SCRIPT blocks are stored on the `RuntimeContext` instance instead of a module-level dict, preventing cross-execution contamination.
236
+ - **Script source directory resolution** — `ScriptCmd` resolves `source_dir` at construction time (when the command is parsed) rather than per-statement at execution time. This is functionally equivalent for all normal usage since the script file does not move between parse and execution.
195
237
 
196
238
  ### Error Handling
197
239
 
@@ -4,7 +4,27 @@ The pages in this section are auto-generated from the source docstrings and show
4
4
 
5
5
  If you want to **extend** execsql — add a new exporter format, support a new database, or add an importer for a file type — start with the Contributing guides, which give you step-by-step walkthroughs and copy-paste skeletons. The API pages here serve as the detailed reference those guides link to.
6
6
 
7
- For a high-level overview of how all the pieces fit together, start with the [Architecture & Design Guide](../dev/architecture.md).
7
+ For programmatic use, see the [Library API](#library-api) section below. For a high-level overview of how all the pieces fit together, start with the [Architecture & Design Guide](../dev/architecture.md).
8
+
9
+ ## Library API
10
+
11
+ The primary public API is `execsql.run()`:
12
+
13
+ ```python
14
+ from execsql import run, ScriptResult, ScriptError, ExecSqlError
15
+
16
+ result: ScriptResult = run(
17
+ script="pipeline.sql", # or sql="SELECT 1;"
18
+ dsn="sqlite:///my.db", # or connection=existing_db_object
19
+ variables={"KEY": "value"}, # optional substitution variables
20
+ halt_on_error=True, # stop on first error (default)
21
+ new_db=False, # create DB if missing
22
+ )
23
+ ```
24
+
25
+ See the [README](https://github.com/geocoug/execsql#library-api) for full examples.
26
+
27
+ ## Extension Guides
8
28
 
9
29
  | Extension type | Guide | API reference |
10
30
  | -------------------- | -------------------------------------------------------- | ------------------------------- |