execsql2 2.13.0__tar.gz → 2.13.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 (302) hide show
  1. {execsql2-2.13.0 → execsql2-2.13.2}/CHANGELOG.md +25 -0
  2. {execsql2-2.13.0 → execsql2-2.13.2}/PKG-INFO +3 -3
  3. {execsql2-2.13.0 → execsql2-2.13.2}/docs/about/divergence.md +15 -15
  4. {execsql2-2.13.0 → execsql2-2.13.2}/docs/getting-started/syntax.md +6 -1
  5. {execsql2-2.13.0 → execsql2-2.13.2}/pyproject.toml +3 -3
  6. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/cli/lint.py +277 -92
  7. {execsql2-2.13.0 → execsql2-2.13.2}/tests/cli/test_lint.py +270 -0
  8. {execsql2-2.13.0 → execsql2-2.13.2}/uv.lock +5 -5
  9. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/agents/dba.md +0 -0
  10. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/agents/herald.md +0 -0
  11. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/agents/inspector.md +0 -0
  12. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/agents/liaison.md +0 -0
  13. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/agents/oracle.md +0 -0
  14. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/agents/patcher.md +0 -0
  15. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/agents/qa.md +0 -0
  16. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/agents/scribe.md +0 -0
  17. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/commands/code-oracle.md +0 -0
  18. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/commands/migrate.md +0 -0
  19. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/commands/review-changes.md +0 -0
  20. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/commands/test-module.md +0 -0
  21. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/commands/update-changelog.md +0 -0
  22. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/commands/where-is.md +0 -0
  23. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/project_context.md +0 -0
  24. {execsql2-2.13.0 → execsql2-2.13.2}/.claude/state/status.md +0 -0
  25. {execsql2-2.13.0 → execsql2-2.13.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  26. {execsql2-2.13.0 → execsql2-2.13.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  27. {execsql2-2.13.0 → execsql2-2.13.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  28. {execsql2-2.13.0 → execsql2-2.13.2}/.github/workflows/ci-cd.yml +0 -0
  29. {execsql2-2.13.0 → execsql2-2.13.2}/.gitignore +0 -0
  30. {execsql2-2.13.0 → execsql2-2.13.2}/.pre-commit-config.yaml +0 -0
  31. {execsql2-2.13.0 → execsql2-2.13.2}/.pre-commit-hooks.yaml +0 -0
  32. {execsql2-2.13.0 → execsql2-2.13.2}/.python-version +0 -0
  33. {execsql2-2.13.0 → execsql2-2.13.2}/.readthedocs.yaml +0 -0
  34. {execsql2-2.13.0 → execsql2-2.13.2}/CLAUDE.md +0 -0
  35. {execsql2-2.13.0 → execsql2-2.13.2}/CONTRIBUTING.md +0 -0
  36. {execsql2-2.13.0 → execsql2-2.13.2}/LICENSE.txt +0 -0
  37. {execsql2-2.13.0 → execsql2-2.13.2}/NOTICE +0 -0
  38. {execsql2-2.13.0 → execsql2-2.13.2}/README.md +0 -0
  39. {execsql2-2.13.0 → execsql2-2.13.2}/SECURITY.md +0 -0
  40. {execsql2-2.13.0 → execsql2-2.13.2}/docs/about/contributors.md +0 -0
  41. {execsql2-2.13.0 → execsql2-2.13.2}/docs/about/copyright.md +0 -0
  42. {execsql2-2.13.0 → execsql2-2.13.2}/docs/api/cli.md +0 -0
  43. {execsql2-2.13.0 → execsql2-2.13.2}/docs/api/db.md +0 -0
  44. {execsql2-2.13.0 → execsql2-2.13.2}/docs/api/exporters.md +0 -0
  45. {execsql2-2.13.0 → execsql2-2.13.2}/docs/api/importers.md +0 -0
  46. {execsql2-2.13.0 → execsql2-2.13.2}/docs/api/index.md +0 -0
  47. {execsql2-2.13.0 → execsql2-2.13.2}/docs/api/metacommands.md +0 -0
  48. {execsql2-2.13.0 → execsql2-2.13.2}/docs/dev/adding_db_adapters.md +0 -0
  49. {execsql2-2.13.0 → execsql2-2.13.2}/docs/dev/adding_exporters.md +0 -0
  50. {execsql2-2.13.0 → execsql2-2.13.2}/docs/dev/adding_importers.md +0 -0
  51. {execsql2-2.13.0 → execsql2-2.13.2}/docs/dev/adding_metacommands.md +0 -0
  52. {execsql2-2.13.0 → execsql2-2.13.2}/docs/dev/architecture.md +0 -0
  53. {execsql2-2.13.0 → execsql2-2.13.2}/docs/getting-started/installation.md +0 -0
  54. {execsql2-2.13.0 → execsql2-2.13.2}/docs/getting-started/requirements.md +0 -0
  55. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/debugging.md +0 -0
  56. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/documentation.md +0 -0
  57. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/encoding.md +0 -0
  58. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/examples.md +0 -0
  59. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/formatter.md +0 -0
  60. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/logging.md +0 -0
  61. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/sql_syntax.md +0 -0
  62. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/usage.md +0 -0
  63. {execsql2-2.13.0 → execsql2-2.13.2}/docs/guides/using_scripts.md +0 -0
  64. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/Compare_planets.png +0 -0
  65. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/actions.png +0 -0
  66. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/actions2.png +0 -0
  67. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/checkboxes.png +0 -0
  68. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/connect.b64 +0 -0
  69. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/connect.png +0 -0
  70. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/create_conf.png +0 -0
  71. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/data_error1_screenshot.jpg +0 -0
  72. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/entry_form.png +0 -0
  73. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/execsql_console.png +0 -0
  74. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/execsql_logo_01.png +0 -0
  75. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/fatals.png +0 -0
  76. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/logo_small.png +0 -0
  77. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/pause_terminal.png +0 -0
  78. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/pause_terminal_sm.b64 +0 -0
  79. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/pause_terminal_sm.png +0 -0
  80. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/prompt_compare.png +0 -0
  81. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/set_build_commands.jpg +0 -0
  82. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/unit_conversions.b64 +0 -0
  83. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/unit_conversions_029.png +0 -0
  84. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/unmatched.png +0 -0
  85. {execsql2-2.13.0 → execsql2-2.13.2}/docs/images/vim_execsql_highlight.png +0 -0
  86. {execsql2-2.13.0 → execsql2-2.13.2}/docs/index.md +0 -0
  87. {execsql2-2.13.0 → execsql2-2.13.2}/docs/reference/configuration.md +0 -0
  88. {execsql2-2.13.0 → execsql2-2.13.2}/docs/reference/metacommands.md +0 -0
  89. {execsql2-2.13.0 → execsql2-2.13.2}/docs/reference/security.md +0 -0
  90. {execsql2-2.13.0 → execsql2-2.13.2}/docs/reference/substitution_vars.md +0 -0
  91. {execsql2-2.13.0 → execsql2-2.13.2}/extras/vscode-execsql/README.md +0 -0
  92. {execsql2-2.13.0 → execsql2-2.13.2}/extras/vscode-execsql/package.json +0 -0
  93. {execsql2-2.13.0 → execsql2-2.13.2}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  94. {execsql2-2.13.0 → execsql2-2.13.2}/justfile +0 -0
  95. {execsql2-2.13.0 → execsql2-2.13.2}/scripts/generate_vscode_grammar.py +0 -0
  96. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/__init__.py +0 -0
  97. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/__main__.py +0 -0
  98. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/cli/__init__.py +0 -0
  99. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/cli/dsn.py +0 -0
  100. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/cli/help.py +0 -0
  101. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/cli/run.py +0 -0
  102. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/config.py +0 -0
  103. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/constants.py +0 -0
  104. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/__init__.py +0 -0
  105. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/access.py +0 -0
  106. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/base.py +0 -0
  107. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/dsn.py +0 -0
  108. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/duckdb.py +0 -0
  109. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/factory.py +0 -0
  110. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/firebird.py +0 -0
  111. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/mysql.py +0 -0
  112. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/oracle.py +0 -0
  113. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/postgres.py +0 -0
  114. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/sqlite.py +0 -0
  115. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/db/sqlserver.py +0 -0
  116. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/debug/__init__.py +0 -0
  117. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/debug/repl.py +0 -0
  118. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exceptions.py +0 -0
  119. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/__init__.py +0 -0
  120. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/base.py +0 -0
  121. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/delimited.py +0 -0
  122. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/duckdb.py +0 -0
  123. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/feather.py +0 -0
  124. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/html.py +0 -0
  125. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/json.py +0 -0
  126. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/latex.py +0 -0
  127. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/markdown.py +0 -0
  128. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/ods.py +0 -0
  129. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/parquet.py +0 -0
  130. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/pretty.py +0 -0
  131. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/protocol.py +0 -0
  132. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/raw.py +0 -0
  133. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/sqlite.py +0 -0
  134. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/templates.py +0 -0
  135. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/values.py +0 -0
  136. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/xls.py +0 -0
  137. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/xlsx.py +0 -0
  138. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/xml.py +0 -0
  139. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/yaml.py +0 -0
  140. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/exporters/zip.py +0 -0
  141. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/format.py +0 -0
  142. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/gui/__init__.py +0 -0
  143. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/gui/base.py +0 -0
  144. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/gui/console.py +0 -0
  145. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/gui/desktop.py +0 -0
  146. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/gui/tui.py +0 -0
  147. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/importers/__init__.py +0 -0
  148. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/importers/base.py +0 -0
  149. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/importers/csv.py +0 -0
  150. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/importers/feather.py +0 -0
  151. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/importers/json.py +0 -0
  152. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/importers/ods.py +0 -0
  153. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/importers/xls.py +0 -0
  154. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/__init__.py +0 -0
  155. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/conditions.py +0 -0
  156. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/connect.py +0 -0
  157. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/control.py +0 -0
  158. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/data.py +0 -0
  159. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/debug.py +0 -0
  160. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/dispatch.py +0 -0
  161. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/io.py +0 -0
  162. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/io_export.py +0 -0
  163. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/io_fileops.py +0 -0
  164. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/io_import.py +0 -0
  165. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/io_write.py +0 -0
  166. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/prompt.py +0 -0
  167. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/script_ext.py +0 -0
  168. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/system.py +0 -0
  169. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/metacommands/upsert.py +0 -0
  170. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/models.py +0 -0
  171. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/parser.py +0 -0
  172. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/py.typed +0 -0
  173. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/script/__init__.py +0 -0
  174. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/script/control.py +0 -0
  175. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/script/engine.py +0 -0
  176. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/script/variables.py +0 -0
  177. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/state.py +0 -0
  178. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/types.py +0 -0
  179. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/__init__.py +0 -0
  180. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/auth.py +0 -0
  181. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/crypto.py +0 -0
  182. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/datetime.py +0 -0
  183. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/errors.py +0 -0
  184. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/fileio.py +0 -0
  185. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/gui.py +0 -0
  186. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/mail.py +0 -0
  187. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/numeric.py +0 -0
  188. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/regex.py +0 -0
  189. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/strings.py +0 -0
  190. {execsql2-2.13.0 → execsql2-2.13.2}/src/execsql/utils/timer.py +0 -0
  191. {execsql2-2.13.0 → execsql2-2.13.2}/templates/README.md +0 -0
  192. {execsql2-2.13.0 → execsql2-2.13.2}/templates/config_settings.sqlite +0 -0
  193. {execsql2-2.13.0 → execsql2-2.13.2}/templates/example_config_prompt.sql +0 -0
  194. {execsql2-2.13.0 → execsql2-2.13.2}/templates/execsql.conf +0 -0
  195. {execsql2-2.13.0 → execsql2-2.13.2}/templates/make_config_db.sql +0 -0
  196. {execsql2-2.13.0 → execsql2-2.13.2}/templates/md_compare.sql +0 -0
  197. {execsql2-2.13.0 → execsql2-2.13.2}/templates/md_glossary.sql +0 -0
  198. {execsql2-2.13.0 → execsql2-2.13.2}/templates/md_upsert.sql +0 -0
  199. {execsql2-2.13.0 → execsql2-2.13.2}/templates/pg_compare.sql +0 -0
  200. {execsql2-2.13.0 → execsql2-2.13.2}/templates/pg_glossary.sql +0 -0
  201. {execsql2-2.13.0 → execsql2-2.13.2}/templates/pg_upsert.sql +0 -0
  202. {execsql2-2.13.0 → execsql2-2.13.2}/templates/script_template.sql +0 -0
  203. {execsql2-2.13.0 → execsql2-2.13.2}/templates/ss_compare.sql +0 -0
  204. {execsql2-2.13.0 → execsql2-2.13.2}/templates/ss_glossary.sql +0 -0
  205. {execsql2-2.13.0 → execsql2-2.13.2}/templates/ss_upsert.sql +0 -0
  206. {execsql2-2.13.0 → execsql2-2.13.2}/tests/__init__.py +0 -0
  207. {execsql2-2.13.0 → execsql2-2.13.2}/tests/cli/__init__.py +0 -0
  208. {execsql2-2.13.0 → execsql2-2.13.2}/tests/cli/test_cli.py +0 -0
  209. {execsql2-2.13.0 → execsql2-2.13.2}/tests/cli/test_cli_e2e.py +0 -0
  210. {execsql2-2.13.0 → execsql2-2.13.2}/tests/cli/test_cli_run.py +0 -0
  211. {execsql2-2.13.0 → execsql2-2.13.2}/tests/cli/test_ping.py +0 -0
  212. {execsql2-2.13.0 → execsql2-2.13.2}/tests/cli/test_profile.py +0 -0
  213. {execsql2-2.13.0 → execsql2-2.13.2}/tests/conftest.py +0 -0
  214. {execsql2-2.13.0 → execsql2-2.13.2}/tests/db/__init__.py +0 -0
  215. {execsql2-2.13.0 → execsql2-2.13.2}/tests/db/test_base.py +0 -0
  216. {execsql2-2.13.0 → execsql2-2.13.2}/tests/db/test_duckdb.py +0 -0
  217. {execsql2-2.13.0 → execsql2-2.13.2}/tests/db/test_factory.py +0 -0
  218. {execsql2-2.13.0 → execsql2-2.13.2}/tests/db/test_postgres.py +0 -0
  219. {execsql2-2.13.0 → execsql2-2.13.2}/tests/db/test_sqlite.py +0 -0
  220. {execsql2-2.13.0 → execsql2-2.13.2}/tests/db/test_sqlite_extra.py +0 -0
  221. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/__init__.py +0 -0
  222. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_base.py +0 -0
  223. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_db.py +0 -0
  224. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_delimited.py +0 -0
  225. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_duckdb_exporter.py +0 -0
  226. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_exporters.py +0 -0
  227. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_feather.py +0 -0
  228. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_html_latex.py +0 -0
  229. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_json.py +0 -0
  230. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_markdown.py +0 -0
  231. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_ods.py +0 -0
  232. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_parquet.py +0 -0
  233. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_sqlite_exporter.py +0 -0
  234. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_templates.py +0 -0
  235. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_xls_xlsx.py +0 -0
  236. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_xlsx.py +0 -0
  237. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_xml.py +0 -0
  238. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_yaml.py +0 -0
  239. {execsql2-2.13.0 → execsql2-2.13.2}/tests/exporters/test_zip.py +0 -0
  240. {execsql2-2.13.0 → execsql2-2.13.2}/tests/gui/__init__.py +0 -0
  241. {execsql2-2.13.0 → execsql2-2.13.2}/tests/gui/test_backends.py +0 -0
  242. {execsql2-2.13.0 → execsql2-2.13.2}/tests/importers/__init__.py +0 -0
  243. {execsql2-2.13.0 → execsql2-2.13.2}/tests/importers/test_csv_importer.py +0 -0
  244. {execsql2-2.13.0 → execsql2-2.13.2}/tests/importers/test_feather_importer.py +0 -0
  245. {execsql2-2.13.0 → execsql2-2.13.2}/tests/importers/test_json_importer.py +0 -0
  246. {execsql2-2.13.0 → execsql2-2.13.2}/tests/importers/test_ods_importer.py +0 -0
  247. {execsql2-2.13.0 → execsql2-2.13.2}/tests/importers/test_xls_importer.py +0 -0
  248. {execsql2-2.13.0 → execsql2-2.13.2}/tests/integration/__init__.py +0 -0
  249. {execsql2-2.13.0 → execsql2-2.13.2}/tests/integration/conftest.py +0 -0
  250. {execsql2-2.13.0 → execsql2-2.13.2}/tests/integration/test_dsn.py +0 -0
  251. {execsql2-2.13.0 → execsql2-2.13.2}/tests/integration/test_duckdb.py +0 -0
  252. {execsql2-2.13.0 → execsql2-2.13.2}/tests/integration/test_mysql.py +0 -0
  253. {execsql2-2.13.0 → execsql2-2.13.2}/tests/integration/test_postgres.py +0 -0
  254. {execsql2-2.13.0 → execsql2-2.13.2}/tests/integration/test_sqlite.py +0 -0
  255. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/__init__.py +0 -0
  256. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_assert.py +0 -0
  257. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_breakpoint.py +0 -0
  258. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_connect.py +0 -0
  259. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_io_export.py +0 -0
  260. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_io_import.py +0 -0
  261. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands.py +0 -0
  262. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_connect.py +0 -0
  263. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_data.py +0 -0
  264. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_extended.py +0 -0
  265. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  266. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_io.py +0 -0
  267. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  268. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  269. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_system.py +0 -0
  270. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  271. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_pg_upsert.py +0 -0
  272. {execsql2-2.13.0 → execsql2-2.13.2}/tests/metacommands/test_row_count.py +0 -0
  273. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_config.py +0 -0
  274. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_config_data.py +0 -0
  275. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_constants.py +0 -0
  276. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_engine.py +0 -0
  277. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_error_messages.py +0 -0
  278. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_exceptions.py +0 -0
  279. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_format.py +0 -0
  280. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_mail.py +0 -0
  281. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_models.py +0 -0
  282. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_package.py +0 -0
  283. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_parser.py +0 -0
  284. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_registry.py +0 -0
  285. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_script.py +0 -0
  286. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_state.py +0 -0
  287. {execsql2-2.13.0 → execsql2-2.13.2}/tests/test_types.py +0 -0
  288. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/__init__.py +0 -0
  289. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_auth.py +0 -0
  290. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_auth_extra.py +0 -0
  291. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_crypto.py +0 -0
  292. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_datetime.py +0 -0
  293. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_errors.py +0 -0
  294. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_errors_extra.py +0 -0
  295. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_fileio.py +0 -0
  296. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_fileio_extra.py +0 -0
  297. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_numeric.py +0 -0
  298. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_regex.py +0 -0
  299. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_strings.py +0 -0
  300. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_timer.py +0 -0
  301. {execsql2-2.13.0 → execsql2-2.13.2}/tests/utils/test_timer_extra.py +0 -0
  302. {execsql2-2.13.0 → execsql2-2.13.2}/zensical.toml +0 -0
@@ -13,6 +13,31 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.13.2] - 2026-04-06
17
+
18
+ ### Changed
19
+
20
+ - `--lint` static analysis improvements:
21
+ - Track `SUB_EMPTY`, `SUB_ADD`, `SUB_APPEND`, and `SUBDATA` as variable definitions, eliminating false undefined-variable warnings.
22
+ - Descend into named script blocks via `EXECUTE SCRIPT` / `EXEC SCRIPT` / `RUN SCRIPT` so variables defined inside are visible to the caller.
23
+ - Two-pass variable collection: definition order no longer matters. Variables can be referenced before their SUB definition without false warnings.
24
+ - Read `SUB_INI` INI files at lint time and register section keys as defined variables.
25
+ - Auto-discover built-in system variables by scanning installed source instead of a hand-maintained list.
26
+ - Exclude `$COUNTER_N` variables from undefined-variable warnings.
27
+ - Warn when `EXECUTE SCRIPT` targets a non-existent script block (respects `IF EXISTS`).
28
+ - Eliminate duplicate warnings for script blocks reached via multiple execution paths.
29
+ - Sort errors before warnings, both by line number. Pad location columns for alignment.
30
+
31
+ ______________________________________________________________________
32
+
33
+ ## [2.13.1] - 2026-04-04
34
+
35
+ ### Changed
36
+
37
+ - Bump pg-upsert minimum to >=1.20.0.
38
+
39
+ ______________________________________________________________________
40
+
16
41
  ## [2.13.0] - 2026-04-04
17
42
 
18
43
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.13.0
3
+ Version: 2.13.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
@@ -51,7 +51,7 @@ Requires-Dist: keyring; extra == 'all'
51
51
  Requires-Dist: odfpy; extra == 'all'
52
52
  Requires-Dist: openpyxl; extra == 'all'
53
53
  Requires-Dist: oracledb; extra == 'all'
54
- Requires-Dist: pg-upsert>=1.18.2; extra == 'all'
54
+ Requires-Dist: pg-upsert>=1.20.0; extra == 'all'
55
55
  Requires-Dist: polars; extra == 'all'
56
56
  Requires-Dist: psycopg2-binary; extra == 'all'
57
57
  Requires-Dist: pymysql; extra == 'all'
@@ -109,7 +109,7 @@ Requires-Dist: oracledb; extra == 'oracle'
109
109
  Provides-Extra: postgres
110
110
  Requires-Dist: psycopg2-binary; extra == 'postgres'
111
111
  Provides-Extra: upsert
112
- Requires-Dist: pg-upsert>=1.18.2; extra == 'upsert'
112
+ Requires-Dist: pg-upsert>=1.20.0; extra == 'upsert'
113
113
  Description-Content-Type: text/markdown
114
114
 
115
115
  > [!NOTE]
@@ -13,21 +13,21 @@ ______________________________________________________________________
13
13
 
14
14
  ### CLI Options
15
15
 
16
- | Flag | Description |
17
- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
- | `--version` | Print version and exit (Rich-formatted). |
19
- | `-c` / `--command` | Execute an inline SQL or metacommand string instead of a script file. |
20
- | `--dsn` / `--connection-string` | Accept a standard database URL (e.g. `postgresql://user:pass@host/db`). Supports `postgresql`, `mysql`, `mssql`, `oracle`, `firebird`, `sqlite`, and `duckdb` schemes. |
21
- | `--output-dir` | Set a default base directory for export output files. |
22
- | `--progress` | Show a Rich progress bar during long-running IMPORT operations. |
23
- | `--dump-keywords` | Emit all metacommand keywords, conditionals, config options, and export formats as structured JSON. |
24
- | `--gui-framework` | Select GUI backend: `tkinter` (default) or `textual` (terminal UI). |
25
- | `--debug` | Start in step-through debug mode. The debug REPL pauses before each statement, as if `BREAKPOINT` were at the top with `.next` always active. |
26
- | `--dry-run` | Parse the script and print the full command list without connecting to a database or executing anything. Substitution variables already populated at parse time (env vars, `--assign-arg` values, built-in start-time vars) are expanded in the output; execution-time variables (`$DB_NAME`, `$CURRENT_TIME`, etc.) remain unexpanded. |
27
- | `--profile` | Record wall-clock time for each SQL and metacommand statement. After the script completes, print a summary table sorted by elapsed time (descending), showing time, percentage of total, source location, command type, and a command preview. |
28
- | `--profile-limit N` | Number of top statements to display in the `--profile` summary (default: 20). Remaining statements are counted and noted in the output footer. |
29
- | `--ping` | Test database connectivity and exit. Connects using the supplied connection parameters, queries for the server version, and prints a one-line success message (exit 0) or the error (exit 1). No script file argument is required. |
30
- | `--lint` | Parse the script and perform static analysis without connecting to a database. Reports unmatched IF/ENDIF, LOOP/END LOOP, and BEGIN BATCH/END BATCH blocks (errors), potentially undefined `!!$VAR!!` references (warnings), and missing INCLUDE file targets (warnings). Exits 0 if no errors, 1 if errors found. |
16
+ | Flag | Description |
17
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
18
+ | `--version` | Print version and exit (Rich-formatted). |
19
+ | `-c` / `--command` | Execute an inline SQL or metacommand string instead of a script file. |
20
+ | `--dsn` / `--connection-string` | Accept a standard database URL (e.g. `postgresql://user:pass@host/db`). Supports `postgresql`, `mysql`, `mssql`, `oracle`, `firebird`, `sqlite`, and `duckdb` schemes. |
21
+ | `--output-dir` | Set a default base directory for export output files. |
22
+ | `--progress` | Show a Rich progress bar during long-running IMPORT operations. |
23
+ | `--dump-keywords` | Emit all metacommand keywords, conditionals, config options, and export formats as structured JSON. |
24
+ | `--gui-framework` | Select GUI backend: `tkinter` (default) or `textual` (terminal UI). |
25
+ | `--debug` | Start in step-through debug mode. The debug REPL pauses before each statement, as if `BREAKPOINT` were at the top with `.next` always active. |
26
+ | `--dry-run` | Parse the script and print the full command list without connecting to a database or executing anything. Substitution variables already populated at parse time (env vars, `--assign-arg` values, built-in start-time vars) are expanded in the output; execution-time variables (`$DB_NAME`, `$CURRENT_TIME`, etc.) remain unexpanded. |
27
+ | `--profile` | Record wall-clock time for each SQL and metacommand statement. After the script completes, print a summary table sorted by elapsed time (descending), showing time, percentage of total, source location, command type, and a command preview. |
28
+ | `--profile-limit N` | Number of top statements to display in the `--profile` summary (default: 20). Remaining statements are counted and noted in the output footer. |
29
+ | `--ping` | Test database connectivity and exit. Connects using the supplied connection parameters, queries for the server version, and prints a one-line success message (exit 0) or the error (exit 1). No script file argument is required. |
30
+ | `--lint` | Parse the script and perform static analysis without connecting to a database. Reports unmatched IF/ENDIF, LOOP/END LOOP, and BEGIN BATCH/END BATCH blocks (errors); potentially undefined `!!$VAR!!` references (warnings); missing INCLUDE file targets (warnings); and unknown `EXECUTE SCRIPT` targets (warnings). Variable analysis uses two passes so definition order does not matter. The linter descends into named script blocks reached via `EXECUTE SCRIPT` / `EXEC SCRIPT` / `RUN SCRIPT`, reads `SUB_INI` INI files at lint time, recognizes `SUB_EMPTY` / `SUB_ADD` / `SUB_APPEND` / `SUBDATA` as definitions, suppresses false warnings for `$COUNTER_N`, and auto-discovers built-in system variables from the installed source. Exits 0 if no errors, 1 if errors found. |
31
31
 
32
32
  ### Export Formats
33
33
 
@@ -205,10 +205,15 @@ Valid encoding names can be displayed with the `-y` option. See also [Character
205
205
  - **Unmatched IF / ENDIF** — open IF blocks with no closing ENDIF, or orphan ENDIF with no IF (error).
206
206
  - **Unmatched LOOP / END LOOP** — open LOOP with no END LOOP, or orphan END LOOP (error).
207
207
  - **Unmatched BEGIN BATCH / END BATCH** — open batch with no close, or orphan END BATCH (error).
208
- - **Potentially undefined variables** — `!!$VAR!!` references where `$VAR` is not a built-in variable, not `$ARG_N`, and was not defined by a preceding `SUB` metacommand in the same script (warning — may be a false-positive if the variable is set in a config file or via `-a`).
208
+ - **Potentially undefined variables** — `!!$VAR!!` references where `$VAR` is not a built-in system variable, not `$ARG_N`, not `$COUNTER_N`, and was not defined by a `SUB`, `SUB_EMPTY`, `SUB_ADD`, `SUB_APPEND`, `SUBDATA`, or `SUB_INI` metacommand anywhere in the script (warning — may be a false-positive if the variable is set in a config file or via `-a`).
209
209
  - **Missing INCLUDE files** — `INCLUDE` targets that do not exist on disk relative to the script's directory (warning; `INCLUDE IF EXISTS` targets are never checked).
210
+ - **Unknown EXECUTE SCRIPT target** — `EXECUTE SCRIPT` names a script block that was not defined in the file (warning; `EXECUTE SCRIPT IF EXISTS` targets are never warned about).
210
211
  - **Empty script** — no commands found (warning).
211
212
 
213
+ Variable analysis uses two passes: the first collects every variable definition across the entire script and all named script blocks; the second performs the checks. Variables may be referenced before their definition point without producing false warnings. The linter also descends into named script blocks (`BEGIN SCRIPT … END SCRIPT`) reached via `EXECUTE SCRIPT`, `EXEC SCRIPT`, or `RUN SCRIPT`, so variables defined inside a block are visible to the caller. `SUB_INI` INI files are read at lint time to register their section keys as defined variables.
214
+
215
+ Built-in system variables are discovered automatically from the installed execsql source, so new variables added in future releases are recognized without any linter changes.
216
+
212
217
  Exits 0 when no errors are found (warnings alone do not affect the exit code). Exits 1 when any errors are found.
213
218
 
214
219
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.13.0"
7
+ version = "2.13.2"
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" }
@@ -58,7 +58,7 @@ odbc = ["pyodbc"]
58
58
  # Feature bundles
59
59
  formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables", "PyYAML"]
60
60
  auth = ["keyring"]
61
- upsert = ["pg-upsert>=1.18.2"]
61
+ upsert = ["pg-upsert>=1.20.0"]
62
62
  # Convenience groups
63
63
  all-db = [
64
64
  "psycopg2-binary",
@@ -161,7 +161,7 @@ skip-magic-trailing-comma = false
161
161
  line-ending = "auto"
162
162
 
163
163
  [tool.bumpversion]
164
- current_version = "2.13.0"
164
+ current_version = "2.13.2"
165
165
  commit = true
166
166
  commit_args = "--no-verify"
167
167
  tag = true
@@ -10,11 +10,18 @@ Checks performed
10
10
  2. **Unmatched LOOP / END LOOP** — mismatched nesting depth (error).
11
11
  3. **Unmatched BEGIN BATCH / END BATCH** — mismatched nesting depth (error).
12
12
  4. **Potentially undefined variables** — ``!!$VAR!!`` tokens not preceded by a
13
- ``SUB`` metacommand in the same parsed command list and not in the set of
14
- built-in variables (warning).
15
- 5. **Missing INCLUDE files** INCLUDE target does not exist on disk relative
13
+ ``SUB`` (or ``SUB_EMPTY``, ``SUB_ADD``, ``SUB_APPEND``, ``SUBDATA``)
14
+ metacommand in the same parsed command list and not in the set of built-in
15
+ variables (warning). Note: ``SUB_INI`` and ``SELECT_SUB`` define variables
16
+ whose names are not statically knowable — those may produce false-positive
17
+ warnings.
18
+ 5. **EXECUTE SCRIPT flow analysis** — when an ``EXECUTE SCRIPT <name>``
19
+ metacommand is encountered, the linter descends into the named script
20
+ block (if found in ``_state.savedscripts``) and merges any variables it
21
+ defines back into the caller's scope.
22
+ 6. **Missing INCLUDE files** — INCLUDE target does not exist on disk relative
16
23
  to the script directory (warning).
17
- 6. **Empty script** — no commands found (warning).
24
+ 7. **Empty script** — no commands found (warning).
18
25
 
19
26
  The function walks ``CommandList.cmdlist`` and also descends into any
20
27
  ``CommandList`` objects stored in ``_state.savedscripts`` (i.e. named scripts
@@ -67,6 +74,31 @@ _RX_END_BATCH = re.compile(r"^\s*END\s+BATCH\s*$", re.I)
67
74
  # SUB <varname> <value> — defines a substitution variable
68
75
  _RX_SUB = re.compile(r"^\s*SUB\s+(?P<name>[+~]?\w+)\s+", re.I)
69
76
 
77
+ # SUB_EMPTY <varname> — defines a variable with empty string
78
+ _RX_SUB_EMPTY = re.compile(r"^\s*SUB_EMPTY\s+(?P<name>[+~]?\w+)\s*$", re.I)
79
+
80
+ # SUB_ADD <varname> <expr> — increments a variable (implies it exists)
81
+ _RX_SUB_ADD = re.compile(r"^\s*SUB_ADD\s+(?P<name>[+~]?\w+)\s+", re.I)
82
+
83
+ # SUB_APPEND <varname> <text> — appends to a variable (implies it exists)
84
+ _RX_SUB_APPEND = re.compile(r"^\s*SUB_APPEND\s+(?P<name>[+~]?\w+)\s", re.I)
85
+
86
+ # SUBDATA <varname> <datasource> — defines a variable from a query result
87
+ _RX_SUBDATA = re.compile(r"^\s*SUBDATA\s+(?P<name>[+~]?\w+)\s+", re.I)
88
+
89
+ # SUB_INI [FILE] <filename> [SECTION] <section> — bulk-defines variables from INI file
90
+ _RX_SUB_INI = re.compile(
91
+ r'^\s*SUB_INI\s+(?:FILE\s+)?(?:"(?P<qfile>[^"]+)"|(?P<file>\S+))'
92
+ r"(?:\s+SECTION)?\s+(?P<section>\w+)\s*$",
93
+ re.I,
94
+ )
95
+
96
+ # EXECUTE SCRIPT / EXEC SCRIPT / RUN SCRIPT
97
+ _RX_EXEC_SCRIPT = re.compile(
98
+ r"^\s*(?:EXEC(?:UTE)?|RUN)\s+SCRIPT(?:\s+IF\s+EXISTS)?\s+(?P<script_id>\w+)",
99
+ re.I,
100
+ )
101
+
70
102
  # INCLUDE <file>
71
103
  _RX_INCLUDE = re.compile(
72
104
  r"^\s*INCLUDE(?:\s+IF\s+EXISTS?)?\s+(?P<path>\S+.*?)\s*$",
@@ -76,67 +108,41 @@ _RX_INCLUDE = re.compile(
76
108
  # Variable reference — !!name!! where name may start with $, @, &, ~, #, +
77
109
  _RX_VAR_REF = re.compile(r"!!([$@&~#+]?\w+)!!", re.I)
78
110
 
79
- # Built-in system variables that are always defined (populated by _run before
80
- # any script commands execute). Variable names are stored without the leading
81
- # ``$`` for case-insensitive set membership tests.
82
- _BUILTIN_VARS: frozenset[str] = frozenset(
83
- {
84
- # Start-time / environment
85
- "SCRIPT_START_TIME",
86
- "SCRIPT_START_TIME_UTC",
87
- "DATE_TAG",
88
- "DATETIME_TAG",
89
- "DATETIME_UTC_TAG",
90
- "LAST_ROWCOUNT",
91
- "LAST_SQL",
92
- "LAST_ERROR",
93
- "ERROR_MESSAGE",
94
- "USER",
95
- "STARTING_PATH",
96
- "PATHSEP",
97
- "OS",
98
- "PYTHON_EXECUTABLE",
99
- "STARTING_SCRIPT",
100
- "STARTING_SCRIPT_NAME",
101
- "STARTING_SCRIPT_REVTIME",
102
- "RUN_ID",
103
- # Execution-time (set during runscripts — not available in --dry-run
104
- # but always defined before any script command can reference them)
105
- "CURRENT_TIME",
106
- "CURRENT_TIME_UTC",
107
- "CURRENT_SCRIPT",
108
- "CURRENT_SCRIPT_PATH",
109
- "CURRENT_SCRIPT_NAME",
110
- "CURRENT_SCRIPT_LINE",
111
- "SCRIPT_LINE",
112
- "CURRENT_DIR",
113
- "CURRENT_PATH",
114
- "CURRENT_ALIAS",
115
- "AUTOCOMMIT_STATE",
116
- "TIMER",
117
- "DB_USER",
118
- "DB_SERVER",
119
- "DB_NAME",
120
- "DB_NEED_PWD",
121
- "RANDOM",
122
- "UUID",
123
- "VERSION1",
124
- "VERSION2",
125
- "VERSION3",
126
- "CANCEL_HALT_STATE",
127
- "ERROR_HALT_STATE",
128
- "METACOMMAND_ERROR_HALT_STATE",
129
- "CONSOLE_WAIT_WHEN_ERROR_HALT_STATE",
130
- "CONSOLE_WAIT_WHEN_DONE_STATE",
131
- "CURRENT_DBMS",
132
- "CURRENT_DATABASE",
133
- "SYSTEM_CMD_EXIT_STATUS",
134
- # Connection-populated
135
- "DB_FILE",
136
- "DB_PORT",
137
- # Counter variables (@@name) are always valid — skip validation
138
- },
139
- )
111
+ # Built-in system variables extracted automatically from the installed
112
+ # ``execsql`` source by scanning for ``add_substitution("$NAME", ...)`` and
113
+ # ``register_lazy("$NAME", ...)`` calls. This avoids maintaining a hand-
114
+ # curated list that drifts out of sync when new system variables are added.
115
+ # Variable names are stored upper-case without the leading ``$``.
116
+
117
+
118
+ def _discover_builtin_vars() -> frozenset[str]:
119
+ """Scan the execsql package source for ``$VARNAME`` system variables."""
120
+ import importlib.util
121
+
122
+ _rx_add_sub = re.compile(r'(?:(?<!\w)add_substitution|(?<!\w)sv)\s*\(\s*["\'](\$\w+)["\']')
123
+ _rx_lazy = re.compile(r'register_lazy\s*\(\s*["\'](\$\w+)["\']')
124
+
125
+ names: set[str] = set()
126
+
127
+ spec = importlib.util.find_spec("execsql")
128
+ if spec is None or spec.submodule_search_locations is None:
129
+ return frozenset(names)
130
+
131
+ pkg_dir = Path(spec.submodule_search_locations[0])
132
+ for src_file in pkg_dir.rglob("*.py"):
133
+ try:
134
+ text = src_file.read_text(encoding="utf-8")
135
+ except OSError:
136
+ continue
137
+ for m in _rx_add_sub.finditer(text):
138
+ names.add(m.group(1).lstrip("$").upper())
139
+ for m in _rx_lazy.finditer(text):
140
+ names.add(m.group(1).lstrip("$").upper())
141
+
142
+ return frozenset(names)
143
+
144
+
145
+ _BUILTIN_VARS: frozenset[str] = _discover_builtin_vars()
140
146
 
141
147
 
142
148
  # ---------------------------------------------------------------------------
@@ -159,23 +165,90 @@ def _warning(source: str, line_no: int, message: str) -> _Issue:
159
165
  # ---------------------------------------------------------------------------
160
166
 
161
167
 
168
+ def _collect_defined_vars(
169
+ cmdlist: CommandList,
170
+ script_dir: Path | None,
171
+ defined_vars: set[str],
172
+ *,
173
+ _savedscripts: dict | None = None,
174
+ _visited_scripts: set[str] | None = None,
175
+ ) -> None:
176
+ """Pass 1: walk *cmdlist* and collect all variable definitions into *defined_vars*.
177
+
178
+ This populates the set with every variable name that could be defined at
179
+ runtime — ``SUB``, ``SUB_EMPTY``, ``SUB_ADD``, ``SUB_APPEND``,
180
+ ``SUBDATA``, and ``SUB_INI`` (by reading the INI file on disk). It also
181
+ descends into ``EXECUTE SCRIPT`` targets to collect their definitions.
182
+
183
+ No issues are reported; structural checks and variable-reference validation
184
+ happen in pass 2 (:func:`_lint_cmdlist`).
185
+ """
186
+ visited = _visited_scripts if _visited_scripts is not None else set()
187
+
188
+ for cmd in cmdlist.cmdlist:
189
+ if cmd.command_type == "sql":
190
+ continue
191
+ stmt = cmd.command.statement
192
+
193
+ # SUB <name> <value>
194
+ sub_m = _RX_SUB.match(stmt)
195
+ if sub_m:
196
+ defined_vars.add(sub_m.group("name").lstrip("+~").upper())
197
+
198
+ # SUB_EMPTY / SUB_ADD / SUB_APPEND / SUBDATA
199
+ for rx in (_RX_SUB_EMPTY, _RX_SUB_ADD, _RX_SUB_APPEND, _RX_SUBDATA):
200
+ m = rx.match(stmt)
201
+ if m:
202
+ defined_vars.add(m.group("name").lstrip("+~").upper())
203
+ break
204
+
205
+ # SUB_INI — read INI file keys
206
+ ini_m = _RX_SUB_INI.match(stmt)
207
+ if ini_m:
208
+ ini_file = ini_m.group("qfile") or ini_m.group("file")
209
+ ini_section = ini_m.group("section")
210
+ if ini_file and not _RX_VAR_REF.search(ini_file):
211
+ _read_ini_vars(ini_file, ini_section, script_dir, defined_vars)
212
+
213
+ # EXECUTE SCRIPT — descend into named script block
214
+ exec_m = _RX_EXEC_SCRIPT.match(stmt)
215
+ if exec_m and _savedscripts is not None:
216
+ script_id = exec_m.group("script_id").lower()
217
+ if script_id in _savedscripts and script_id not in visited:
218
+ visited.add(script_id)
219
+ _collect_defined_vars(
220
+ _savedscripts[script_id],
221
+ script_dir,
222
+ defined_vars,
223
+ _savedscripts=_savedscripts,
224
+ _visited_scripts=visited,
225
+ )
226
+
227
+
162
228
  def _lint_cmdlist(
163
229
  cmdlist: CommandList,
164
230
  script_dir: Path | None,
165
231
  defined_vars: set[str],
232
+ *,
233
+ _savedscripts: dict | None = None,
234
+ _visited_scripts: set[str] | None = None,
166
235
  ) -> list[_Issue]:
167
- """Lint a single :class:`CommandList` and return any issues found.
236
+ """Pass 2: lint a :class:`CommandList` for structural and variable issues.
168
237
 
169
238
  Args:
170
239
  cmdlist: The parsed command list to analyse.
171
240
  script_dir: Directory of the top-level script file, used for resolving
172
241
  relative INCLUDE paths. ``None`` for inline (``-c``) scripts.
173
- defined_vars: Mutable set of variable names (without sigil) that have
174
- been defined by preceding ``SUB`` metacommands. The caller passes
175
- in the set from the outer scope so that variables defined before an
176
- EXECUTE SCRIPT call are visible inside the script block when
177
- analysing top-level scripts. For named-script analysis the caller
178
- passes a *copy* so that local definitions don't leak.
242
+ defined_vars: Set of variable names (without sigil) that have been
243
+ pre-collected by :func:`_collect_defined_vars`. This includes
244
+ *all* top-level and script-block definitions so that ordering
245
+ does not matter.
246
+ _savedscripts: Dictionary of named script blocks (from
247
+ ``_state.savedscripts``). Passed explicitly so the function can
248
+ descend into EXECUTE SCRIPT targets.
249
+ _visited_scripts: Set of script IDs already descended into, shared
250
+ across recursive calls to prevent infinite recursion from circular
251
+ EXECUTE SCRIPT references.
179
252
 
180
253
  Returns:
181
254
  List of ``(severity, source, line_no, message)`` issue tuples.
@@ -191,6 +264,10 @@ def _lint_cmdlist(
191
264
  batch_depth = 0
192
265
  batch_open_locs: list[tuple[str, int]] = []
193
266
 
267
+ # Track which EXECUTE SCRIPT targets we've already descended into to
268
+ # prevent infinite recursion from circular script references.
269
+ visited_scripts: set[str] = _visited_scripts if _visited_scripts is not None else set()
270
+
194
271
  for cmd in cmdlist.cmdlist:
195
272
  src = cmd.source
196
273
  lno = cmd.line_no
@@ -202,7 +279,7 @@ def _lint_cmdlist(
202
279
  _check_var_ref(m.group(1), src, lno, defined_vars, issues)
203
280
  continue
204
281
 
205
- # Metacommand checks
282
+ # Metacommand checks — variable references
206
283
  for m in _RX_VAR_REF.finditer(stmt):
207
284
  _check_var_ref(m.group(1), src, lno, defined_vars, issues)
208
285
 
@@ -247,11 +324,27 @@ def _lint_cmdlist(
247
324
  batch_depth -= 1
248
325
  batch_open_locs.pop()
249
326
 
250
- # -- SUB variable definition --
251
- sub_m = _RX_SUB.match(stmt)
252
- if sub_m:
253
- varname = sub_m.group("name").lstrip("+~")
254
- defined_vars.add(varname.upper())
327
+ # -- EXECUTE SCRIPT descend into named script block --
328
+ exec_m = _RX_EXEC_SCRIPT.match(stmt)
329
+ if exec_m and _savedscripts is not None:
330
+ script_id = exec_m.group("script_id").lower()
331
+ if script_id not in _savedscripts:
332
+ # Warn unless it's EXECUTE SCRIPT IF EXISTS
333
+ if not re.search(r"\bIF\s+EXISTS\b", stmt, re.I):
334
+ issues.append(
335
+ _warning(src, lno, f"EXECUTE SCRIPT target not found: '{script_id}'"),
336
+ )
337
+ elif script_id not in visited_scripts:
338
+ visited_scripts.add(script_id)
339
+ sub_issues = _lint_cmdlist(
340
+ _savedscripts[script_id],
341
+ script_dir,
342
+ defined_vars,
343
+ _savedscripts=_savedscripts,
344
+ _visited_scripts=visited_scripts,
345
+ )
346
+ for sev, ssrc, slno, msg in sub_issues:
347
+ issues.append((sev, ssrc, slno, f"[script '{script_id}'] {msg}"))
255
348
 
256
349
  # -- INCLUDE file existence --
257
350
  inc_m = _RX_INCLUDE.match(stmt)
@@ -307,6 +400,10 @@ def _check_var_ref(
307
400
  if re.match(r"^ARG_\d+$", name, re.I):
308
401
  return
309
402
 
403
+ # $COUNTER_N is managed by CounterVars (@@counter metacommands)
404
+ if re.match(r"^COUNTER_\d+$", name, re.I):
405
+ return
406
+
310
407
  # Built-in system variables
311
408
  if name.upper() in _BUILTIN_VARS:
312
409
  return
@@ -325,6 +422,35 @@ def _check_var_ref(
325
422
  )
326
423
 
327
424
 
425
+ def _read_ini_vars(
426
+ ini_file: str,
427
+ section: str,
428
+ script_dir: Path | None,
429
+ defined_vars: set[str],
430
+ ) -> None:
431
+ """Read an INI file and register its section keys as defined variables.
432
+
433
+ Mirrors what ``SUB_INI`` does at runtime: reads a
434
+ :class:`~configparser.ConfigParser` section and defines each key as a
435
+ substitution variable. If the file does not exist or the section is
436
+ missing, silently does nothing (the runtime handler behaves the same way).
437
+ """
438
+ from configparser import ConfigParser
439
+
440
+ p = Path(ini_file)
441
+ if not p.is_absolute() and script_dir is not None:
442
+ p = script_dir / p
443
+
444
+ if not p.exists():
445
+ return
446
+
447
+ cp = ConfigParser()
448
+ cp.read(p)
449
+ if cp.has_section(section):
450
+ for key, _value in cp.items(section):
451
+ defined_vars.add(key.upper())
452
+
453
+
328
454
  def _check_include_path(
329
455
  raw_path: str,
330
456
  script_dir: Path | None,
@@ -393,19 +519,68 @@ def _lint_script(
393
519
  return issues
394
520
 
395
521
  script_dir = Path(script_path).resolve().parent if script_path else None
522
+ savedscripts: dict = getattr(_state, "savedscripts", {})
523
+
524
+ # ------------------------------------------------------------------
525
+ # Pass 1: collect all variable definitions from the top-level script
526
+ # and all reachable script blocks. This ensures definition order does
527
+ # not matter — a script block executed early can reference variables
528
+ # defined later in the top-level script.
529
+ # ------------------------------------------------------------------
530
+ all_defined: set[str] = set()
531
+ collect_visited: set[str] = set()
532
+ _collect_defined_vars(
533
+ cmdlist,
534
+ script_dir,
535
+ all_defined,
536
+ _savedscripts=savedscripts,
537
+ _visited_scripts=collect_visited,
538
+ )
539
+ # Also collect from every saved script block (they may define vars
540
+ # referenced by other blocks). Share the visited set so each block
541
+ # is only traversed once (O(N) instead of O(N²)).
542
+ for saved_cl in savedscripts.values():
543
+ _collect_defined_vars(
544
+ saved_cl,
545
+ script_dir,
546
+ all_defined,
547
+ _savedscripts=savedscripts,
548
+ _visited_scripts=collect_visited,
549
+ )
396
550
 
397
- # Shared set of variables defined in the top-level script via SUB.
398
- # Named scripts get a fresh copy so their internal definitions don't bleed
399
- # back into the top-level analysis.
400
- top_defined: set[str] = set()
401
-
402
- issues.extend(_lint_cmdlist(cmdlist, script_dir, top_defined))
551
+ # ------------------------------------------------------------------
552
+ # Pass 2: lint for structural issues and undefined-variable warnings
553
+ # using the complete variable set from pass 1.
554
+ # ------------------------------------------------------------------
555
+ # Shared visited-scripts tracker — prevents duplicate lint warnings
556
+ # when the same script block is reached via multiple paths.
557
+ visited: set[str] = set()
558
+
559
+ issues.extend(
560
+ _lint_cmdlist(
561
+ cmdlist,
562
+ script_dir,
563
+ all_defined,
564
+ _savedscripts=savedscripts,
565
+ _visited_scripts=visited,
566
+ ),
567
+ )
403
568
 
404
- # Analyse each named SCRIPT block collected during parsing
405
- for script_name, saved_cl in getattr(_state, "savedscripts", {}).items():
406
- saved_issues = _lint_cmdlist(saved_cl, script_dir, set(top_defined))
569
+ # Analyse each named SCRIPT block that was NOT already visited via
570
+ # EXECUTE SCRIPT (standalone analysis catches structural issues like
571
+ # unmatched IF/ENDIF in script blocks that are never executed).
572
+ for script_name, saved_cl in savedscripts.items():
573
+ if script_name in visited:
574
+ continue
575
+ visited.add(script_name)
576
+ saved_issues = _lint_cmdlist(
577
+ saved_cl,
578
+ script_dir,
579
+ set(all_defined),
580
+ _savedscripts=savedscripts,
581
+ _visited_scripts=visited,
582
+ )
407
583
  for sev, src, lno, msg in saved_issues:
408
- # Annotate with the script name if the source is the same file
409
584
  issues.append((sev, src, lno, f"[script '{script_name}'] {msg}"))
410
585
 
411
586
  return issues
@@ -440,12 +615,22 @@ def _print_lint_results(issues: list[_Issue], script_label: str) -> int:
440
615
  _console.print()
441
616
  return 0
442
617
 
443
- for severity, source, line_no, message in issues:
444
- loc = f"{source}:{line_no}" if line_no else source
618
+ # Sort: errors first, then warnings; within each group sort by line number.
619
+ _sev_order = {"error": 0, "warning": 1}
620
+ sorted_issues = sorted(issues, key=lambda i: (_sev_order.get(i[0], 9), i[2]))
621
+
622
+ # Compute the widest location string so columns align.
623
+ locs: list[str] = []
624
+ for _, source, line_no, _ in sorted_issues:
625
+ locs.append(f"{source}:{line_no}" if line_no else source)
626
+ loc_width = max(len(loc) for loc in locs) if locs else 0
627
+
628
+ for (severity, _source, _line_no, message), loc in zip(sorted_issues, locs):
629
+ pad = " " * (loc_width - len(loc))
445
630
  if severity == "error":
446
- _console.print(f" [bold red]ERROR [/bold red] [dim]{loc}[/dim] {message}")
631
+ _console.print(f" [bold red]ERROR [/bold red] [dim]{loc}[/dim]{pad} {message}")
447
632
  else:
448
- _console.print(f" [bold yellow]WARNING[/bold yellow] [dim]{loc}[/dim] {message}")
633
+ _console.print(f" [bold yellow]WARNING[/bold yellow] [dim]{loc}[/dim]{pad} {message}")
449
634
 
450
635
  _console.print()
451
636
  parts = []