execsql2 2.9.0__tar.gz → 2.10.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 (293) hide show
  1. {execsql2-2.9.0 → execsql2-2.10.0}/CHANGELOG.md +12 -0
  2. {execsql2-2.9.0 → execsql2-2.10.0}/PKG-INFO +1 -1
  3. {execsql2-2.9.0 → execsql2-2.10.0}/docs/about/divergence.md +15 -5
  4. {execsql2-2.9.0 → execsql2-2.10.0}/docs/reference/metacommands.md +94 -0
  5. {execsql2-2.9.0 → execsql2-2.10.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +2 -2
  6. {execsql2-2.9.0 → execsql2-2.10.0}/pyproject.toml +2 -2
  7. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/__init__.py +3 -0
  8. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/conditions.py +148 -0
  9. execsql2-2.10.0/src/execsql/metacommands/debug_repl.py +223 -0
  10. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/dispatch.py +12 -0
  11. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/script/engine.py +5 -0
  12. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/state.py +9 -0
  13. execsql2-2.10.0/tests/metacommands/test_breakpoint.py +464 -0
  14. execsql2-2.10.0/tests/metacommands/test_row_count.py +496 -0
  15. {execsql2-2.9.0 → execsql2-2.10.0}/uv.lock +1 -1
  16. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/agents/dba.md +0 -0
  17. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/agents/herald.md +0 -0
  18. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/agents/inspector.md +0 -0
  19. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/agents/oracle.md +0 -0
  20. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/agents/patcher.md +0 -0
  21. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/agents/qa.md +0 -0
  22. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/agents/scribe.md +0 -0
  23. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/commands/code-oracle.md +0 -0
  24. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/commands/migrate.md +0 -0
  25. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/commands/review-changes.md +0 -0
  26. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/commands/test-module.md +0 -0
  27. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/commands/update-changelog.md +0 -0
  28. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/commands/where-is.md +0 -0
  29. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/project_context.md +0 -0
  30. {execsql2-2.9.0 → execsql2-2.10.0}/.claude/state/status.md +0 -0
  31. {execsql2-2.9.0 → execsql2-2.10.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  32. {execsql2-2.9.0 → execsql2-2.10.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  33. {execsql2-2.9.0 → execsql2-2.10.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  34. {execsql2-2.9.0 → execsql2-2.10.0}/.github/workflows/ci-cd.yml +0 -0
  35. {execsql2-2.9.0 → execsql2-2.10.0}/.gitignore +0 -0
  36. {execsql2-2.9.0 → execsql2-2.10.0}/.pre-commit-config.yaml +0 -0
  37. {execsql2-2.9.0 → execsql2-2.10.0}/.pre-commit-hooks.yaml +0 -0
  38. {execsql2-2.9.0 → execsql2-2.10.0}/.python-version +0 -0
  39. {execsql2-2.9.0 → execsql2-2.10.0}/.readthedocs.yaml +0 -0
  40. {execsql2-2.9.0 → execsql2-2.10.0}/CLAUDE.md +0 -0
  41. {execsql2-2.9.0 → execsql2-2.10.0}/CONTRIBUTING.md +0 -0
  42. {execsql2-2.9.0 → execsql2-2.10.0}/LICENSE.txt +0 -0
  43. {execsql2-2.9.0 → execsql2-2.10.0}/NOTICE +0 -0
  44. {execsql2-2.9.0 → execsql2-2.10.0}/README.md +0 -0
  45. {execsql2-2.9.0 → execsql2-2.10.0}/SECURITY.md +0 -0
  46. {execsql2-2.9.0 → execsql2-2.10.0}/docs/about/contributors.md +0 -0
  47. {execsql2-2.9.0 → execsql2-2.10.0}/docs/about/copyright.md +0 -0
  48. {execsql2-2.9.0 → execsql2-2.10.0}/docs/api/cli.md +0 -0
  49. {execsql2-2.9.0 → execsql2-2.10.0}/docs/api/db.md +0 -0
  50. {execsql2-2.9.0 → execsql2-2.10.0}/docs/api/exporters.md +0 -0
  51. {execsql2-2.9.0 → execsql2-2.10.0}/docs/api/importers.md +0 -0
  52. {execsql2-2.9.0 → execsql2-2.10.0}/docs/api/index.md +0 -0
  53. {execsql2-2.9.0 → execsql2-2.10.0}/docs/api/metacommands.md +0 -0
  54. {execsql2-2.9.0 → execsql2-2.10.0}/docs/dev/adding_db_adapters.md +0 -0
  55. {execsql2-2.9.0 → execsql2-2.10.0}/docs/dev/adding_exporters.md +0 -0
  56. {execsql2-2.9.0 → execsql2-2.10.0}/docs/dev/adding_importers.md +0 -0
  57. {execsql2-2.9.0 → execsql2-2.10.0}/docs/dev/adding_metacommands.md +0 -0
  58. {execsql2-2.9.0 → execsql2-2.10.0}/docs/dev/architecture.md +0 -0
  59. {execsql2-2.9.0 → execsql2-2.10.0}/docs/getting-started/installation.md +0 -0
  60. {execsql2-2.9.0 → execsql2-2.10.0}/docs/getting-started/requirements.md +0 -0
  61. {execsql2-2.9.0 → execsql2-2.10.0}/docs/getting-started/syntax.md +0 -0
  62. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/debugging.md +0 -0
  63. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/documentation.md +0 -0
  64. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/encoding.md +0 -0
  65. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/examples.md +0 -0
  66. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/formatter.md +0 -0
  67. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/logging.md +0 -0
  68. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/sql_syntax.md +0 -0
  69. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/usage.md +0 -0
  70. {execsql2-2.9.0 → execsql2-2.10.0}/docs/guides/using_scripts.md +0 -0
  71. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/Compare_planets.png +0 -0
  72. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/actions.png +0 -0
  73. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/actions2.png +0 -0
  74. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/checkboxes.png +0 -0
  75. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/connect.b64 +0 -0
  76. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/connect.png +0 -0
  77. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/create_conf.png +0 -0
  78. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/data_error1_screenshot.jpg +0 -0
  79. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/entry_form.png +0 -0
  80. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/execsql_console.png +0 -0
  81. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/execsql_logo_01.png +0 -0
  82. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/fatals.png +0 -0
  83. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/logo_small.png +0 -0
  84. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/pause_terminal.png +0 -0
  85. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/pause_terminal_sm.b64 +0 -0
  86. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/pause_terminal_sm.png +0 -0
  87. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/prompt_compare.png +0 -0
  88. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/set_build_commands.jpg +0 -0
  89. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/unit_conversions.b64 +0 -0
  90. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/unit_conversions_029.png +0 -0
  91. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/unmatched.png +0 -0
  92. {execsql2-2.9.0 → execsql2-2.10.0}/docs/images/vim_execsql_highlight.png +0 -0
  93. {execsql2-2.9.0 → execsql2-2.10.0}/docs/index.md +0 -0
  94. {execsql2-2.9.0 → execsql2-2.10.0}/docs/reference/configuration.md +0 -0
  95. {execsql2-2.9.0 → execsql2-2.10.0}/docs/reference/security.md +0 -0
  96. {execsql2-2.9.0 → execsql2-2.10.0}/docs/reference/substitution_vars.md +0 -0
  97. {execsql2-2.9.0 → execsql2-2.10.0}/extras/vscode-execsql/README.md +0 -0
  98. {execsql2-2.9.0 → execsql2-2.10.0}/extras/vscode-execsql/package.json +0 -0
  99. {execsql2-2.9.0 → execsql2-2.10.0}/justfile +0 -0
  100. {execsql2-2.9.0 → execsql2-2.10.0}/scripts/generate_vscode_grammar.py +0 -0
  101. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/__init__.py +0 -0
  102. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/__main__.py +0 -0
  103. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/cli/__init__.py +0 -0
  104. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/cli/dsn.py +0 -0
  105. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/cli/help.py +0 -0
  106. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/cli/lint.py +0 -0
  107. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/cli/run.py +0 -0
  108. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/config.py +0 -0
  109. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/constants.py +0 -0
  110. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/__init__.py +0 -0
  111. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/access.py +0 -0
  112. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/base.py +0 -0
  113. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/dsn.py +0 -0
  114. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/duckdb.py +0 -0
  115. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/factory.py +0 -0
  116. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/firebird.py +0 -0
  117. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/mysql.py +0 -0
  118. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/oracle.py +0 -0
  119. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/postgres.py +0 -0
  120. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/sqlite.py +0 -0
  121. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/db/sqlserver.py +0 -0
  122. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exceptions.py +0 -0
  123. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/__init__.py +0 -0
  124. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/base.py +0 -0
  125. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/delimited.py +0 -0
  126. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/duckdb.py +0 -0
  127. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/feather.py +0 -0
  128. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/html.py +0 -0
  129. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/json.py +0 -0
  130. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/latex.py +0 -0
  131. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/markdown.py +0 -0
  132. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/ods.py +0 -0
  133. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/parquet.py +0 -0
  134. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/pretty.py +0 -0
  135. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/protocol.py +0 -0
  136. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/raw.py +0 -0
  137. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/sqlite.py +0 -0
  138. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/templates.py +0 -0
  139. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/values.py +0 -0
  140. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/xls.py +0 -0
  141. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/xlsx.py +0 -0
  142. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/xml.py +0 -0
  143. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/yaml.py +0 -0
  144. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/exporters/zip.py +0 -0
  145. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/format.py +0 -0
  146. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/gui/__init__.py +0 -0
  147. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/gui/base.py +0 -0
  148. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/gui/console.py +0 -0
  149. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/gui/desktop.py +0 -0
  150. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/gui/tui.py +0 -0
  151. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/importers/__init__.py +0 -0
  152. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/importers/base.py +0 -0
  153. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/importers/csv.py +0 -0
  154. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/importers/feather.py +0 -0
  155. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/importers/ods.py +0 -0
  156. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/importers/xls.py +0 -0
  157. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/connect.py +0 -0
  158. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/control.py +0 -0
  159. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/data.py +0 -0
  160. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/debug.py +0 -0
  161. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/io.py +0 -0
  162. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/io_export.py +0 -0
  163. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/io_fileops.py +0 -0
  164. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/io_import.py +0 -0
  165. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/io_write.py +0 -0
  166. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/prompt.py +0 -0
  167. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/script_ext.py +0 -0
  168. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/metacommands/system.py +0 -0
  169. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/models.py +0 -0
  170. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/parser.py +0 -0
  171. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/py.typed +0 -0
  172. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/script/__init__.py +0 -0
  173. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/script/control.py +0 -0
  174. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/script/variables.py +0 -0
  175. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/types.py +0 -0
  176. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/__init__.py +0 -0
  177. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/auth.py +0 -0
  178. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/crypto.py +0 -0
  179. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/datetime.py +0 -0
  180. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/errors.py +0 -0
  181. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/fileio.py +0 -0
  182. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/gui.py +0 -0
  183. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/mail.py +0 -0
  184. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/numeric.py +0 -0
  185. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/regex.py +0 -0
  186. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/strings.py +0 -0
  187. {execsql2-2.9.0 → execsql2-2.10.0}/src/execsql/utils/timer.py +0 -0
  188. {execsql2-2.9.0 → execsql2-2.10.0}/templates/README.md +0 -0
  189. {execsql2-2.9.0 → execsql2-2.10.0}/templates/config_settings.sqlite +0 -0
  190. {execsql2-2.9.0 → execsql2-2.10.0}/templates/example_config_prompt.sql +0 -0
  191. {execsql2-2.9.0 → execsql2-2.10.0}/templates/execsql.conf +0 -0
  192. {execsql2-2.9.0 → execsql2-2.10.0}/templates/make_config_db.sql +0 -0
  193. {execsql2-2.9.0 → execsql2-2.10.0}/templates/md_compare.sql +0 -0
  194. {execsql2-2.9.0 → execsql2-2.10.0}/templates/md_glossary.sql +0 -0
  195. {execsql2-2.9.0 → execsql2-2.10.0}/templates/md_upsert.sql +0 -0
  196. {execsql2-2.9.0 → execsql2-2.10.0}/templates/pg_compare.sql +0 -0
  197. {execsql2-2.9.0 → execsql2-2.10.0}/templates/pg_glossary.sql +0 -0
  198. {execsql2-2.9.0 → execsql2-2.10.0}/templates/pg_upsert.sql +0 -0
  199. {execsql2-2.9.0 → execsql2-2.10.0}/templates/script_template.sql +0 -0
  200. {execsql2-2.9.0 → execsql2-2.10.0}/templates/ss_compare.sql +0 -0
  201. {execsql2-2.9.0 → execsql2-2.10.0}/templates/ss_glossary.sql +0 -0
  202. {execsql2-2.9.0 → execsql2-2.10.0}/templates/ss_upsert.sql +0 -0
  203. {execsql2-2.9.0 → execsql2-2.10.0}/tests/__init__.py +0 -0
  204. {execsql2-2.9.0 → execsql2-2.10.0}/tests/cli/__init__.py +0 -0
  205. {execsql2-2.9.0 → execsql2-2.10.0}/tests/cli/test_cli.py +0 -0
  206. {execsql2-2.9.0 → execsql2-2.10.0}/tests/cli/test_cli_e2e.py +0 -0
  207. {execsql2-2.9.0 → execsql2-2.10.0}/tests/cli/test_cli_run.py +0 -0
  208. {execsql2-2.9.0 → execsql2-2.10.0}/tests/cli/test_lint.py +0 -0
  209. {execsql2-2.9.0 → execsql2-2.10.0}/tests/cli/test_ping.py +0 -0
  210. {execsql2-2.9.0 → execsql2-2.10.0}/tests/cli/test_profile.py +0 -0
  211. {execsql2-2.9.0 → execsql2-2.10.0}/tests/conftest.py +0 -0
  212. {execsql2-2.9.0 → execsql2-2.10.0}/tests/db/__init__.py +0 -0
  213. {execsql2-2.9.0 → execsql2-2.10.0}/tests/db/test_base.py +0 -0
  214. {execsql2-2.9.0 → execsql2-2.10.0}/tests/db/test_duckdb.py +0 -0
  215. {execsql2-2.9.0 → execsql2-2.10.0}/tests/db/test_factory.py +0 -0
  216. {execsql2-2.9.0 → execsql2-2.10.0}/tests/db/test_postgres.py +0 -0
  217. {execsql2-2.9.0 → execsql2-2.10.0}/tests/db/test_sqlite.py +0 -0
  218. {execsql2-2.9.0 → execsql2-2.10.0}/tests/db/test_sqlite_extra.py +0 -0
  219. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/__init__.py +0 -0
  220. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_base.py +0 -0
  221. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_db.py +0 -0
  222. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_delimited.py +0 -0
  223. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  224. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_exporters.py +0 -0
  225. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_feather.py +0 -0
  226. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_html_latex.py +0 -0
  227. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_json.py +0 -0
  228. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_markdown.py +0 -0
  229. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_ods.py +0 -0
  230. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_parquet.py +0 -0
  231. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  232. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_templates.py +0 -0
  233. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_xls_xlsx.py +0 -0
  234. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_xlsx.py +0 -0
  235. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_xml.py +0 -0
  236. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_yaml.py +0 -0
  237. {execsql2-2.9.0 → execsql2-2.10.0}/tests/exporters/test_zip.py +0 -0
  238. {execsql2-2.9.0 → execsql2-2.10.0}/tests/gui/__init__.py +0 -0
  239. {execsql2-2.9.0 → execsql2-2.10.0}/tests/gui/test_backends.py +0 -0
  240. {execsql2-2.9.0 → execsql2-2.10.0}/tests/importers/__init__.py +0 -0
  241. {execsql2-2.9.0 → execsql2-2.10.0}/tests/importers/test_csv_importer.py +0 -0
  242. {execsql2-2.9.0 → execsql2-2.10.0}/tests/importers/test_feather_importer.py +0 -0
  243. {execsql2-2.9.0 → execsql2-2.10.0}/tests/importers/test_ods_importer.py +0 -0
  244. {execsql2-2.9.0 → execsql2-2.10.0}/tests/importers/test_xls_importer.py +0 -0
  245. {execsql2-2.9.0 → execsql2-2.10.0}/tests/integration/__init__.py +0 -0
  246. {execsql2-2.9.0 → execsql2-2.10.0}/tests/integration/conftest.py +0 -0
  247. {execsql2-2.9.0 → execsql2-2.10.0}/tests/integration/test_dsn.py +0 -0
  248. {execsql2-2.9.0 → execsql2-2.10.0}/tests/integration/test_duckdb.py +0 -0
  249. {execsql2-2.9.0 → execsql2-2.10.0}/tests/integration/test_mysql.py +0 -0
  250. {execsql2-2.9.0 → execsql2-2.10.0}/tests/integration/test_postgres.py +0 -0
  251. {execsql2-2.9.0 → execsql2-2.10.0}/tests/integration/test_sqlite.py +0 -0
  252. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/__init__.py +0 -0
  253. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_assert.py +0 -0
  254. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_connect.py +0 -0
  255. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands.py +0 -0
  256. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  257. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_data.py +0 -0
  258. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_extended.py +0 -0
  259. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  260. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_io.py +0 -0
  261. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  262. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  263. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_system.py +0 -0
  264. {execsql2-2.9.0 → execsql2-2.10.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  265. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_config.py +0 -0
  266. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_config_data.py +0 -0
  267. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_constants.py +0 -0
  268. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_engine.py +0 -0
  269. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_exceptions.py +0 -0
  270. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_format.py +0 -0
  271. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_mail.py +0 -0
  272. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_models.py +0 -0
  273. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_package.py +0 -0
  274. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_parser.py +0 -0
  275. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_registry.py +0 -0
  276. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_script.py +0 -0
  277. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_state.py +0 -0
  278. {execsql2-2.9.0 → execsql2-2.10.0}/tests/test_types.py +0 -0
  279. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/__init__.py +0 -0
  280. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_auth.py +0 -0
  281. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_auth_extra.py +0 -0
  282. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_crypto.py +0 -0
  283. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_datetime.py +0 -0
  284. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_errors.py +0 -0
  285. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_errors_extra.py +0 -0
  286. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_fileio.py +0 -0
  287. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_fileio_extra.py +0 -0
  288. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_numeric.py +0 -0
  289. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_regex.py +0 -0
  290. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_strings.py +0 -0
  291. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_timer.py +0 -0
  292. {execsql2-2.9.0 → execsql2-2.10.0}/tests/utils/test_timer_extra.py +0 -0
  293. {execsql2-2.9.0 → execsql2-2.10.0}/zensical.toml +0 -0
@@ -13,6 +13,18 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.10.0] - 2026-04-01
17
+
18
+ ### Added
19
+
20
+ - **`BREAKPOINT` metacommand** — pauses script execution and drops into an interactive debug REPL. The prompt accepts `continue`/`c` to resume, `abort`/`q` to halt, `vars` to list substitution variables, `$VARNAME` to print a single variable, `SELECT ...;` to run ad-hoc SQL against the current database, `next`/`n` to step one statement at a time, `stack` to inspect the command-list stack, and `help` for a command summary. Silently skipped in non-TTY environments (CI, piped input) so automated pipelines are never blocked.
21
+
22
+ - **`step_mode` on `RuntimeContext`** — internal boolean flag set by the REPL's `next` command; the script engine re-enters the debug REPL after each subsequent statement while step mode is active.
23
+
24
+ - **`ROW_COUNT_GT(table, N)`**, **`ROW_COUNT_GTE(table, N)`**, **`ROW_COUNT_EQ(table, N)`**, **`ROW_COUNT_LT(table, N)`** conditional tests — compare the row count of any table or view against an integer threshold using `IF`, `ELSEIF`, or `ASSERT`. Each issues a `SELECT count(*)` query against the current database. An error is raised if the table does not exist or the threshold is not an integer.
25
+
26
+ ______________________________________________________________________
27
+
16
28
  ## [2.9.0] - 2026-04-01
17
29
 
18
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.9.0
3
+ Version: 2.10.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: Repository, https://github.com/geocoug/execsql
6
6
  Project-URL: Issues, https://github.com/geocoug/execsql/issues
@@ -39,11 +39,21 @@ ______________________________________________________________________
39
39
 
40
40
  ### Metacommands
41
41
 
42
- | Metacommand | Description |
43
- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `ASSERT` | Evaluate a condition and raise an error (halting the script) if it is false. Supports all IF conditions. Optional quoted failure message. Skipped in false IF blocks. |
45
- | `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
46
- | `CONFIG LOG_SQL` | Enable SQL query audit logging writes executed SQL to the log file. |
42
+ | Metacommand | Description |
43
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
44
+ | `ASSERT` | Evaluate a condition and raise an error (halting the script) if it is false. Supports all IF conditions. Optional quoted failure message. Skipped in false IF blocks. |
45
+ | `BREAKPOINT` | Pause script execution and drop into an interactive debug REPL. Inspect variables, run ad-hoc SQL, and step through the script. Silently skipped in non-TTY (CI) environments. |
46
+ | `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
47
+ | `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file. |
48
+
49
+ ### Conditional Tests
50
+
51
+ | Conditional | Description |
52
+ | ------------------------- | -------------------------------------------------------------------------------------------------------- |
53
+ | `ROW_COUNT_GT(table, N)` | True if the number of rows in *table* is strictly greater than *N* (integer). Queries `SELECT count(*)`. |
54
+ | `ROW_COUNT_GTE(table, N)` | True if the number of rows in *table* is greater than or equal to *N*. |
55
+ | `ROW_COUNT_EQ(table, N)` | True if the number of rows in *table* is exactly equal to *N*. |
56
+ | `ROW_COUNT_LT(table, N)` | True if the number of rows in *table* is strictly less than *N*. |
47
57
 
48
58
  ### Configuration Options
49
59
 
@@ -67,6 +67,7 @@ The `<condition>` supports all [conditional tests](metacommands.md#conditional_t
67
67
  - `HAS_ROWS(<table>)` / `NOT HAS_ROWS(<table>)`
68
68
  - `EQUAL(<val1>, <val2>)` / `NOT EQUAL(<val1>, <val2>)`
69
69
  - `IS_GT(<val1>, <val2>)`, `IS_GTE(<val1>, <val2>)`, `IS_ZERO(<val>)`
70
+ - `ROW_COUNT_GT(<table>, N)`, `ROW_COUNT_GTE(<table>, N)`, `ROW_COUNT_EQ(<table>, N)`, `ROW_COUNT_LT(<table>, N)`
70
71
  - `DBMS(<type>)`, `SCHEMA_EXISTS(<schema>)`, `VIEW_EXISTS(<view>)`
71
72
  - Boolean combinators: `AND`, `OR`, `NOT`
72
73
 
@@ -116,6 +117,43 @@ The AUTOCOMMIT metacommand is database-specific, and affects only the database i
116
117
  The [IMPORT](#import) and [COPY](#copy) metacommands do not commit data changes while AUTOCOMMIT is off (except when the NEW or REPLACEMENT clauses are used with Firebird; in those cases the 'create table' statement that *execsql* generates and runs will be committed). The SQL statements generated by the [IMPORT](#import) and [COPY](#copy) metacommands are sent to the database, however. Therefore the AUTOCOMMIT metacommand is recommended when explicit transaction control is to be applied to the [IMPORT](#import) and [COPY](#copy) metacommands.
117
118
 
118
119
 
120
+ ## BREAKPOINT { #breakpoint }
121
+
122
+ ```
123
+ BREAKPOINT
124
+ ```
125
+
126
+ Pauses script execution and drops into an interactive debug REPL (read-eval-print loop) on the console. Use this to inspect state and step through a script interactively while debugging.
127
+
128
+ **Non-interactive safety:** If `sys.stdin` is not a TTY (e.g. CI pipelines, piped input, batch execution) the metacommand is silently skipped. Scripts will never hang in automation.
129
+
130
+ **REPL commands:**
131
+
132
+ | Command | Description |
133
+ |---------|-------------|
134
+ | `continue` or `c` | Resume script execution |
135
+ | `abort`, `q`, or `quit` | Halt the script with exit status 1 |
136
+ | `vars` | List all substitution variables and their current values |
137
+ | `$VARNAME` | Print the value of a single variable (also `&VAR`, `@VAR`) |
138
+ | `SELECT ...;` | Run an ad-hoc SQL query against the current database and pretty-print results |
139
+ | `next` or `n` | Execute the next script statement, then pause again (step mode) |
140
+ | `stack` | Show the command-list stack: script name, cursor index, and nesting depth |
141
+ | `help` | Show the list of available REPL commands |
142
+
143
+ Pressing Ctrl-D (EOF) or Ctrl-C (KeyboardInterrupt) at the `execsql debug>` prompt resumes execution, the same as typing `continue`.
144
+
145
+ **Example:**
146
+
147
+ ```sql
148
+ INSERT INTO staging SELECT * FROM raw_data;
149
+ -- !x! BREAKPOINT
150
+ -- Script pauses here. Inspect variables, run queries, then continue.
151
+ SELECT count(*) FROM staging;
152
+ ```
153
+
154
+ BREAKPOINT is silently skipped inside a `False` [IF](#if_cmd) block.
155
+
156
+
119
157
  ## BEGIN BATCH and END BATCH { #batch }
120
158
 
121
159
  ```
@@ -1435,6 +1473,62 @@ IS_GTE(<value1>, <value2>)
1435
1473
  Evaluates whether or not the first of the specified values is greater than or equal to the second value. If the values are not numeric, an error will occur, and script processing will halt.
1436
1474
 
1437
1475
 
1476
+ ### *ROW_COUNT_GT* { #row_count_gt }
1477
+
1478
+ ```
1479
+ ROW_COUNT_GT(<table_name>, <N>)
1480
+ ```
1481
+
1482
+ Evaluates whether the number of rows in the specified table or view is strictly greater than the integer *N*. Issues a `SELECT count(*)` query against the current database. An error will occur if the table does not exist or is not accessible.
1483
+
1484
+
1485
+ ### *ROW_COUNT_GTE* { #row_count_gte }
1486
+
1487
+ ```
1488
+ ROW_COUNT_GTE(<table_name>, <N>)
1489
+ ```
1490
+
1491
+ Evaluates whether the number of rows in the specified table or view is greater than or equal to the integer *N*.
1492
+
1493
+
1494
+ ### *ROW_COUNT_EQ* { #row_count_eq }
1495
+
1496
+ ```
1497
+ ROW_COUNT_EQ(<table_name>, <N>)
1498
+ ```
1499
+
1500
+ Evaluates whether the number of rows in the specified table or view is exactly equal to the integer *N*.
1501
+
1502
+
1503
+ ### *ROW_COUNT_LT* { #row_count_lt }
1504
+
1505
+ ```
1506
+ ROW_COUNT_LT(<table_name>, <N>)
1507
+ ```
1508
+
1509
+ Evaluates whether the number of rows in the specified table or view is strictly less than the integer *N*.
1510
+
1511
+ **Examples:**
1512
+
1513
+ ```sql
1514
+ -- Halt if the orders table is empty.
1515
+ -- !x! ASSERT ROW_COUNT_GT(orders, 0) "orders table must not be empty"
1516
+
1517
+ -- Only process if staging has at least 1000 rows.
1518
+ -- !x! IF (ROW_COUNT_GTE(staging, 1000))
1519
+ -- !x! WRITE "staging is ready"
1520
+ -- !x! ENDIF
1521
+
1522
+ -- Verify exactly 12 monthly records were loaded.
1523
+ -- !x! ASSERT ROW_COUNT_EQ(monthly_totals, 12) "expected 12 monthly rows"
1524
+
1525
+ -- Skip cleanup if the temp table already has fewer than 100 rows.
1526
+ -- !x! IF (ROW_COUNT_LT(temp_work, 100))
1527
+ -- !x! WRITE "temp_work is small, skipping truncate"
1528
+ -- !x! ENDIF
1529
+ ```
1530
+
1531
+
1438
1532
  ### *IS_NULL*
1439
1533
 
1440
1534
  ```
@@ -93,7 +93,7 @@
93
93
  },
94
94
  "action-keywords": {
95
95
  "comment": "sub, write, execute script, export, etc.",
96
- "match": "(?i)\\b(reset\\s+dialog_canceled|write\\s+create_table|export_metadata|sub_querystring|execute\\s+script|append\\s+script|extend\\s+script|reset\\s+counter|export\\s+query|sub_tempfile|write\\s+script|import_file|set\\s+counter|sub_decrypt|sub_encrypt|autocommit|copy\\s+query|disconnect|select_sub|sub_append|system_cmd|pg_vacuum|sub_empty|sub_local|connect|include|max_int|rm_file|subdata|sub_add|sub_ini|assert|export|import|rm_sub|debug|email|serve|write|copy|log|run|sub|use|zip|cd)\\b",
96
+ "match": "(?i)\\b(reset\\s+dialog_canceled|write\\s+create_table|export_metadata|sub_querystring|execute\\s+script|append\\s+script|extend\\s+script|reset\\s+counter|export\\s+query|sub_tempfile|write\\s+script|import_file|set\\s+counter|sub_decrypt|sub_encrypt|autocommit|breakpoint|copy\\s+query|disconnect|select_sub|sub_append|system_cmd|pg_vacuum|sub_empty|sub_local|connect|include|max_int|rm_file|subdata|sub_add|sub_ini|assert|export|import|rm_sub|debug|email|serve|write|copy|log|run|sub|use|zip|cd)\\b",
97
97
  "name": "keyword.other.execsql"
98
98
  },
99
99
  "config-event-keywords": {
@@ -108,7 +108,7 @@
108
108
  },
109
109
  "builtin-functions": {
110
110
  "comment": "Conditional test functions used in if/elseif",
111
- "match": "(?i)\\b(metacommand_error|directory_exists|dialog_canceled|alias_defined|column_exists|database_name|schema_exists|script_exists|table_exists|file_exists|role_exists|starts_with|sub_defined|view_exists|console_on|newer_date|newer_file|ends_with|identical|sql_error|sub_empty|contains|is_false|hasrows|is_null|is_true|is_zero|is_gte|equal|is_gt|dbms|not|or)\\b",
111
+ "match": "(?i)\\b(metacommand_error|directory_exists|dialog_canceled|alias_defined|column_exists|database_name|row_count_gte|schema_exists|script_exists|row_count_eq|row_count_gt|row_count_lt|table_exists|file_exists|role_exists|starts_with|sub_defined|view_exists|console_on|newer_date|newer_file|ends_with|identical|sql_error|sub_empty|contains|is_false|hasrows|is_null|is_true|is_zero|is_gte|equal|is_gt|dbms|not|or)\\b",
112
112
  "name": "support.function.execsql"
113
113
  },
114
114
  "config-option-names": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.9.0"
7
+ version = "2.10.0"
8
8
  description = "Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = { file = "LICENSE.txt" }
@@ -158,7 +158,7 @@ skip-magic-trailing-comma = false
158
158
  line-ending = "auto"
159
159
 
160
160
  [tool.bumpversion]
161
- current_version = "2.9.0"
161
+ current_version = "2.10.0"
162
162
  commit = true
163
163
  commit_args = "--no-verify"
164
164
  tag = true
@@ -100,6 +100,7 @@ from execsql.metacommands.debug import (
100
100
  x_debug_write_subvars,
101
101
  x_debug_write_config,
102
102
  )
103
+ from execsql.metacommands.debug_repl import x_breakpoint
103
104
  from execsql.metacommands.io import (
104
105
  x_export,
105
106
  x_export_query,
@@ -300,6 +301,8 @@ __all__ = [
300
301
  "x_debug_log_config",
301
302
  "x_debug_write_subvars",
302
303
  "x_debug_write_config",
304
+ # debug repl handlers
305
+ "x_breakpoint",
303
306
  # io handlers
304
307
  "x_export",
305
308
  "x_export_query",
@@ -71,6 +71,124 @@ def xf_hasrows(**kwargs: Any) -> bool:
71
71
  return nrows > 0
72
72
 
73
73
 
74
+ def _row_count(queryname: str, sql_context: str, metacommandline: str) -> int:
75
+ """Return the number of rows in *queryname*, raising ErrInfo on failure.
76
+
77
+ Args:
78
+ queryname: Table or view name to count rows in.
79
+ sql_context: The SQL string to include in error messages.
80
+ metacommandline: The full metacommand line for error context.
81
+
82
+ Returns:
83
+ Integer row count.
84
+
85
+ Raises:
86
+ ErrInfo: If the query fails or the result is not numeric.
87
+ """
88
+ sql = f"select count(*) from {queryname};"
89
+ try:
90
+ _hdrs, rec = _state.dbs.current().select_data(sql)
91
+ except ErrInfo:
92
+ raise
93
+ except Exception as e:
94
+ raise ErrInfo("db", sql, exception_msg=exception_desc()) from e
95
+ try:
96
+ return int(rec[0][0])
97
+ except (IndexError, TypeError, ValueError) as e:
98
+ raise ErrInfo(
99
+ type="cmd",
100
+ command_text=metacommandline,
101
+ other_msg=f"Could not read row count for {queryname}.",
102
+ ) from e
103
+
104
+
105
+ def _parse_row_count_n(raw: str, metacommandline: str) -> int:
106
+ """Parse and return the numeric threshold N from the matched group.
107
+
108
+ Args:
109
+ raw: The raw string captured by the regex group (``n``).
110
+ metacommandline: The full metacommand line for error context.
111
+
112
+ Returns:
113
+ Integer value of *raw*.
114
+
115
+ Raises:
116
+ ErrInfo: If *raw* cannot be parsed as an integer.
117
+ """
118
+ try:
119
+ return int(raw.strip())
120
+ except (ValueError, TypeError) as e:
121
+ raise ErrInfo(
122
+ type="cmd",
123
+ command_text=metacommandline,
124
+ other_msg=f"ROW_COUNT threshold must be an integer; got {raw!r}.",
125
+ ) from e
126
+
127
+
128
+ def xf_row_count_gt(**kwargs: Any) -> bool:
129
+ """Return True if the row count of *queryname* is strictly greater than N.
130
+
131
+ Args:
132
+ **kwargs: Named groups from the regex match, plus ``metacommandline``.
133
+ Required keys: ``queryname``, ``n``.
134
+
135
+ Returns:
136
+ True if ``count(*) > N``.
137
+ """
138
+ queryname = kwargs["queryname"]
139
+ mcl = kwargs["metacommandline"]
140
+ n = _parse_row_count_n(kwargs["n"], mcl)
141
+ return _row_count(queryname, f"select count(*) from {queryname};", mcl) > n
142
+
143
+
144
+ def xf_row_count_gte(**kwargs: Any) -> bool:
145
+ """Return True if the row count of *queryname* is greater than or equal to N.
146
+
147
+ Args:
148
+ **kwargs: Named groups from the regex match, plus ``metacommandline``.
149
+ Required keys: ``queryname``, ``n``.
150
+
151
+ Returns:
152
+ True if ``count(*) >= N``.
153
+ """
154
+ queryname = kwargs["queryname"]
155
+ mcl = kwargs["metacommandline"]
156
+ n = _parse_row_count_n(kwargs["n"], mcl)
157
+ return _row_count(queryname, f"select count(*) from {queryname};", mcl) >= n
158
+
159
+
160
+ def xf_row_count_eq(**kwargs: Any) -> bool:
161
+ """Return True if the row count of *queryname* equals N exactly.
162
+
163
+ Args:
164
+ **kwargs: Named groups from the regex match, plus ``metacommandline``.
165
+ Required keys: ``queryname``, ``n``.
166
+
167
+ Returns:
168
+ True if ``count(*) == N``.
169
+ """
170
+ queryname = kwargs["queryname"]
171
+ mcl = kwargs["metacommandline"]
172
+ n = _parse_row_count_n(kwargs["n"], mcl)
173
+ return _row_count(queryname, f"select count(*) from {queryname};", mcl) == n
174
+
175
+
176
+ def xf_row_count_lt(**kwargs: Any) -> bool:
177
+ """Return True if the row count of *queryname* is strictly less than N.
178
+
179
+ Args:
180
+ **kwargs: Named groups from the regex match, plus ``metacommandline``.
181
+ Required keys: ``queryname``, ``n``.
182
+
183
+ Returns:
184
+ True if ``count(*) < N``.
185
+ """
186
+ queryname = kwargs["queryname"]
187
+ mcl = kwargs["metacommandline"]
188
+ n = _parse_row_count_n(kwargs["n"], mcl)
189
+ return _row_count(queryname, f"select count(*) from {queryname};", mcl) < n
190
+
191
+
74
192
  def xf_sqlerror(**kwargs: Any) -> bool:
75
193
  return _state.status.sql_error
76
194
 
@@ -495,6 +613,36 @@ def build_conditional_table() -> Any:
495
613
  mcl.add(r"^\s*HASROWS\((?P<queryname>[^)]+)\)", xf_hasrows, description="HASROWS", category="condition")
496
614
  mcl.add(r"^\s*HAS_ROWS\((?P<queryname>[^)]+)\)", xf_hasrows)
497
615
 
616
+ # ROW_COUNT comparisons — ROW_COUNT_GT/GTE/EQ/LT(table, N)
617
+ # Table name: unquoted, double-quoted, or single-quoted. N: integer literal.
618
+ _rc_table = r"(?P<queryname>[A-Za-z0-9_.\"'\[\]]+)"
619
+ _rc_n = r"(?P<n>\d+)"
620
+ _rc_sep = r"\s*,\s*"
621
+ mcl.add(
622
+ rf"^\s*ROW_COUNT_GT\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
623
+ xf_row_count_gt,
624
+ description="ROW_COUNT_GT",
625
+ category="condition",
626
+ )
627
+ mcl.add(
628
+ rf"^\s*ROW_COUNT_GTE\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
629
+ xf_row_count_gte,
630
+ description="ROW_COUNT_GTE",
631
+ category="condition",
632
+ )
633
+ mcl.add(
634
+ rf"^\s*ROW_COUNT_EQ\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
635
+ xf_row_count_eq,
636
+ description="ROW_COUNT_EQ",
637
+ category="condition",
638
+ )
639
+ mcl.add(
640
+ rf"^\s*ROW_COUNT_LT\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
641
+ xf_row_count_lt,
642
+ description="ROW_COUNT_LT",
643
+ category="condition",
644
+ )
645
+
498
646
  # Status predicates
499
647
  mcl.add(r"^\s*sql_error\(\s*\)", xf_sqlerror, description="SQL_ERROR", category="condition")
500
648
  mcl.add(r"^\s*dialog_canceled\(\s*\)", xf_dialogcanceled, description="DIALOG_CANCELED", category="condition")
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Interactive debug REPL metacommand handler for execsql.
5
+
6
+ Implements ``x_breakpoint`` — the ``BREAKPOINT`` metacommand — which pauses
7
+ script execution and drops into an interactive read-eval-print loop.
8
+
9
+ The REPL allows the user to:
10
+
11
+ - Inspect and print substitution variables.
12
+ - Run ad-hoc SQL queries against the current database.
13
+ - Step through the script one statement at a time.
14
+ - Resume or abort execution.
15
+
16
+ In non-interactive environments (CI, piped input, ``sys.stdin.isatty()`` is
17
+ ``False``) the metacommand is silently skipped so automated pipelines are not
18
+ blocked.
19
+ """
20
+
21
+ import sys
22
+ from typing import Any
23
+
24
+ import execsql.state as _state
25
+
26
+ __all__ = ["x_breakpoint"]
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Public handler
30
+ # ---------------------------------------------------------------------------
31
+
32
+ _HELP_TEXT = """\
33
+ execsql debug REPL commands:
34
+ continue c Resume script execution
35
+ abort q quit Halt the script (exit 1)
36
+ vars List all substitution variables and their values
37
+ $VARNAME Print a single variable's value (also &VAR, @VAR)
38
+ SELECT ...; Run ad-hoc SQL against the current database
39
+ next n Execute the next statement then pause again (step mode)
40
+ stack Show the command-list stack (script name, line, depth)
41
+ help Show this help text
42
+ """
43
+
44
+
45
+ def x_breakpoint(**kwargs: Any) -> None:
46
+ """Pause execution and enter the interactive debug REPL.
47
+
48
+ If ``sys.stdin`` is not a TTY (CI, piped input), the metacommand is
49
+ silently skipped — scripts will not hang in automation.
50
+
51
+ Args:
52
+ **kwargs: Keyword arguments injected by the dispatch table (unused).
53
+ """
54
+ if not sys.stdin.isatty():
55
+ return
56
+ _debug_repl()
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # REPL core
61
+ # ---------------------------------------------------------------------------
62
+
63
+
64
+ def _debug_repl() -> None:
65
+ """Interactive read-eval-print loop for script debugging.
66
+
67
+ Reads commands from stdin until the user types ``continue`` or ``abort``,
68
+ or until EOF / KeyboardInterrupt.
69
+ """
70
+ try:
71
+ import readline as _readline # noqa: F401 — side-effect: enables history/arrow keys
72
+ except ImportError:
73
+ pass # readline not available on Windows; continue without it
74
+
75
+ _write("\n[Breakpoint] Script paused. Type 'help' for commands, 'continue' to resume.\n")
76
+
77
+ while True:
78
+ try:
79
+ line = input("execsql debug> ").strip()
80
+ except EOFError:
81
+ _write("\n")
82
+ return # Ctrl-D → continue
83
+ except KeyboardInterrupt:
84
+ _write("\n")
85
+ return # Ctrl-C → continue
86
+
87
+ if not line:
88
+ continue
89
+
90
+ lower = line.lower()
91
+
92
+ if lower in ("continue", "c"):
93
+ return
94
+ elif lower in ("abort", "q", "quit"):
95
+ raise SystemExit(1)
96
+ elif lower == "help":
97
+ _write(_HELP_TEXT)
98
+ elif lower == "vars":
99
+ _print_all_vars()
100
+ elif lower == "stack":
101
+ _print_stack()
102
+ elif lower in ("next", "n"):
103
+ _enable_step_mode()
104
+ return
105
+ elif line[0] in ("$", "&", "@"):
106
+ _print_var(line)
107
+ elif line.rstrip().endswith(";"):
108
+ _run_sql(line)
109
+ else:
110
+ _write(f"Unknown command: {line!r}. Type 'help' for available commands.\n")
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # REPL command implementations
115
+ # ---------------------------------------------------------------------------
116
+
117
+
118
+ def _write(text: str) -> None:
119
+ """Write *text* to the execsql output stream (falls back to stdout)."""
120
+ output = _state.output
121
+ if output is not None:
122
+ output.write(text)
123
+ else:
124
+ sys.stdout.write(text)
125
+ sys.stdout.flush()
126
+
127
+
128
+ def _print_all_vars() -> None:
129
+ """Print all substitution variables and their current values."""
130
+ subvars = _state.subvars
131
+ if subvars is None:
132
+ _write(" (no substitution variables defined)\n")
133
+ return
134
+ items = subvars.substitutions # list of (name, value) tuples
135
+ if not items:
136
+ _write(" (no substitution variables defined)\n")
137
+ return
138
+ # Compute column width for aligned output.
139
+ max_name = max((len(name) for name, _ in items), default=0)
140
+ for name, value in sorted(items):
141
+ _write(f" {name:<{max_name}} = {value!r}\n")
142
+
143
+
144
+ def _print_var(varname: str) -> None:
145
+ """Print the value of a single substitution variable.
146
+
147
+ Args:
148
+ varname: The variable reference as typed by the user, e.g. ``$FOO``.
149
+ """
150
+ subvars = _state.subvars
151
+ if subvars is None:
152
+ _write(f" {varname}: (substitution variables not initialised)\n")
153
+ return
154
+ # varvalue() expects the name with its prefix (e.g. "$foo"); it lowercases internally.
155
+ value = subvars.varvalue(varname)
156
+ if value is None:
157
+ _write(f" {varname}: (undefined)\n")
158
+ else:
159
+ _write(f" {varname} = {value!r}\n")
160
+
161
+
162
+ def _print_stack() -> None:
163
+ """Print the current command-list stack (script name, line number, depth)."""
164
+ stack = _state.commandliststack
165
+ if not stack:
166
+ _write(" (command list stack is empty)\n")
167
+ return
168
+ _write(f" Stack depth: {len(stack)}\n")
169
+ for depth, cmdlist in enumerate(stack):
170
+ listname = getattr(cmdlist, "listname", "<unknown>")
171
+ cmdptr = getattr(cmdlist, "cmdptr", 0)
172
+ _write(f" [{depth}] {listname} (cursor at index {cmdptr})\n")
173
+
174
+
175
+ def _run_sql(sql: str) -> None:
176
+ """Execute ad-hoc SQL against the current database and pretty-print the results.
177
+
178
+ Args:
179
+ sql: A complete SQL statement ending with a semicolon.
180
+ """
181
+ dbs = _state.dbs
182
+ if dbs is None:
183
+ _write(" (no database connection is active)\n")
184
+ return
185
+ db = dbs.current()
186
+ if db is None:
187
+ _write(" (no database connection is active)\n")
188
+ return
189
+ try:
190
+ colnames, rows = db.select_data(sql)
191
+ except Exception as exc:
192
+ _write(f" SQL error: {exc}\n")
193
+ return
194
+
195
+ if not colnames:
196
+ _write(" (query returned no columns)\n")
197
+ return
198
+
199
+ # Build a simple text table.
200
+ col_widths = [len(c) for c in colnames]
201
+ str_rows: list[list[str]] = []
202
+ for row in rows:
203
+ str_row = [str(v) if v is not None else "NULL" for v in row]
204
+ str_rows.append(str_row)
205
+ for i, cell in enumerate(str_row):
206
+ col_widths[i] = max(col_widths[i], len(cell))
207
+
208
+ sep = "+-" + "-+-".join("-" * w for w in col_widths) + "-+"
209
+ header = "| " + " | ".join(c.ljust(col_widths[i]) for i, c in enumerate(colnames)) + " |"
210
+ _write(sep + "\n")
211
+ _write(header + "\n")
212
+ _write(sep + "\n")
213
+ for str_row in str_rows:
214
+ data_line = "| " + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(str_row)) + " |"
215
+ _write(data_line + "\n")
216
+ _write(sep + "\n")
217
+ row_word = "row" if len(str_rows) == 1 else "rows"
218
+ _write(f" ({len(str_rows)} {row_word})\n")
219
+
220
+
221
+ def _enable_step_mode() -> None:
222
+ """Activate step mode so the engine re-enters the REPL after the next statement."""
223
+ _state.step_mode = True
@@ -100,6 +100,7 @@ from execsql.metacommands.debug import (
100
100
  x_debug_write_odbc_drivers,
101
101
  x_debug_write_subvars,
102
102
  )
103
+ from execsql.metacommands.debug_repl import x_breakpoint
103
104
  from execsql.metacommands.io import (
104
105
  x_cd,
105
106
  x_copy,
@@ -1690,6 +1691,17 @@ def build_dispatch_table() -> MetaCommandList:
1690
1691
  run_when_false=False,
1691
1692
  )
1692
1693
 
1694
+ # ------------------------------------------------------------------
1695
+ # BREAKPOINT
1696
+ # ------------------------------------------------------------------
1697
+ mcl.add(
1698
+ r"^\s*BREAKPOINT\s*$",
1699
+ x_breakpoint,
1700
+ description="BREAKPOINT",
1701
+ category="action",
1702
+ run_when_false=False,
1703
+ )
1704
+
1693
1705
  # ------------------------------------------------------------------
1694
1706
  # IF / ORIF / ANDIF / ELSEIF / ELSE / ENDIF
1695
1707
  # ------------------------------------------------------------------
@@ -506,6 +506,11 @@ class CommandList:
506
506
  cmditem.command.commandline()[:100],
507
507
  ),
508
508
  )
509
+ if _state.step_mode:
510
+ _state.step_mode = False
511
+ from execsql.metacommands.debug_repl import _debug_repl
512
+
513
+ _debug_repl()
509
514
  self.cmdptr += 1
510
515
 
511
516
  def run_next(self) -> None: