execsql2 2.16.0__tar.gz → 2.16.2__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 → execsql2-2.16.2}/.gitignore +3 -2
  2. {execsql2-2.16.0 → execsql2-2.16.2}/CHANGELOG.md +36 -0
  3. {execsql2-2.16.0 → execsql2-2.16.2}/CONTRIBUTING.md +0 -20
  4. {execsql2-2.16.0 → execsql2-2.16.2}/PKG-INFO +38 -29
  5. {execsql2-2.16.0 → execsql2-2.16.2}/README.md +37 -28
  6. {execsql2-2.16.0 → execsql2-2.16.2}/docs/about/contributors.md +3 -0
  7. {execsql2-2.16.0 → execsql2-2.16.2}/docs/about/divergence.md +39 -33
  8. {execsql2-2.16.0 → execsql2-2.16.2}/docs/api/exporters.md +6 -0
  9. {execsql2-2.16.0 → execsql2-2.16.2}/docs/api/importers.md +2 -0
  10. {execsql2-2.16.0 → execsql2-2.16.2}/docs/api/index.md +24 -0
  11. {execsql2-2.16.0 → execsql2-2.16.2}/docs/dev/adding_metacommands.md +14 -10
  12. {execsql2-2.16.0 → execsql2-2.16.2}/docs/dev/architecture.md +21 -9
  13. {execsql2-2.16.0 → execsql2-2.16.2}/docs/getting-started/installation.md +2 -0
  14. {execsql2-2.16.0 → execsql2-2.16.2}/docs/getting-started/syntax.md +2 -1
  15. {execsql2-2.16.0 → execsql2-2.16.2}/docs/guides/examples.md +303 -76
  16. execsql2-2.16.2/docs/guides/logging.md +106 -0
  17. {execsql2-2.16.0 → execsql2-2.16.2}/docs/reference/configuration.md +1 -1
  18. {execsql2-2.16.0 → execsql2-2.16.2}/docs/reference/substitution_vars.md +12 -8
  19. {execsql2-2.16.0 → execsql2-2.16.2}/pyproject.toml +2 -2
  20. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/cli/run.py +333 -173
  21. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/config.py +1 -1
  22. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/script/executor.py +145 -47
  23. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/script/parser.py +9 -1
  24. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/state.py +72 -51
  25. {execsql2-2.16.0 → execsql2-2.16.2}/tests/cli/test_cli_run.py +173 -1
  26. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_config_data.py +2 -2
  27. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_config_extended.py +1 -1
  28. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_executor.py +209 -0
  29. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_plugins.py +122 -0
  30. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_state.py +108 -1
  31. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_strings.py +5 -0
  32. {execsql2-2.16.0 → execsql2-2.16.2}/uv.lock +1 -1
  33. {execsql2-2.16.0 → execsql2-2.16.2}/zensical.toml +40 -2
  34. execsql2-2.16.0/AUDIT.md +0 -324
  35. execsql2-2.16.0/docs/guides/logging.md +0 -103
  36. {execsql2-2.16.0 → execsql2-2.16.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  37. {execsql2-2.16.0 → execsql2-2.16.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  38. {execsql2-2.16.0 → execsql2-2.16.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  39. {execsql2-2.16.0 → execsql2-2.16.2}/.github/workflows/ci-cd.yml +0 -0
  40. {execsql2-2.16.0 → execsql2-2.16.2}/.pre-commit-config.yaml +0 -0
  41. {execsql2-2.16.0 → execsql2-2.16.2}/.pre-commit-hooks.yaml +0 -0
  42. {execsql2-2.16.0 → execsql2-2.16.2}/.python-version +0 -0
  43. {execsql2-2.16.0 → execsql2-2.16.2}/.readthedocs.yaml +0 -0
  44. {execsql2-2.16.0 → execsql2-2.16.2}/LICENSE.txt +0 -0
  45. {execsql2-2.16.0 → execsql2-2.16.2}/NOTICE +0 -0
  46. {execsql2-2.16.0 → execsql2-2.16.2}/SECURITY.md +0 -0
  47. {execsql2-2.16.0 → execsql2-2.16.2}/docs/about/copyright.md +0 -0
  48. {execsql2-2.16.0 → execsql2-2.16.2}/docs/api/cli.md +0 -0
  49. {execsql2-2.16.0 → execsql2-2.16.2}/docs/api/db.md +0 -0
  50. {execsql2-2.16.0 → execsql2-2.16.2}/docs/api/metacommands.md +0 -0
  51. {execsql2-2.16.0 → execsql2-2.16.2}/docs/dev/adding_db_adapters.md +0 -0
  52. {execsql2-2.16.0 → execsql2-2.16.2}/docs/dev/adding_exporters.md +0 -0
  53. {execsql2-2.16.0 → execsql2-2.16.2}/docs/dev/adding_importers.md +0 -0
  54. {execsql2-2.16.0 → execsql2-2.16.2}/docs/getting-started/requirements.md +0 -0
  55. {execsql2-2.16.0 → execsql2-2.16.2}/docs/guides/debugging.md +0 -0
  56. {execsql2-2.16.0 → execsql2-2.16.2}/docs/guides/documentation.md +0 -0
  57. {execsql2-2.16.0 → execsql2-2.16.2}/docs/guides/encoding.md +0 -0
  58. {execsql2-2.16.0 → execsql2-2.16.2}/docs/guides/formatter.md +0 -0
  59. {execsql2-2.16.0 → execsql2-2.16.2}/docs/guides/sql_syntax.md +0 -0
  60. {execsql2-2.16.0 → execsql2-2.16.2}/docs/guides/usage.md +0 -0
  61. {execsql2-2.16.0 → execsql2-2.16.2}/docs/guides/using_scripts.md +0 -0
  62. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/Compare_planets.png +0 -0
  63. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/actions.png +0 -0
  64. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/actions2.png +0 -0
  65. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/checkboxes.png +0 -0
  66. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/connect.b64 +0 -0
  67. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/connect.png +0 -0
  68. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/create_conf.png +0 -0
  69. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/data_error1_screenshot.jpg +0 -0
  70. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/entry_form.png +0 -0
  71. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/execsql_console.png +0 -0
  72. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/execsql_logo_01.png +0 -0
  73. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/fatals.png +0 -0
  74. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/logo_small.png +0 -0
  75. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/pause_terminal.png +0 -0
  76. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/pause_terminal_sm.b64 +0 -0
  77. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/pause_terminal_sm.png +0 -0
  78. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/prompt_compare.png +0 -0
  79. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/set_build_commands.jpg +0 -0
  80. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/unit_conversions.b64 +0 -0
  81. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/unit_conversions_029.png +0 -0
  82. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/unmatched.png +0 -0
  83. {execsql2-2.16.0 → execsql2-2.16.2}/docs/images/vim_execsql_highlight.png +0 -0
  84. {execsql2-2.16.0 → execsql2-2.16.2}/docs/index.md +0 -0
  85. {execsql2-2.16.0 → execsql2-2.16.2}/docs/reference/metacommands.md +0 -0
  86. {execsql2-2.16.0 → execsql2-2.16.2}/docs/reference/security.md +0 -0
  87. {execsql2-2.16.0 → execsql2-2.16.2}/extras/plugin-template/README.md +0 -0
  88. {execsql2-2.16.0 → execsql2-2.16.2}/extras/plugin-template/pyproject.toml +0 -0
  89. {execsql2-2.16.0 → execsql2-2.16.2}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  90. {execsql2-2.16.0 → execsql2-2.16.2}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  91. {execsql2-2.16.0 → execsql2-2.16.2}/extras/vscode-execsql/README.md +0 -0
  92. {execsql2-2.16.0 → execsql2-2.16.2}/extras/vscode-execsql/package.json +0 -0
  93. {execsql2-2.16.0 → execsql2-2.16.2}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  94. {execsql2-2.16.0 → execsql2-2.16.2}/justfile +0 -0
  95. {execsql2-2.16.0 → execsql2-2.16.2}/scripts/generate_vscode_grammar.py +0 -0
  96. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/__init__.py +0 -0
  97. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/__main__.py +0 -0
  98. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/api.py +0 -0
  99. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/cli/__init__.py +0 -0
  100. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/cli/dsn.py +0 -0
  101. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/cli/help.py +0 -0
  102. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/cli/lint.py +0 -0
  103. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/cli/lint_ast.py +0 -0
  104. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/__init__.py +0 -0
  105. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/access.py +0 -0
  106. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/base.py +0 -0
  107. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/dsn.py +0 -0
  108. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/duckdb.py +0 -0
  109. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/factory.py +0 -0
  110. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/firebird.py +0 -0
  111. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/mysql.py +0 -0
  112. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/oracle.py +0 -0
  113. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/postgres.py +0 -0
  114. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/sqlite.py +0 -0
  115. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/db/sqlserver.py +0 -0
  116. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/debug/__init__.py +0 -0
  117. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/debug/repl.py +0 -0
  118. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exceptions.py +0 -0
  119. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/__init__.py +0 -0
  120. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/base.py +0 -0
  121. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/delimited.py +0 -0
  122. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/duckdb.py +0 -0
  123. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/feather.py +0 -0
  124. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/html.py +0 -0
  125. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/json.py +0 -0
  126. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/latex.py +0 -0
  127. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/markdown.py +0 -0
  128. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/ods.py +0 -0
  129. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/parquet.py +0 -0
  130. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/pretty.py +0 -0
  131. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/protocol.py +0 -0
  132. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/raw.py +0 -0
  133. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/sqlite.py +0 -0
  134. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/templates.py +0 -0
  135. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/values.py +0 -0
  136. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/xls.py +0 -0
  137. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/xlsx.py +0 -0
  138. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/xml.py +0 -0
  139. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/yaml.py +0 -0
  140. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/exporters/zip.py +0 -0
  141. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/format.py +0 -0
  142. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/gui/__init__.py +0 -0
  143. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/gui/base.py +0 -0
  144. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/gui/console.py +0 -0
  145. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/gui/desktop.py +0 -0
  146. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/gui/tui.py +0 -0
  147. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/importers/__init__.py +0 -0
  148. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/importers/base.py +0 -0
  149. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/importers/csv.py +0 -0
  150. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/importers/feather.py +0 -0
  151. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/importers/json.py +0 -0
  152. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/importers/ods.py +0 -0
  153. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/importers/xls.py +0 -0
  154. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/__init__.py +0 -0
  155. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/conditions.py +0 -0
  156. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/connect.py +0 -0
  157. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/control.py +0 -0
  158. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/data.py +0 -0
  159. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/debug.py +0 -0
  160. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/dispatch.py +0 -0
  161. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/io.py +0 -0
  162. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/io_export.py +0 -0
  163. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/io_fileops.py +0 -0
  164. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/io_import.py +0 -0
  165. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/io_write.py +0 -0
  166. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/prompt.py +0 -0
  167. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/script_ext.py +0 -0
  168. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/system.py +0 -0
  169. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/metacommands/upsert.py +0 -0
  170. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/models.py +0 -0
  171. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/parser.py +0 -0
  172. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/plugins.py +0 -0
  173. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/py.typed +0 -0
  174. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/script/__init__.py +0 -0
  175. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/script/ast.py +0 -0
  176. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/script/control.py +0 -0
  177. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/script/engine.py +0 -0
  178. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/script/variables.py +0 -0
  179. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/types.py +0 -0
  180. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/__init__.py +0 -0
  181. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/auth.py +0 -0
  182. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/crypto.py +0 -0
  183. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/datetime.py +0 -0
  184. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/errors.py +0 -0
  185. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/fileio.py +0 -0
  186. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/gui.py +0 -0
  187. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/mail.py +0 -0
  188. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/numeric.py +0 -0
  189. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/regex.py +0 -0
  190. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/strings.py +0 -0
  191. {execsql2-2.16.0 → execsql2-2.16.2}/src/execsql/utils/timer.py +0 -0
  192. {execsql2-2.16.0 → execsql2-2.16.2}/templates/README.md +0 -0
  193. {execsql2-2.16.0 → execsql2-2.16.2}/templates/config_settings.sqlite +0 -0
  194. {execsql2-2.16.0 → execsql2-2.16.2}/templates/example_config_prompt.sql +0 -0
  195. {execsql2-2.16.0 → execsql2-2.16.2}/templates/execsql.conf +0 -0
  196. {execsql2-2.16.0 → execsql2-2.16.2}/templates/make_config_db.sql +0 -0
  197. {execsql2-2.16.0 → execsql2-2.16.2}/templates/md_compare.sql +0 -0
  198. {execsql2-2.16.0 → execsql2-2.16.2}/templates/md_glossary.sql +0 -0
  199. {execsql2-2.16.0 → execsql2-2.16.2}/templates/md_upsert.sql +0 -0
  200. {execsql2-2.16.0 → execsql2-2.16.2}/templates/pg_compare.sql +0 -0
  201. {execsql2-2.16.0 → execsql2-2.16.2}/templates/pg_glossary.sql +0 -0
  202. {execsql2-2.16.0 → execsql2-2.16.2}/templates/pg_upsert.sql +0 -0
  203. {execsql2-2.16.0 → execsql2-2.16.2}/templates/script_template.sql +0 -0
  204. {execsql2-2.16.0 → execsql2-2.16.2}/templates/ss_compare.sql +0 -0
  205. {execsql2-2.16.0 → execsql2-2.16.2}/templates/ss_glossary.sql +0 -0
  206. {execsql2-2.16.0 → execsql2-2.16.2}/templates/ss_upsert.sql +0 -0
  207. {execsql2-2.16.0 → execsql2-2.16.2}/tests/__init__.py +0 -0
  208. {execsql2-2.16.0 → execsql2-2.16.2}/tests/cli/__init__.py +0 -0
  209. {execsql2-2.16.0 → execsql2-2.16.2}/tests/cli/test_cli.py +0 -0
  210. {execsql2-2.16.0 → execsql2-2.16.2}/tests/cli/test_cli_e2e.py +0 -0
  211. {execsql2-2.16.0 → execsql2-2.16.2}/tests/cli/test_lint.py +0 -0
  212. {execsql2-2.16.0 → execsql2-2.16.2}/tests/cli/test_ping.py +0 -0
  213. {execsql2-2.16.0 → execsql2-2.16.2}/tests/cli/test_profile.py +0 -0
  214. {execsql2-2.16.0 → execsql2-2.16.2}/tests/conftest.py +0 -0
  215. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/__init__.py +0 -0
  216. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/test_base.py +0 -0
  217. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/test_db_adapters_mocked.py +0 -0
  218. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/test_dsn.py +0 -0
  219. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/test_duckdb.py +0 -0
  220. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/test_factory.py +0 -0
  221. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/test_postgres.py +0 -0
  222. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/test_sqlite.py +0 -0
  223. {execsql2-2.16.0 → execsql2-2.16.2}/tests/db/test_sqlite_extra.py +0 -0
  224. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/__init__.py +0 -0
  225. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_base.py +0 -0
  226. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_db.py +0 -0
  227. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_delimited.py +0 -0
  228. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_duckdb_exporter.py +0 -0
  229. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_exporters.py +0 -0
  230. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_feather.py +0 -0
  231. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_html_extended.py +0 -0
  232. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_html_latex.py +0 -0
  233. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_json.py +0 -0
  234. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_json_extended.py +0 -0
  235. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_latex_extended.py +0 -0
  236. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_markdown.py +0 -0
  237. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_ods.py +0 -0
  238. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_parquet.py +0 -0
  239. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_pretty_extended.py +0 -0
  240. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_raw_extended.py +0 -0
  241. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_sqlite_exporter.py +0 -0
  242. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_templates.py +0 -0
  243. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_templates_extended.py +0 -0
  244. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_values_extended.py +0 -0
  245. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_xls_xlsx.py +0 -0
  246. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_xlsx.py +0 -0
  247. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_xml.py +0 -0
  248. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_yaml.py +0 -0
  249. {execsql2-2.16.0 → execsql2-2.16.2}/tests/exporters/test_zip.py +0 -0
  250. {execsql2-2.16.0 → execsql2-2.16.2}/tests/gui/__init__.py +0 -0
  251. {execsql2-2.16.0 → execsql2-2.16.2}/tests/gui/test_backends.py +0 -0
  252. {execsql2-2.16.0 → execsql2-2.16.2}/tests/gui/test_compare_stats.py +0 -0
  253. {execsql2-2.16.0 → execsql2-2.16.2}/tests/gui/test_compute_row_diffs.py +0 -0
  254. {execsql2-2.16.0 → execsql2-2.16.2}/tests/importers/__init__.py +0 -0
  255. {execsql2-2.16.0 → execsql2-2.16.2}/tests/importers/test_base_extended.py +0 -0
  256. {execsql2-2.16.0 → execsql2-2.16.2}/tests/importers/test_csv_edge_cases.py +0 -0
  257. {execsql2-2.16.0 → execsql2-2.16.2}/tests/importers/test_csv_importer.py +0 -0
  258. {execsql2-2.16.0 → execsql2-2.16.2}/tests/importers/test_feather_importer.py +0 -0
  259. {execsql2-2.16.0 → execsql2-2.16.2}/tests/importers/test_json_importer.py +0 -0
  260. {execsql2-2.16.0 → execsql2-2.16.2}/tests/importers/test_ods_importer.py +0 -0
  261. {execsql2-2.16.0 → execsql2-2.16.2}/tests/importers/test_xls_importer.py +0 -0
  262. {execsql2-2.16.0 → execsql2-2.16.2}/tests/integration/__init__.py +0 -0
  263. {execsql2-2.16.0 → execsql2-2.16.2}/tests/integration/conftest.py +0 -0
  264. {execsql2-2.16.0 → execsql2-2.16.2}/tests/integration/test_dsn.py +0 -0
  265. {execsql2-2.16.0 → execsql2-2.16.2}/tests/integration/test_duckdb.py +0 -0
  266. {execsql2-2.16.0 → execsql2-2.16.2}/tests/integration/test_mysql.py +0 -0
  267. {execsql2-2.16.0 → execsql2-2.16.2}/tests/integration/test_postgres.py +0 -0
  268. {execsql2-2.16.0 → execsql2-2.16.2}/tests/integration/test_sqlite.py +0 -0
  269. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/__init__.py +0 -0
  270. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_assert.py +0 -0
  271. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_breakpoint.py +0 -0
  272. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_connect.py +0 -0
  273. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_io_export.py +0 -0
  274. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_io_import.py +0 -0
  275. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands.py +0 -0
  276. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_connect.py +0 -0
  277. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_data.py +0 -0
  278. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_extended.py +0 -0
  279. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  280. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_io.py +0 -0
  281. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  282. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  283. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_system.py +0 -0
  284. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  285. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_pg_upsert.py +0 -0
  286. {execsql2-2.16.0 → execsql2-2.16.2}/tests/metacommands/test_row_count.py +0 -0
  287. {execsql2-2.16.0 → execsql2-2.16.2}/tests/scripts/__init__.py +0 -0
  288. {execsql2-2.16.0 → execsql2-2.16.2}/tests/scripts/fixtures/control_flow.sql +0 -0
  289. {execsql2-2.16.0 → execsql2-2.16.2}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  290. {execsql2-2.16.0 → execsql2-2.16.2}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  291. {execsql2-2.16.0 → execsql2-2.16.2}/tests/scripts/fixtures/smoke.sql +0 -0
  292. {execsql2-2.16.0 → execsql2-2.16.2}/tests/scripts/test_sql_scripts.py +0 -0
  293. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_api.py +0 -0
  294. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_ast.py +0 -0
  295. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_ast_parser.py +0 -0
  296. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_config.py +0 -0
  297. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_debug_repl.py +0 -0
  298. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_engine.py +0 -0
  299. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_error_messages.py +0 -0
  300. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_exceptions.py +0 -0
  301. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_format.py +0 -0
  302. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_mail.py +0 -0
  303. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_models.py +0 -0
  304. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_package.py +0 -0
  305. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_parser.py +0 -0
  306. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_registry.py +0 -0
  307. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_script.py +0 -0
  308. {execsql2-2.16.0 → execsql2-2.16.2}/tests/test_types.py +0 -0
  309. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/__init__.py +0 -0
  310. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_auth.py +0 -0
  311. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_auth_extra.py +0 -0
  312. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_crypto.py +0 -0
  313. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_datetime.py +0 -0
  314. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_errors.py +0 -0
  315. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_errors_extra.py +0 -0
  316. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_fileio.py +0 -0
  317. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_fileio_extra.py +0 -0
  318. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_numeric.py +0 -0
  319. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_regex.py +0 -0
  320. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_timer.py +0 -0
  321. {execsql2-2.16.0 → execsql2-2.16.2}/tests/utils/test_timer_extra.py +0 -0
@@ -46,6 +46,7 @@ _execsql/
46
46
 
47
47
  # Claude
48
48
  .claude/
49
- ANALYSIS.md
50
- FINDINGS.md
51
49
  CLAUDE.md
50
+ AUDIT.md
51
+ DOC_AUDIT.md
52
+ PROMPTS.md
@@ -13,6 +13,28 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.16.2] - 2026-04-30
17
+
18
+ ### Fixed
19
+
20
+ - INCLUDE with quoted paths (e.g., `-- !x! INCLUDE "!!path!!/file.sql"`) now strips the surrounding quotes before resolving the file path. The AST parser captured the full target text including quotes, but the legacy dispatch regex stripped them — quoted INCLUDE paths would fail with "File does not exist" even when the file was present on disk.
21
+
22
+ ______________________________________________________________________
23
+
24
+ ## [2.16.1] - 2026-04-30
25
+
26
+ ### Fixed
27
+
28
+ - AST executor now correctly handles `~` (local) and `+` (outer-scope) substitution variables inside SCRIPT blocks. Previously, `-- !x! SUB ~var value` inside a SCRIPT body would write to a disconnected scope, causing the variable to be invisible to subsequent SQL statements and producing spurious "potential un-substituted variable" warnings. The fix pushes proper `CommandList` frames onto `commandliststack` at script and top-level boundaries, bridging the AST executor with the legacy metacommand handlers (`x_sub`, `x_rm_sub`, `xf_sub_defined`, `SUB_LOCAL`, prompt handlers, REPL `.vars`/`.stack`, etc.).
29
+ - EXECUTE SCRIPT argument expressions (e.g., `val=!!#parent_param!!`) are now expanded in the caller's scope before the child script frame is created, fixing nested script calls that pass `~` or `#` variables as arguments.
30
+
31
+ ### Changed
32
+
33
+ - `RuntimeContext` is now stored in `threading.local()` instead of a module-level global, making `_state.foo` access thread-safe. Each thread gets its own isolated context via lazy initialization. The `active_context` context manager is now safe for concurrent use across threads. Enables future PARALLEL blocks and concurrent `from execsql import run` calls.
34
+ - `_run()` in `cli/run.py` decomposed into 8 standalone functions: `_seed_early_subvars()`, `_load_config()`, `_seed_script_subvars()`, `_load_script()`, `_apply_dsn()`, `_apply_cli_options()`, `_route_positionals()`, and `_setup_logging()`. Reduces `_run()` from ~380 lines to ~150 lines of orchestration with zero behavioral change.
35
+
36
+ ______________________________________________________________________
37
+
16
38
  ## [2.16.0] - 2026-04-29
17
39
 
18
40
  ### Added
@@ -37,6 +59,7 @@ ______________________________________________________________________
37
59
  - `--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
60
  - 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
61
  - `MailSpec.send()` refactored: extracted `_expand()` helper to replace 12 repetitive substitution lines.
62
+ - Default database type changed from Access (`-t a`) to SQLite (`-t l`). Upstream defaulted to Access, which requires Windows and pyodbc. Users targeting Access databases should pass `-t a` explicitly.
40
63
 
41
64
  ### Fixed
42
65
 
@@ -55,6 +78,19 @@ ______________________________________________________________________
55
78
  - `JsonDatatype` attributes are now declared as class variables in the class body instead of assigned externally after class definition.
56
79
  - `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
80
 
81
+ ### Documentation
82
+
83
+ - Fixed false `$ENV:` prefix claim in substitution variables reference — feature does not exist.
84
+ - Documented environment variable filtering (SECRET, TOKEN, PASSWORD, etc.) in substitution variables reference.
85
+ - Added missing exporter API docs (markdown, yaml, xlsx) and importer API docs (json).
86
+ - Added 8 missing CLI flags to README Options table (-b, -e, -g, -i, -o, -s, -y, -z).
87
+ - Added missing installation extras ([upsert], [firebird], [oracle]) to README and installation guide.
88
+ - Fixed broken `PROMPT.md` link in logging guide.
89
+ - Added explicit `{ #exampleN }` anchors to all 34 examples for reliable cross-referencing.
90
+ - Updated architecture doc: corrected metacommand count (~225), export format count (20+), added debug/notebook/server/lsp packages to module map.
91
+ - Updated metacommand developer guide to reflect io.py split into io_export.py, io_import.py, io_write.py, io_fileops.py.
92
+ - Noted SQLite as the default database type in syntax reference.
93
+
58
94
  ### Removed
59
95
 
60
96
  - `--ast` / `--no-ast` CLI flag — the AST executor is now the only execution engine; no opt-out.
@@ -24,26 +24,6 @@ uv tool install rust-just # via uv
24
24
 
25
25
  Run `just` with no arguments to list all available recipes.
26
26
 
27
- ```
28
- just sync # Sync dependencies from lockfile
29
- just update-hooks # Update pre-commit hooks to latest versions
30
-
31
- just lint # Run ruff linter + formatter
32
- just pre-commit # Run all pre-commit hooks against every file
33
- just test # Run tests against the active Python version
34
- just test-all # Run tests across all supported Python versions (3.10–3.13)
35
- just clean # Remove build artifacts and caches
36
-
37
- just docs # Build the documentation site
38
- just docs-serve # Serve docs locally at http://127.0.0.1:8000
39
-
40
- just bump # Show available version bumps from current version
41
- just bump-patch # Bump patch version (e.g. 2.0.0 → 2.0.1)
42
- just bump-minor # Bump minor version (e.g. 2.0.0 → 2.1.0)
43
- just bump-major # Bump major version (e.g. 2.0.0 → 3.0.0)
44
- just bump-pre 2.0.0a1 # Tag a specific pre-release version
45
- ```
46
-
47
27
  ## Code Quality
48
28
 
49
29
  Linting and formatting are handled by [ruff](https://docs.astral.sh/ruff/):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.16.0
3
+ Version: 2.16.2
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
@@ -173,7 +173,8 @@ pip install execsql2[oracle] # Oracle (oracledb)
173
173
  pip install execsql2[odbc] # ODBC DSN (pyodbc)
174
174
 
175
175
  # Feature bundles
176
- pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
176
+ pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
177
+ pip install execsql2[upsert] # pg-upsert for PostgreSQL upsert operations
177
178
  pip install execsql2[auth] # OS keyring integration
178
179
  pip install execsql2[auth-plaintext] # Keyring + plaintext file backend (headless Linux)
179
180
  pip install execsql2[auth-encrypted] # Keyring + encrypted file backend (headless Linux)
@@ -219,33 +220,41 @@ execsql script.sql # read connection from config file
219
220
 
220
221
  ## Options
221
222
 
222
- | Flag | Description |
223
- | ----------------------------------- | --------------------------------------------------------------- |
224
- | `-t {p,m,s,l,k,a,f,o,d}` | Database type |
225
- | `-u USER` | Database username |
226
- | `-p PORT` | Server port |
227
- | `-a VALUE` | Set substitution variable `$ARG_x` |
228
- | `-c SCRIPT` | Execute inline SQL or metacommand string |
229
- | `-d` | Auto-create export directories |
230
- | `-f ENCODING` | Script file encoding (default: UTF-8) |
231
- | `-l` | Write run log to `~/execsql.log` |
232
- | `-m` | List metacommands and exit |
233
- | `-n` | Create a new SQLite or PostgreSQL database if it does not exist |
234
- | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
235
- | `-w` | Skip password prompt when a username is supplied |
236
- | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
237
- | `--output-dir DIR` | Default base directory for EXPORT output files |
238
- | `--dry-run` | Parse the script and report commands without executing |
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 |
242
- | `--ping` | Test database connectivity and exit |
243
- | `--profile` | Show per-statement timing summary after execution |
244
- | `--progress` | Show a progress bar for long-running IMPORT operations |
245
- | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
246
- | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
247
- | `--dump-keywords` | Print metacommand keywords as JSON and exit |
248
- | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
223
+ | Flag | Description |
224
+ | ------------------------------------- | ---------------------------------------------------------------- |
225
+ | `-t {p,m,s,l,k,a,f,o,d}` | Database type |
226
+ | `-u USER` | Database username |
227
+ | `-p PORT` | Server port |
228
+ | `-a VALUE` | Set substitution variable `$ARG_x` |
229
+ | `-b` / `--boolean-int` | Treat integers 0 and 1 as boolean values |
230
+ | `-c SCRIPT` | Execute inline SQL or metacommand string |
231
+ | `-d` | Auto-create export directories |
232
+ | `-e ENCODING` / `--database-encoding` | Character encoding used in the database |
233
+ | `-f ENCODING` | Script file encoding (default: UTF-8) |
234
+ | `-g ENCODING` / `--output-encoding` | Encoding for WRITE and EXPORT output |
235
+ | `-i ENCODING` / `--import-encoding` | Encoding for data files used with IMPORT |
236
+ | `-l` | Write run log to `~/execsql.log` |
237
+ | `-m` | List metacommands and exit |
238
+ | `-n` | Create a new SQLite or PostgreSQL database if it does not exist |
239
+ | `-o` / `--online-help` | Open the online documentation in the default browser |
240
+ | `-s N` / `--scan-lines` | Lines to scan for IMPORT format detection (0 = scan entire file) |
241
+ | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
242
+ | `-w` | Skip password prompt when a username is supplied |
243
+ | `-y` / `--encodings` | List available encoding names and exit |
244
+ | `-z KB` / `--import-buffer` | Import buffer size in KB (default: 32) |
245
+ | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
246
+ | `--output-dir DIR` | Default base directory for EXPORT output files |
247
+ | `--dry-run` | Parse the script and report commands without executing |
248
+ | `--lint` | Static analysis: check structure and warn on issues (no DB) |
249
+ | `--parse-tree` | Print the script's AST structure and exit (no DB) |
250
+ | `--list-plugins` | List discovered plugins and exit |
251
+ | `--ping` | Test database connectivity and exit |
252
+ | `--profile` | Show per-statement timing summary after execution |
253
+ | `--progress` | Show a progress bar for long-running IMPORT operations |
254
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
255
+ | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
256
+ | `--dump-keywords` | Print metacommand keywords as JSON and exit |
257
+ | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
249
258
 
250
259
  Run `execsql --help` for the full option list, or `execsql -m` to list all metacommands.
251
260
 
@@ -51,7 +51,8 @@ pip install execsql2[oracle] # Oracle (oracledb)
51
51
  pip install execsql2[odbc] # ODBC DSN (pyodbc)
52
52
 
53
53
  # Feature bundles
54
- pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
54
+ pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
55
+ pip install execsql2[upsert] # pg-upsert for PostgreSQL upsert operations
55
56
  pip install execsql2[auth] # OS keyring integration
56
57
  pip install execsql2[auth-plaintext] # Keyring + plaintext file backend (headless Linux)
57
58
  pip install execsql2[auth-encrypted] # Keyring + encrypted file backend (headless Linux)
@@ -97,33 +98,41 @@ execsql script.sql # read connection from config file
97
98
 
98
99
  ## Options
99
100
 
100
- | Flag | Description |
101
- | ----------------------------------- | --------------------------------------------------------------- |
102
- | `-t {p,m,s,l,k,a,f,o,d}` | Database type |
103
- | `-u USER` | Database username |
104
- | `-p PORT` | Server port |
105
- | `-a VALUE` | Set substitution variable `$ARG_x` |
106
- | `-c SCRIPT` | Execute inline SQL or metacommand string |
107
- | `-d` | Auto-create export directories |
108
- | `-f ENCODING` | Script file encoding (default: UTF-8) |
109
- | `-l` | Write run log to `~/execsql.log` |
110
- | `-m` | List metacommands and exit |
111
- | `-n` | Create a new SQLite or PostgreSQL database if it does not exist |
112
- | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
113
- | `-w` | Skip password prompt when a username is supplied |
114
- | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
115
- | `--output-dir DIR` | Default base directory for EXPORT output files |
116
- | `--dry-run` | Parse the script and report commands without executing |
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 |
120
- | `--ping` | Test database connectivity and exit |
121
- | `--profile` | Show per-statement timing summary after execution |
122
- | `--progress` | Show a progress bar for long-running IMPORT operations |
123
- | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
124
- | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
125
- | `--dump-keywords` | Print metacommand keywords as JSON and exit |
126
- | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
101
+ | Flag | Description |
102
+ | ------------------------------------- | ---------------------------------------------------------------- |
103
+ | `-t {p,m,s,l,k,a,f,o,d}` | Database type |
104
+ | `-u USER` | Database username |
105
+ | `-p PORT` | Server port |
106
+ | `-a VALUE` | Set substitution variable `$ARG_x` |
107
+ | `-b` / `--boolean-int` | Treat integers 0 and 1 as boolean values |
108
+ | `-c SCRIPT` | Execute inline SQL or metacommand string |
109
+ | `-d` | Auto-create export directories |
110
+ | `-e ENCODING` / `--database-encoding` | Character encoding used in the database |
111
+ | `-f ENCODING` | Script file encoding (default: UTF-8) |
112
+ | `-g ENCODING` / `--output-encoding` | Encoding for WRITE and EXPORT output |
113
+ | `-i ENCODING` / `--import-encoding` | Encoding for data files used with IMPORT |
114
+ | `-l` | Write run log to `~/execsql.log` |
115
+ | `-m` | List metacommands and exit |
116
+ | `-n` | Create a new SQLite or PostgreSQL database if it does not exist |
117
+ | `-o` / `--online-help` | Open the online documentation in the default browser |
118
+ | `-s N` / `--scan-lines` | Lines to scan for IMPORT format detection (0 = scan entire file) |
119
+ | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
120
+ | `-w` | Skip password prompt when a username is supplied |
121
+ | `-y` / `--encodings` | List available encoding names and exit |
122
+ | `-z KB` / `--import-buffer` | Import buffer size in KB (default: 32) |
123
+ | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
124
+ | `--output-dir DIR` | Default base directory for EXPORT output files |
125
+ | `--dry-run` | Parse the script and report commands without executing |
126
+ | `--lint` | Static analysis: check structure and warn on issues (no DB) |
127
+ | `--parse-tree` | Print the script's AST structure and exit (no DB) |
128
+ | `--list-plugins` | List discovered plugins and exit |
129
+ | `--ping` | Test database connectivity and exit |
130
+ | `--profile` | Show per-statement timing summary after execution |
131
+ | `--progress` | Show a progress bar for long-running IMPORT operations |
132
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
133
+ | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
134
+ | `--dump-keywords` | Print metacommand keywords as JSON and exit |
135
+ | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
127
136
 
128
137
  Run `execsql --help` for the full option list, or `execsql -m` to list all metacommands.
129
138
 
@@ -1,4 +1,7 @@
1
1
  # Contributors
2
2
 
3
+ R. Dreas Nielsen
4
+ : Original author of [execsql](https://execsql.readthedocs.io/) (v1.0 through v1.130.1). Designed and implemented the metacommand language, substitution variable system, database adapters, export/import framework, GUI prompt system, and all core functionality that execsql2 is built on.
5
+
3
6
  Elizabeth Shea
4
7
  : Brainstorming, testing, coding of the conditional expression parser, coding of the 'with arguments' and 'with parameters' clauses of the [SCRIPT](../reference/metacommands.md#beginscript) metacommand, coding of the WHILE and UNTIL clauses of the [EXECUTE SCRIPT](../reference/metacommands.md#executescript) metacommand, coding of [deferred variable substitution](../reference/substitution_vars.md#deferred_substitution), corrections to the [PROMPT ENTRY_FORM](../reference/metacommands.md#prompt_entry) metacommand, examples, implementation of the ["+" prefix](../reference/substitution_vars.md#outer_scope_referent) for substitution variables, and other minor corrections.
@@ -185,7 +185,7 @@ print(result.variables) # final substitution variable state
185
185
  - **DSN or connection** — pass a DSN string (`dsn="sqlite:///my.db"`) or a pre-existing `Database` object (`connection=conn`).
186
186
  - **Substitution variables** — pass a `variables` dict; keys are automatically `$`-prefixed.
187
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.
188
+ - **Isolation** — each `run()` call uses an isolated `RuntimeContext` stored in thread-local storage. Multiple calls do not share state, and concurrent calls from different threads are safe.
189
189
  - **Result object** — `ScriptResult` is a frozen dataclass with `success`, `commands_run`, `elapsed`, `errors`, and `variables`.
190
190
  - **Exception convenience** — call `result.raise_on_error()` to raise `ExecSqlError` if the script failed.
191
191
 
@@ -197,13 +197,17 @@ ______________________________________________________________________
197
197
 
198
198
  The CLI framework changed from `optparse` to [Typer](https://typer.tiangolo.com/) with Rich-formatted help text. All original short flags (`-a` through `-z`) are preserved. The tool can be invoked as either `execsql` or `execsql2`.
199
199
 
200
+ ### Default Database Type
201
+
202
+ The default database type (`-t`) changed from Access (`a`) to SQLite (`l`). Upstream defaulted to Access, which requires Windows and pyodbc. SQLite is cross-platform, ships with Python, and is the most common use case. Users targeting Access databases should pass `-t a` explicitly.
203
+
200
204
  ### Configuration
201
205
 
202
206
  - **`linux_config_file`** — now only active on Linux (`sys.platform == "linux"`). Upstream applied it to all POSIX systems, including macOS. A new `macos_config_file` option handles macOS specifically.
203
207
 
204
208
  ### Internal State Management
205
209
 
206
- All 33 mutable runtime globals in `state.py` have been consolidated into a `RuntimeContext` object. The module uses a transparent proxy so existing code is unaffected, but the architecture now supports isolated contexts for testing and future concurrent execution.
210
+ All 33 mutable runtime globals in `state.py` have been consolidated into a `RuntimeContext` object stored in `threading.local()`. The module uses a transparent proxy so existing code is unaffected. Each thread gets its own isolated context, enabling concurrent `from execsql import run` calls and future PARALLEL blocks.
207
211
 
208
212
  ### Substitution Variables
209
213
 
@@ -276,37 +280,39 @@ These are behavioral changes driven by security or correctness issues in the ups
276
280
 
277
281
  ### Bug Fixes
278
282
 
279
- | Area | Fix |
280
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
281
- | Oracle default port | Corrected from `5432` (PostgreSQL) to `1521`. |
282
- | MySQL `LOAD DATA INFILE` encoding | Python encoding names are now mapped to MySQL charset names. |
283
- | `dt_cast` type converters | Base `Database` class auto-populates 8 type converters that were previously left empty after the refactor. |
284
- | `FileWriter` CPU busy-loop | Uses blocking `queue.get(timeout=0.1)` instead of `get_nowait()` in a tight loop. |
285
- | Substitution variable cycles | 100-iteration limit prevents infinite loops on cyclic variable references. |
286
- | Script location in error messages | `ErrInfo.script_file` and `script_line_no` are now populated via `stamp_errinfo()` so error output includes "Line N of script foo.sql" context — restoring behavior present in the monolith. |
287
- | `$ERROR_MESSAGE` not updated | `$ERROR_MESSAGE` is now set on every error path: `exit_now()`, non-halting SQL errors, and non-halting metacommand errors. Previously it was initialized to `""` and never changed. |
288
- | Metacommand error message lost | When `halt_on_metacommand_err` is `ON`, the original handler `ErrInfo` is now re-raised; the generic "Unknown metacommand" message no longer replaces the specific error from the handler. |
289
- | Empty script name in error msg | `_execute_script_direct()` and `_execute_script_textual_console()` no longer append "in script , line 0" to uncaught-exception messages when `current_script_line()` returns an empty string. |
290
- | `PROMPT COMPARE` diff comparison | Diff engine uses native Python equality instead of string comparison — numeric types, Decimals, and booleans compare correctly. `None` is distinguished from empty string. Columns are matched by name (not position), key columns are excluded from comparison, and duplicate PKs keep the first row. |
291
- | `win_config_file` broken | Checked `os.name == "windows"` which Python never returns (correct value is `"nt"`). Fixed to `os.name == "nt"`. Inherited from upstream. |
292
- | `NumericParser` right-associative | Arithmetic operators were parsed right-to-left. `10 - 3 - 2` evaluated as `9` instead of `5`. Fixed to left-associative parsing. Inherited from upstream. |
293
- | Empty-column check precedence | `DataTable` and `Database.populate_table()` had an operator precedence bug in the extra-column emptiness check — a redundant `and conf.del_empty_cols` caused incorrect short-circuit evaluation. Inherited from upstream. |
294
- | SQLite import string processing | `SQLiteDatabase.populate_table()` applied `trim_strings`, `replace_newlines`, and `empty_strings` after copying row data, so processing never reached the INSERT. Fixed to process before extraction. Inherited from upstream. |
295
- | `$CURRENT_DATABASE`/`$CURRENT_DBMS` stale after USE | These variables were only set at startup and on CONNECT, not refreshed when `USE` switched the active database. Now set in `set_static_system_vars()` so they update on any connection change. |
296
- | `DT_Text.data_type_name` wrong | Was `"character"` (same as `DT_Character`), making error messages indistinguishable. Corrected to `"text"`. Inherited from upstream. |
297
- | `DT_Varchar` non-string data unchecked | `_from_data()` only enforced the 255-char limit for `str` inputs; non-string values passed through without conversion or length check. Inherited from upstream. |
298
- | `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. Inherited from upstream. |
299
- | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. Inherited from upstream. |
300
- | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. Inherited from upstream. |
301
- | `DT_Timestamp` claims time-only values | `dateutil.parser.parse()` silently fills in today's date for bare time strings like `"13:15:45"`, so `DT_Timestamp` matched before `DT_Time` in the inference order. `parse_datetime()` now rejects time-only strings. Inherited from upstream. |
302
- | `CounterVars.substitute` skipped pos 0–1 | `re.I` was passed as the positional `pos` argument, skipping the first 2 characters of every string during counter variable expansion. Counter variables at the very start of a line were never matched. Inherited from upstream. |
303
- | `exec_cmd` (SQLite/DuckDB) always raised TypeError | Passed bytes to `curs.execute()` via `.encode()`, which Python 3 `sqlite3`/`duckdb` reject. `EXECUTE PROCEDURE` metacommand was non-functional on these backends. Inherited from upstream. |
304
- | MySQL `LOAD DATA INFILE` injection | File path, delimiter, and quotechar were interpolated without escaping. Now single-quotes are escaped consistent with the PostgreSQL COPY path. Inherited from upstream. |
305
- | Config file chain infinite loop | The `config_file` directive could chain config files without limit. A circular reference (via symlinks or different relative paths) caused an infinite loop at startup. Now capped at 20 files. Inherited from upstream. |
306
- | Cursor leaks in database adapters | ~15 methods across all adapters used `curs = self.cursor()` / `curs.close()` without `try/finally`. If the query raised, the cursor leaked. Converted to `with self._cursor() as curs:`. Inherited from upstream. |
307
- | JSON export malformed on special column names | Column names containing `"` or `\` produced invalid JSON. Now uses `json.dumps()` for all field names. Inherited from upstream. |
308
- | Temp file creation TOCTOU race | `TempFileMgr.new_temp_fn()` discarded the `NamedTemporaryFile` handle, creating a race window. Now uses `tempfile.mkstemp()` for secure creation. Inherited from upstream. |
309
- | `shlex.split` on Windows incorrect mode | Called without `posix=False` on Windows, mishandling backslash-heavy paths in SHELL commands. Inherited from upstream. |
283
+ | Area | Fix |
284
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
285
+ | Oracle default port | Corrected from `5432` (PostgreSQL) to `1521`. |
286
+ | MySQL `LOAD DATA INFILE` encoding | Python encoding names are now mapped to MySQL charset names. |
287
+ | `dt_cast` type converters | Base `Database` class auto-populates 8 type converters that were previously left empty after the refactor. |
288
+ | `FileWriter` CPU busy-loop | Uses blocking `queue.get(timeout=0.1)` instead of `get_nowait()` in a tight loop. |
289
+ | Substitution variable cycles | 100-iteration limit prevents infinite loops on cyclic variable references. |
290
+ | Script location in error messages | `ErrInfo.script_file` and `script_line_no` are now populated via `stamp_errinfo()` so error output includes "Line N of script foo.sql" context — restoring behavior present in the monolith. |
291
+ | `$ERROR_MESSAGE` not updated | `$ERROR_MESSAGE` is now set on every error path: `exit_now()`, non-halting SQL errors, and non-halting metacommand errors. Previously it was initialized to `""` and never changed. |
292
+ | Metacommand error message lost | When `halt_on_metacommand_err` is `ON`, the original handler `ErrInfo` is now re-raised; the generic "Unknown metacommand" message no longer replaces the specific error from the handler. |
293
+ | Empty script name in error msg | `_execute_script_direct()` and `_execute_script_textual_console()` no longer append "in script , line 0" to uncaught-exception messages when `current_script_line()` returns an empty string. |
294
+ | `PROMPT COMPARE` diff comparison | Diff engine uses native Python equality instead of string comparison — numeric types, Decimals, and booleans compare correctly. `None` is distinguished from empty string. Columns are matched by name (not position), key columns are excluded from comparison, and duplicate PKs keep the first row. |
295
+ | `win_config_file` broken | Checked `os.name == "windows"` which Python never returns (correct value is `"nt"`). Fixed to `os.name == "nt"`. Inherited from upstream. |
296
+ | `NumericParser` right-associative | Arithmetic operators were parsed right-to-left. `10 - 3 - 2` evaluated as `9` instead of `5`. Fixed to left-associative parsing. Inherited from upstream. |
297
+ | Empty-column check precedence | `DataTable` and `Database.populate_table()` had an operator precedence bug in the extra-column emptiness check — a redundant `and conf.del_empty_cols` caused incorrect short-circuit evaluation. Inherited from upstream. |
298
+ | SQLite import string processing | `SQLiteDatabase.populate_table()` applied `trim_strings`, `replace_newlines`, and `empty_strings` after copying row data, so processing never reached the INSERT. Fixed to process before extraction. Inherited from upstream. |
299
+ | `$CURRENT_DATABASE`/`$CURRENT_DBMS` stale after USE | These variables were only set at startup and on CONNECT, not refreshed when `USE` switched the active database. Now set in `set_static_system_vars()` so they update on any connection change. |
300
+ | `DT_Text.data_type_name` wrong | Was `"character"` (same as `DT_Character`), making error messages indistinguishable. Corrected to `"text"`. Inherited from upstream. |
301
+ | `DT_Varchar` non-string data unchecked | `_from_data()` only enforced the 255-char limit for `str` inputs; non-string values passed through without conversion or length check. Inherited from upstream. |
302
+ | `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. Inherited from upstream. |
303
+ | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. Inherited from upstream. |
304
+ | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. Inherited from upstream. |
305
+ | `DT_Timestamp` claims time-only values | `dateutil.parser.parse()` silently fills in today's date for bare time strings like `"13:15:45"`, so `DT_Timestamp` matched before `DT_Time` in the inference order. `parse_datetime()` now rejects time-only strings. Inherited from upstream. |
306
+ | `CounterVars.substitute` skipped pos 0–1 | `re.I` was passed as the positional `pos` argument, skipping the first 2 characters of every string during counter variable expansion. Counter variables at the very start of a line were never matched. Inherited from upstream. |
307
+ | `exec_cmd` (SQLite/DuckDB) always raised TypeError | Passed bytes to `curs.execute()` via `.encode()`, which Python 3 `sqlite3`/`duckdb` reject. `EXECUTE PROCEDURE` metacommand was non-functional on these backends. Inherited from upstream. |
308
+ | MySQL `LOAD DATA INFILE` injection | File path, delimiter, and quotechar were interpolated without escaping. Now single-quotes are escaped consistent with the PostgreSQL COPY path. Inherited from upstream. |
309
+ | Config file chain infinite loop | The `config_file` directive could chain config files without limit. A circular reference (via symlinks or different relative paths) caused an infinite loop at startup. Now capped at 20 files. Inherited from upstream. |
310
+ | Cursor leaks in database adapters | ~15 methods across all adapters used `curs = self.cursor()` / `curs.close()` without `try/finally`. If the query raised, the cursor leaked. Converted to `with self._cursor() as curs:`. Inherited from upstream. |
311
+ | JSON export malformed on special column names | Column names containing `"` or `\` produced invalid JSON. Now uses `json.dumps()` for all field names. Inherited from upstream. |
312
+ | Temp file creation TOCTOU race | `TempFileMgr.new_temp_fn()` discarded the `NamedTemporaryFile` handle, creating a race window. Now uses `tempfile.mkstemp()` for secure creation. Inherited from upstream. |
313
+ | `shlex.split` on Windows incorrect mode | Called without `posix=False` on Windows, mishandling backslash-heavy paths in SHELL commands. Inherited from upstream. |
314
+ | AST executor `~`/`+` variable scoping broken | The AST executor passed `localvars` through function parameters but never pushed `CommandList` frames onto `commandliststack`. Legacy metacommand handlers (`x_sub`, `x_rm_sub`, `xf_sub_defined`, `SUB_LOCAL`, prompt handlers, REPL) access `commandliststack[-1]` for `~` local and `+` outer-scope variables. This caused `~` vars to be invisible to SQL, `SUB_DEFINED(~var)` to always return false, and the REPL `.vars`/`.stack` to show empty state. Fixed by pushing/popping `CommandList` frames in `execute()` and `_execute_script_native()`. |
315
+ | AST parser INCLUDE quoted paths broken | The AST parser captured the full INCLUDE target including surrounding quotes (`"path"`), but the legacy dispatch regex stripped them. Quoted INCLUDE paths failed with "File does not exist" even when the file was present. Fixed by stripping matched quote pairs in the parser. |
310
316
 
311
317
  ______________________________________________________________________
312
318
 
@@ -28,10 +28,16 @@ Each module below implements one or more output formats. Every writer follows th
28
28
 
29
29
  ::: execsql.exporters.latex
30
30
 
31
+ ::: execsql.exporters.markdown
32
+
31
33
  ::: execsql.exporters.ods
32
34
 
33
35
  ::: execsql.exporters.xls
34
36
 
37
+ ::: execsql.exporters.xlsx
38
+
39
+ ::: execsql.exporters.yaml
40
+
35
41
  ::: execsql.exporters.feather
36
42
 
37
43
  ::: execsql.exporters.parquet
@@ -14,6 +14,8 @@ Each module below implements one or more import formats.
14
14
 
15
15
  ::: execsql.importers.csv
16
16
 
17
+ ::: execsql.importers.json
18
+
17
19
  ::: execsql.importers.ods
18
20
 
19
21
  ::: execsql.importers.xls
@@ -24,6 +24,30 @@ result: ScriptResult = run(
24
24
 
25
25
  See the [README](https://github.com/geocoug/execsql#library-api) for full examples.
26
26
 
27
+ ### Thread Safety
28
+
29
+ `run()` is **thread-safe**. Each call creates an isolated `RuntimeContext` stored in thread-local storage, so concurrent calls from different threads do not share database connections, substitution variables, or execution state.
30
+
31
+ ```python
32
+ import threading
33
+ from execsql import run
34
+
35
+ def etl_worker(script, dsn):
36
+ result = run(script=script, dsn=dsn)
37
+ print(f"{script}: {'OK' if result.success else 'FAIL'}")
38
+
39
+ threads = [
40
+ threading.Thread(target=etl_worker, args=("load_us.sql", "postgresql://host/us_db")),
41
+ threading.Thread(target=etl_worker, args=("load_eu.sql", "postgresql://host/eu_db")),
42
+ ]
43
+ for t in threads:
44
+ t.start()
45
+ for t in threads:
46
+ t.join()
47
+ ```
48
+
49
+ Each thread gets its own database connections, IF/LOOP stacks, substitution variables, and error state. No locking is required.
50
+
27
51
  ## Extension Guides
28
52
 
29
53
  | Extension type | Guide | API reference |
@@ -42,16 +42,20 @@ ______________________________________________________________________
42
42
 
43
43
  Add the handler to whichever sibling module fits best:
44
44
 
45
- | Module | Handles |
46
- | --------------- | ----------------------------------------------------- |
47
- | `connect.py` | Database connections, `USE`, `DISCONNECT` |
48
- | `control.py` | Control flow: `IF`, `LOOP`, `BREAK`, `HALT`, batch |
49
- | `data.py` | Variable manipulation: `SUB`, `SUBDATA`, counters |
50
- | `io.py` | File I/O: `EXPORT`, `IMPORT`, `INCLUDE`, `WRITE` |
51
- | `prompt.py` | User interaction: `PROMPT`, `ASK`, `PAUSE`, `MSG` |
52
- | `system.py` | OS interaction: `SYSTEM_CMD`, `LOG`, `EMAIL`, console |
53
- | `debug.py` | Debug output |
54
- | `script_ext.py` | Script extensions |
45
+ | Module | Handles |
46
+ | --------------- | ---------------------------------------------------------------------- |
47
+ | `connect.py` | Database connections, `USE`, `DISCONNECT` |
48
+ | `control.py` | Control flow: `IF`, `LOOP`, `BREAK`, `HALT`, batch |
49
+ | `data.py` | Variable manipulation: `SUB`, `SUBDATA`, counters |
50
+ | `io.py` | Re-export façade only -- imports and re-exports from the modules below |
51
+ | `io_export.py` | `EXPORT` handlers |
52
+ | `io_import.py` | `IMPORT` handlers |
53
+ | `io_write.py` | `WRITE` and `WRITESCRIPT` handlers |
54
+ | `io_fileops.py` | `INCLUDE`, `ZIP`, `CD`, `SERVE`, and other file operations |
55
+ | `prompt.py` | User interaction: `PROMPT`, `ASK`, `PAUSE`, `MSG` |
56
+ | `system.py` | OS interaction: `SYSTEM_CMD`, `LOG`, `EMAIL`, console |
57
+ | `debug.py` | Debug output |
58
+ | `script_ext.py` | Script extensions |
55
59
 
56
60
  If none of these fit, create a new module and follow the same structure.
57
61