execsql2 2.16.18__tar.gz → 2.17.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 (322) hide show
  1. {execsql2-2.16.18 → execsql2-2.17.0}/CHANGELOG.md +12 -0
  2. {execsql2-2.16.18 → execsql2-2.17.0}/PKG-INFO +1 -1
  3. {execsql2-2.16.18 → execsql2-2.17.0}/docs/about/divergence.md +9 -9
  4. {execsql2-2.16.18 → execsql2-2.17.0}/docs/reference/metacommands.md +18 -0
  5. {execsql2-2.16.18 → execsql2-2.17.0}/pyproject.toml +2 -2
  6. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/upsert.py +0 -29
  7. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/script/executor.py +12 -8
  8. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_pg_upsert.py +39 -75
  9. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_executor.py +39 -0
  10. {execsql2-2.16.18 → execsql2-2.17.0}/uv.lock +1 -1
  11. {execsql2-2.16.18 → execsql2-2.17.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {execsql2-2.16.18 → execsql2-2.17.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {execsql2-2.16.18 → execsql2-2.17.0}/.github/workflows/ci-cd.yml +0 -0
  14. {execsql2-2.16.18 → execsql2-2.17.0}/.gitignore +0 -0
  15. {execsql2-2.16.18 → execsql2-2.17.0}/.pre-commit-config.yaml +0 -0
  16. {execsql2-2.16.18 → execsql2-2.17.0}/.pre-commit-hooks.yaml +0 -0
  17. {execsql2-2.16.18 → execsql2-2.17.0}/.python-version +0 -0
  18. {execsql2-2.16.18 → execsql2-2.17.0}/.readthedocs.yaml +0 -0
  19. {execsql2-2.16.18 → execsql2-2.17.0}/CONTRIBUTING.md +0 -0
  20. {execsql2-2.16.18 → execsql2-2.17.0}/LICENSE.txt +0 -0
  21. {execsql2-2.16.18 → execsql2-2.17.0}/NOTICE +0 -0
  22. {execsql2-2.16.18 → execsql2-2.17.0}/README.md +0 -0
  23. {execsql2-2.16.18 → execsql2-2.17.0}/SECURITY.md +0 -0
  24. {execsql2-2.16.18 → execsql2-2.17.0}/docs/about/contributors.md +0 -0
  25. {execsql2-2.16.18 → execsql2-2.17.0}/docs/about/copyright.md +0 -0
  26. {execsql2-2.16.18 → execsql2-2.17.0}/docs/api/cli.md +0 -0
  27. {execsql2-2.16.18 → execsql2-2.17.0}/docs/api/db.md +0 -0
  28. {execsql2-2.16.18 → execsql2-2.17.0}/docs/api/exporters.md +0 -0
  29. {execsql2-2.16.18 → execsql2-2.17.0}/docs/api/importers.md +0 -0
  30. {execsql2-2.16.18 → execsql2-2.17.0}/docs/api/index.md +0 -0
  31. {execsql2-2.16.18 → execsql2-2.17.0}/docs/api/metacommands.md +0 -0
  32. {execsql2-2.16.18 → execsql2-2.17.0}/docs/dev/adding_db_adapters.md +0 -0
  33. {execsql2-2.16.18 → execsql2-2.17.0}/docs/dev/adding_exporters.md +0 -0
  34. {execsql2-2.16.18 → execsql2-2.17.0}/docs/dev/adding_importers.md +0 -0
  35. {execsql2-2.16.18 → execsql2-2.17.0}/docs/dev/adding_metacommands.md +0 -0
  36. {execsql2-2.16.18 → execsql2-2.17.0}/docs/dev/architecture.md +0 -0
  37. {execsql2-2.16.18 → execsql2-2.17.0}/docs/getting-started/installation.md +0 -0
  38. {execsql2-2.16.18 → execsql2-2.17.0}/docs/getting-started/requirements.md +0 -0
  39. {execsql2-2.16.18 → execsql2-2.17.0}/docs/getting-started/syntax.md +0 -0
  40. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/debugging.md +0 -0
  41. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/documentation.md +0 -0
  42. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/encoding.md +0 -0
  43. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/examples.md +0 -0
  44. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/formatter.md +0 -0
  45. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/logging.md +0 -0
  46. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/sql_syntax.md +0 -0
  47. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/usage.md +0 -0
  48. {execsql2-2.16.18 → execsql2-2.17.0}/docs/guides/using_scripts.md +0 -0
  49. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/Compare_planets.png +0 -0
  50. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/actions.png +0 -0
  51. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/actions2.png +0 -0
  52. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/checkboxes.png +0 -0
  53. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/connect.b64 +0 -0
  54. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/connect.png +0 -0
  55. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/create_conf.png +0 -0
  56. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/data_error1_screenshot.jpg +0 -0
  57. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/entry_form.png +0 -0
  58. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/execsql_console.png +0 -0
  59. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/execsql_logo_01.png +0 -0
  60. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/fatals.png +0 -0
  61. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/logo_small.png +0 -0
  62. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/pause_terminal.png +0 -0
  63. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/pause_terminal_sm.b64 +0 -0
  64. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/pause_terminal_sm.png +0 -0
  65. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/prompt_compare.png +0 -0
  66. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/set_build_commands.jpg +0 -0
  67. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/unit_conversions.b64 +0 -0
  68. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/unit_conversions_029.png +0 -0
  69. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/unmatched.png +0 -0
  70. {execsql2-2.16.18 → execsql2-2.17.0}/docs/images/vim_execsql_highlight.png +0 -0
  71. {execsql2-2.16.18 → execsql2-2.17.0}/docs/index.md +0 -0
  72. {execsql2-2.16.18 → execsql2-2.17.0}/docs/reference/configuration.md +0 -0
  73. {execsql2-2.16.18 → execsql2-2.17.0}/docs/reference/security.md +0 -0
  74. {execsql2-2.16.18 → execsql2-2.17.0}/docs/reference/substitution_vars.md +0 -0
  75. {execsql2-2.16.18 → execsql2-2.17.0}/extras/plugin-template/README.md +0 -0
  76. {execsql2-2.16.18 → execsql2-2.17.0}/extras/plugin-template/pyproject.toml +0 -0
  77. {execsql2-2.16.18 → execsql2-2.17.0}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  78. {execsql2-2.16.18 → execsql2-2.17.0}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  79. {execsql2-2.16.18 → execsql2-2.17.0}/extras/vscode-execsql/README.md +0 -0
  80. {execsql2-2.16.18 → execsql2-2.17.0}/extras/vscode-execsql/package.json +0 -0
  81. {execsql2-2.16.18 → execsql2-2.17.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  82. {execsql2-2.16.18 → execsql2-2.17.0}/justfile +0 -0
  83. {execsql2-2.16.18 → execsql2-2.17.0}/scripts/generate_vscode_grammar.py +0 -0
  84. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/__init__.py +0 -0
  85. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/__main__.py +0 -0
  86. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/api.py +0 -0
  87. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/cli/__init__.py +0 -0
  88. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/cli/dsn.py +0 -0
  89. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/cli/help.py +0 -0
  90. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/cli/lint.py +0 -0
  91. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/cli/lint_ast.py +0 -0
  92. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/cli/run.py +0 -0
  93. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/config.py +0 -0
  94. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/data/__init__.py +0 -0
  95. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/data/execsql.conf.template +0 -0
  96. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/__init__.py +0 -0
  97. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/access.py +0 -0
  98. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/base.py +0 -0
  99. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/dsn.py +0 -0
  100. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/duckdb.py +0 -0
  101. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/factory.py +0 -0
  102. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/firebird.py +0 -0
  103. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/mysql.py +0 -0
  104. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/oracle.py +0 -0
  105. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/postgres.py +0 -0
  106. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/sqlite.py +0 -0
  107. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/db/sqlserver.py +0 -0
  108. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/debug/__init__.py +0 -0
  109. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/debug/repl.py +0 -0
  110. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exceptions.py +0 -0
  111. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/__init__.py +0 -0
  112. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/base.py +0 -0
  113. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/delimited.py +0 -0
  114. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/duckdb.py +0 -0
  115. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/feather.py +0 -0
  116. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/html.py +0 -0
  117. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/json.py +0 -0
  118. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/latex.py +0 -0
  119. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/markdown.py +0 -0
  120. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/ods.py +0 -0
  121. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/parquet.py +0 -0
  122. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/pretty.py +0 -0
  123. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/protocol.py +0 -0
  124. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/raw.py +0 -0
  125. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/sqlite.py +0 -0
  126. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/templates.py +0 -0
  127. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/values.py +0 -0
  128. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/xls.py +0 -0
  129. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/xlsx.py +0 -0
  130. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/xml.py +0 -0
  131. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/yaml.py +0 -0
  132. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/exporters/zip.py +0 -0
  133. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/format.py +0 -0
  134. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/gui/__init__.py +0 -0
  135. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/gui/base.py +0 -0
  136. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/gui/console.py +0 -0
  137. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/gui/desktop.py +0 -0
  138. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/gui/tui.py +0 -0
  139. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/importers/__init__.py +0 -0
  140. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/importers/base.py +0 -0
  141. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/importers/csv.py +0 -0
  142. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/importers/feather.py +0 -0
  143. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/importers/json.py +0 -0
  144. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/importers/ods.py +0 -0
  145. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/importers/xls.py +0 -0
  146. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/__init__.py +0 -0
  147. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/conditions.py +0 -0
  148. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/connect.py +0 -0
  149. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/control.py +0 -0
  150. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/data.py +0 -0
  151. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/debug.py +0 -0
  152. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/dispatch.py +0 -0
  153. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/io.py +0 -0
  154. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/io_export.py +0 -0
  155. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/io_fileops.py +0 -0
  156. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/io_import.py +0 -0
  157. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/io_write.py +0 -0
  158. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/prompt.py +0 -0
  159. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/script_ext.py +0 -0
  160. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/metacommands/system.py +0 -0
  161. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/models.py +0 -0
  162. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/parser.py +0 -0
  163. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/plugins.py +0 -0
  164. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/py.typed +0 -0
  165. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/script/__init__.py +0 -0
  166. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/script/ast.py +0 -0
  167. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/script/control.py +0 -0
  168. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/script/engine.py +0 -0
  169. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/script/parser.py +0 -0
  170. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/script/variables.py +0 -0
  171. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/state.py +0 -0
  172. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/types.py +0 -0
  173. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/__init__.py +0 -0
  174. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/auth.py +0 -0
  175. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/crypto.py +0 -0
  176. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/datetime.py +0 -0
  177. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/errors.py +0 -0
  178. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/fileio.py +0 -0
  179. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/gui.py +0 -0
  180. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/mail.py +0 -0
  181. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/numeric.py +0 -0
  182. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/regex.py +0 -0
  183. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/strings.py +0 -0
  184. {execsql2-2.16.18 → execsql2-2.17.0}/src/execsql/utils/timer.py +0 -0
  185. {execsql2-2.16.18 → execsql2-2.17.0}/templates/README.md +0 -0
  186. {execsql2-2.16.18 → execsql2-2.17.0}/templates/config_settings.sqlite +0 -0
  187. {execsql2-2.16.18 → execsql2-2.17.0}/templates/example_config_prompt.sql +0 -0
  188. {execsql2-2.16.18 → execsql2-2.17.0}/templates/execsql.conf +0 -0
  189. {execsql2-2.16.18 → execsql2-2.17.0}/templates/make_config_db.sql +0 -0
  190. {execsql2-2.16.18 → execsql2-2.17.0}/templates/md_compare.sql +0 -0
  191. {execsql2-2.16.18 → execsql2-2.17.0}/templates/md_glossary.sql +0 -0
  192. {execsql2-2.16.18 → execsql2-2.17.0}/templates/md_upsert.sql +0 -0
  193. {execsql2-2.16.18 → execsql2-2.17.0}/templates/pg_compare.sql +0 -0
  194. {execsql2-2.16.18 → execsql2-2.17.0}/templates/pg_glossary.sql +0 -0
  195. {execsql2-2.16.18 → execsql2-2.17.0}/templates/pg_upsert.sql +0 -0
  196. {execsql2-2.16.18 → execsql2-2.17.0}/templates/script_template.sql +0 -0
  197. {execsql2-2.16.18 → execsql2-2.17.0}/templates/ss_compare.sql +0 -0
  198. {execsql2-2.16.18 → execsql2-2.17.0}/templates/ss_glossary.sql +0 -0
  199. {execsql2-2.16.18 → execsql2-2.17.0}/templates/ss_upsert.sql +0 -0
  200. {execsql2-2.16.18 → execsql2-2.17.0}/tests/__init__.py +0 -0
  201. {execsql2-2.16.18 → execsql2-2.17.0}/tests/cli/__init__.py +0 -0
  202. {execsql2-2.16.18 → execsql2-2.17.0}/tests/cli/test_cli.py +0 -0
  203. {execsql2-2.16.18 → execsql2-2.17.0}/tests/cli/test_cli_e2e.py +0 -0
  204. {execsql2-2.16.18 → execsql2-2.17.0}/tests/cli/test_cli_run.py +0 -0
  205. {execsql2-2.16.18 → execsql2-2.17.0}/tests/cli/test_lint.py +0 -0
  206. {execsql2-2.16.18 → execsql2-2.17.0}/tests/cli/test_ping.py +0 -0
  207. {execsql2-2.16.18 → execsql2-2.17.0}/tests/cli/test_profile.py +0 -0
  208. {execsql2-2.16.18 → execsql2-2.17.0}/tests/conftest.py +0 -0
  209. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/__init__.py +0 -0
  210. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/test_base.py +0 -0
  211. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/test_db_adapters_mocked.py +0 -0
  212. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/test_dsn.py +0 -0
  213. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/test_duckdb.py +0 -0
  214. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/test_factory.py +0 -0
  215. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/test_postgres.py +0 -0
  216. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/test_sqlite.py +0 -0
  217. {execsql2-2.16.18 → execsql2-2.17.0}/tests/db/test_sqlite_extra.py +0 -0
  218. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/__init__.py +0 -0
  219. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_base.py +0 -0
  220. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_db.py +0 -0
  221. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_delimited.py +0 -0
  222. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  223. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_exporters.py +0 -0
  224. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_feather.py +0 -0
  225. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_html_extended.py +0 -0
  226. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_html_latex.py +0 -0
  227. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_json.py +0 -0
  228. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_json_extended.py +0 -0
  229. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_latex_extended.py +0 -0
  230. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_markdown.py +0 -0
  231. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_ods.py +0 -0
  232. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_parquet.py +0 -0
  233. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_pretty_extended.py +0 -0
  234. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_raw_extended.py +0 -0
  235. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  236. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_templates.py +0 -0
  237. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_templates_extended.py +0 -0
  238. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_values_extended.py +0 -0
  239. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_xls_xlsx.py +0 -0
  240. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_xlsx.py +0 -0
  241. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_xml.py +0 -0
  242. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_yaml.py +0 -0
  243. {execsql2-2.16.18 → execsql2-2.17.0}/tests/exporters/test_zip.py +0 -0
  244. {execsql2-2.16.18 → execsql2-2.17.0}/tests/gui/__init__.py +0 -0
  245. {execsql2-2.16.18 → execsql2-2.17.0}/tests/gui/test_backends.py +0 -0
  246. {execsql2-2.16.18 → execsql2-2.17.0}/tests/gui/test_compare_stats.py +0 -0
  247. {execsql2-2.16.18 → execsql2-2.17.0}/tests/gui/test_compute_row_diffs.py +0 -0
  248. {execsql2-2.16.18 → execsql2-2.17.0}/tests/importers/__init__.py +0 -0
  249. {execsql2-2.16.18 → execsql2-2.17.0}/tests/importers/test_base_extended.py +0 -0
  250. {execsql2-2.16.18 → execsql2-2.17.0}/tests/importers/test_csv_edge_cases.py +0 -0
  251. {execsql2-2.16.18 → execsql2-2.17.0}/tests/importers/test_csv_importer.py +0 -0
  252. {execsql2-2.16.18 → execsql2-2.17.0}/tests/importers/test_feather_importer.py +0 -0
  253. {execsql2-2.16.18 → execsql2-2.17.0}/tests/importers/test_json_importer.py +0 -0
  254. {execsql2-2.16.18 → execsql2-2.17.0}/tests/importers/test_ods_importer.py +0 -0
  255. {execsql2-2.16.18 → execsql2-2.17.0}/tests/importers/test_xls_importer.py +0 -0
  256. {execsql2-2.16.18 → execsql2-2.17.0}/tests/integration/__init__.py +0 -0
  257. {execsql2-2.16.18 → execsql2-2.17.0}/tests/integration/conftest.py +0 -0
  258. {execsql2-2.16.18 → execsql2-2.17.0}/tests/integration/test_dsn.py +0 -0
  259. {execsql2-2.16.18 → execsql2-2.17.0}/tests/integration/test_duckdb.py +0 -0
  260. {execsql2-2.16.18 → execsql2-2.17.0}/tests/integration/test_mysql.py +0 -0
  261. {execsql2-2.16.18 → execsql2-2.17.0}/tests/integration/test_postgres.py +0 -0
  262. {execsql2-2.16.18 → execsql2-2.17.0}/tests/integration/test_sqlite.py +0 -0
  263. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/__init__.py +0 -0
  264. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_assert.py +0 -0
  265. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_breakpoint.py +0 -0
  266. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_connect.py +0 -0
  267. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_io_export.py +0 -0
  268. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_io_import.py +0 -0
  269. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands.py +0 -0
  270. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  271. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_data.py +0 -0
  272. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_extended.py +0 -0
  273. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  274. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_io.py +0 -0
  275. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  276. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  277. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_system.py +0 -0
  278. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  279. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_row_count.py +0 -0
  280. {execsql2-2.16.18 → execsql2-2.17.0}/tests/metacommands/test_show_scripts.py +0 -0
  281. {execsql2-2.16.18 → execsql2-2.17.0}/tests/scripts/__init__.py +0 -0
  282. {execsql2-2.16.18 → execsql2-2.17.0}/tests/scripts/fixtures/control_flow.sql +0 -0
  283. {execsql2-2.16.18 → execsql2-2.17.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  284. {execsql2-2.16.18 → execsql2-2.17.0}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  285. {execsql2-2.16.18 → execsql2-2.17.0}/tests/scripts/fixtures/smoke.sql +0 -0
  286. {execsql2-2.16.18 → execsql2-2.17.0}/tests/scripts/test_sql_scripts.py +0 -0
  287. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_api.py +0 -0
  288. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_ast.py +0 -0
  289. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_ast_parser.py +0 -0
  290. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_config.py +0 -0
  291. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_config_data.py +0 -0
  292. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_config_extended.py +0 -0
  293. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_debug_repl.py +0 -0
  294. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_engine.py +0 -0
  295. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_error_messages.py +0 -0
  296. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_exceptions.py +0 -0
  297. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_format.py +0 -0
  298. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_mail.py +0 -0
  299. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_models.py +0 -0
  300. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_package.py +0 -0
  301. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_parser.py +0 -0
  302. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_parser_params.py +0 -0
  303. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_plugins.py +0 -0
  304. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_registry.py +0 -0
  305. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_script.py +0 -0
  306. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_state.py +0 -0
  307. {execsql2-2.16.18 → execsql2-2.17.0}/tests/test_types.py +0 -0
  308. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/__init__.py +0 -0
  309. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_auth.py +0 -0
  310. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_auth_extra.py +0 -0
  311. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_crypto.py +0 -0
  312. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_datetime.py +0 -0
  313. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_errors.py +0 -0
  314. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_errors_extra.py +0 -0
  315. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_fileio.py +0 -0
  316. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_fileio_extra.py +0 -0
  317. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_numeric.py +0 -0
  318. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_regex.py +0 -0
  319. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_strings.py +0 -0
  320. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_timer.py +0 -0
  321. {execsql2-2.16.18 → execsql2-2.17.0}/tests/utils/test_timer_extra.py +0 -0
  322. {execsql2-2.16.18 → execsql2-2.17.0}/zensical.toml +0 -0
@@ -13,6 +13,18 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.17.0] - 2026-05-07
17
+
18
+ ### Changed
19
+
20
+ - **Behavior change.** `PG_UPSERT`, `PG_UPSERT QA`, and `PG_UPSERT CHECK` no longer raise a metacommand error when QA checks fail. The outcome is reported via `$PG_UPSERT_QA_PASSED` (and the per-table `$PG_UPSERT_TABLE_QA_PASSED`) along with `$PG_UPSERT_RESULT_JSON`, so the script controls flow with `IF` or `ASSERT` instead of being forced into execsql's halt-on-error path. `EXPORT_FAILURES` still runs on QA failure, and the upsert is still skipped (no commit). Scripts that previously relied on a hard halt should add `ASSERT !!$PG_UPSERT_QA_PASSED!! = TRUE` (or branch via `IF`) at the call site.
21
+
22
+ ### Fixed
23
+
24
+ - `!!$COUNTER_N!!` references inside metacommands (e.g. `WRITE`, `IF`, `SUB`) now return the documented sequence `1, 2, 3, …` starting at 1. The AST executor was calling `substitute_vars()` twice per metacommand — once to detect a `BREAK` token before dispatch, then again inside the dispatch handler — which double-incremented every counter reference so the first read returned 2 and subsequent reads stepped by 2. Counter references in plain SQL statements were unaffected (they already substituted exactly once). The duplicate substitution also re-rolled `!!$RANDOM!!` and `!!$UUID!!` for any metacommand that referenced them, so those values are now stable across BREAK detection and dispatch as well.
25
+
26
+ ______________________________________________________________________
27
+
16
28
  ## [2.16.18] - 2026-05-05
17
29
 
18
30
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.16.18
3
+ Version: 2.17.0
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -46,15 +46,15 @@ ______________________________________________________________________
46
46
 
47
47
  ### Metacommands
48
48
 
49
- | Metacommand | Description |
50
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
51
- | `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. |
52
- | `BREAKPOINT` | Pause script execution and drop into an interactive debug REPL. See [Debugging](#debugging) below for full details. |
53
- | `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
54
- | `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file. |
55
- | `PG_UPSERT` | QA-checked, FK-dependency-ordered upserts from staging to base schema on PostgreSQL. Integrates [pg-upsert](https://pg-upsert.readthedocs.io/) as an optional dependency. Three modes: full pipeline, QA-only, and schema check. Supports `EXPORT_FAILURES`, `EXPORT_FORMAT`, `EXPORT_MAX_ROWS`, and `STRICT_COLUMNS` keywords. `STRICT_COLUMNS` forces all missing columns to be errors (requires `pg-upsert>=1.22.0`). |
56
- | `IMPORT … FROM JSON` | Import a JSON file (array of objects or NDJSON) into a database table. Nested objects are flattened with dot-separated column names; arrays are stored as JSON strings. |
57
- | `SHOW SCRIPTS [<name>]` | Without a name, lists all registered SCRIPT definitions with parameter signatures and source locations. With a name, shows detail including parameters (with defaults), source file/line range, and docstring. |
49
+ | Metacommand | Description |
50
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | `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. |
52
+ | `BREAKPOINT` | Pause script execution and drop into an interactive debug REPL. See [Debugging](#debugging) below for full details. |
53
+ | `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
54
+ | `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file. |
55
+ | `PG_UPSERT` | QA-checked, FK-dependency-ordered upserts from staging to base schema on PostgreSQL. Integrates [pg-upsert](https://pg-upsert.readthedocs.io/) as an optional dependency. Three modes: full pipeline, QA-only, and schema check. Supports `EXPORT_FAILURES`, `EXPORT_FORMAT`, `EXPORT_MAX_ROWS`, and `STRICT_COLUMNS` keywords. `STRICT_COLUMNS` forces all missing columns to be errors (requires `pg-upsert>=1.22.0`). QA failure does not raise — the outcome is reported via `$PG_UPSERT_QA_PASSED` so the user's own `IF` / `ON ERROR HALT` decides control flow. |
56
+ | `IMPORT … FROM JSON` | Import a JSON file (array of objects or NDJSON) into a database table. Nested objects are flattened with dot-separated column names; arrays are stored as JSON strings. |
57
+ | `SHOW SCRIPTS [<name>]` | Without a name, lists all registered SCRIPT definitions with parameter signatures and source locations. With a name, shows detail including parameters (with defaults), source file/line range, and docstring. |
58
58
 
59
59
  ### SCRIPT Enhancements
60
60
 
@@ -2248,6 +2248,24 @@ Set after every `PG_UPSERT` execution:
2248
2248
  -- !x! WRITE `!!$PG_UPSERT_RESULT_JSON!!`
2249
2249
  ```
2250
2250
 
2251
+ ### QA failure behavior
2252
+
2253
+ `PG_UPSERT`, `PG_UPSERT QA`, and `PG_UPSERT CHECK` do **not** raise an error when QA checks fail. The outcome is reported via `$PG_UPSERT_QA_PASSED` (and the per-table `$PG_UPSERT_TABLE_QA_PASSED`), so the script controls flow with `IF` or with execsql's standard error-halt configuration. When QA fails, the metacommand still runs `EXPORT_FAILURES`, populates all `$PG_UPSERT_*` substitution variables, and skips the upsert (no commit). Pair with `ASSERT` to halt explicitly:
2254
+
2255
+ ```sql
2256
+ -- Soft failure: branch on the result, keep running.
2257
+ -- !x! PG_UPSERT FROM staging TO public TABLES books, authors EXPORT_FAILURES "qa/"
2258
+ -- !x! IF !!$PG_UPSERT_QA_PASSED!! = TRUE
2259
+ -- !x! PG_UPSERT FROM staging TO public TABLES books, authors COMMIT
2260
+ -- !x! ELSE
2261
+ -- !x! WRITE "QA failed — fix sheet at !!$PG_UPSERT_EXPORT_PATH!!"
2262
+ -- !x! ENDIF
2263
+
2264
+ -- Hard failure: halt the script when QA fails.
2265
+ -- !x! PG_UPSERT QA FROM staging TO public TABLES books, authors
2266
+ -- !x! ASSERT !!$PG_UPSERT_QA_PASSED!! = TRUE "Pre-load QA failed; aborting."
2267
+ ```
2268
+
2251
2269
  ### Temporary objects
2252
2270
 
2253
2271
  pg-upsert creates temporary tables and views (all prefixed `ups_`) that persist after the metacommand completes. Users can query these for debugging and inspection — for example, `SELECT * FROM ups_control` shows per-table QA results and row counts.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.16.18"
7
+ version = "2.17.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" }
@@ -165,7 +165,7 @@ skip-magic-trailing-comma = false
165
165
  line-ending = "auto"
166
166
 
167
167
  [tool.bumpversion]
168
- current_version = "2.16.18"
168
+ current_version = "2.17.0"
169
169
  commit = true
170
170
  tag = true
171
171
  tag_name = "v{new_version}"
@@ -201,14 +201,6 @@ def _set_subvars(result: Any) -> None:
201
201
  sv("$PG_UPSERT_EXPORT_PATH", "")
202
202
 
203
203
 
204
- def _qa_failure_msg(result: Any) -> str:
205
- """Build a concise QA failure message listing which tables failed."""
206
- failed = [t.table_name for t in result.tables if not t.qa_passed]
207
- if failed:
208
- return f"PG_UPSERT QA failed for: {', '.join(failed)}"
209
- return "PG_UPSERT QA checks failed."
210
-
211
-
212
204
  # ---------------------------------------------------------------------------
213
205
  # Import guard + helpers
214
206
  # ---------------------------------------------------------------------------
@@ -484,13 +476,6 @@ def x_pg_upsert(**kwargs: Any) -> None:
484
476
  if opts.get("cleanup"):
485
477
  ups.cleanup()
486
478
 
487
- if not result.qa_passed:
488
- raise ErrInfo(
489
- "cmd",
490
- command_text=metacommandline,
491
- other_msg=_qa_failure_msg(result),
492
- )
493
-
494
479
 
495
480
  def x_pg_upsert_qa(**kwargs: Any) -> None:
496
481
  """PG_UPSERT QA FROM <staging> TO <base> TABLES <t1>, <t2> [options]
@@ -524,13 +509,6 @@ def x_pg_upsert_qa(**kwargs: Any) -> None:
524
509
  if opts.get("cleanup"):
525
510
  ups.cleanup()
526
511
 
527
- if not result.qa_passed:
528
- raise ErrInfo(
529
- "cmd",
530
- command_text=metacommandline,
531
- other_msg=_qa_failure_msg(result),
532
- )
533
-
534
512
 
535
513
  def x_pg_upsert_check(**kwargs: Any) -> None:
536
514
  """PG_UPSERT CHECK FROM <staging> TO <base> TABLES <t1>, <t2>
@@ -567,13 +545,6 @@ def x_pg_upsert_check(**kwargs: Any) -> None:
567
545
  if opts.get("cleanup"):
568
546
  ups.cleanup()
569
547
 
570
- if not result.qa_passed:
571
- raise ErrInfo(
572
- "cmd",
573
- command_text=metacommandline,
574
- other_msg=_qa_failure_msg(result),
575
- )
576
-
577
548
 
578
549
  # ---------------------------------------------------------------------------
579
550
  # Plugin registration
@@ -243,15 +243,17 @@ def _exec_sql(
243
243
 
244
244
  def _exec_metacommand(
245
245
  ctx: RuntimeContext,
246
- command: str,
246
+ cmd: str,
247
247
  source: str,
248
248
  line_no: int,
249
- localvars: SubVarSet | None = None,
250
249
  ) -> Any:
251
- """Dispatch a metacommand through the dispatch table."""
252
- # Build localvars from the command-list stack frame (see _exec_sql comment).
253
- effective_locals = _stack_localvars(ctx) or localvars
254
- cmd = substitute_vars(command, effective_locals, ctx=ctx)
250
+ """Dispatch a metacommand through the dispatch table.
251
+
252
+ *cmd* must already have ``!!$VAR!!`` substitution applied. The caller is
253
+ responsible for expansion so that side-effecting substitutions (counter
254
+ increments, ``$RANDOM``, ``$UUID``) are evaluated exactly once per
255
+ metacommand reference.
256
+ """
255
257
  if _VARLIKE.search(cmd):
256
258
  ctx.output.write(
257
259
  f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
@@ -355,13 +357,15 @@ def _execute_node(
355
357
  command = node.command
356
358
  if in_loop:
357
359
  command = _convert_deferred_vars(command)
358
- # Intercept BREAK before dispatch it controls loop flow
360
+ # Substitute once: the same expanded text is used for BREAK detection
361
+ # and dispatch. Calling substitute_vars twice would double-increment
362
+ # !!$COUNTER_N!! and re-roll !!$RANDOM!!/!!$UUID!! references.
359
363
  effective_locals = _stack_localvars(ctx) or localvars
360
364
  expanded = substitute_vars(command, effective_locals, ctx=ctx)
361
365
  if _BREAK_RX.match(expanded):
362
366
  raise _BreakLoop
363
367
  ctx.last_command = _FakeScriptCmd(node)
364
- _exec_metacommand(ctx, command, node.span.file, node.span.start_line, localvars)
368
+ _exec_metacommand(ctx, expanded, node.span.file, node.span.start_line)
365
369
 
366
370
  elif isinstance(node, IfBlock):
367
371
  ctx.last_command = _FakeScriptCmd(node)
@@ -19,7 +19,6 @@ from execsql.metacommands.upsert import (
19
19
  _FileWriterHandler,
20
20
  _build_result_from_qa_findings,
21
21
  _parse_tables_and_options,
22
- _qa_failure_msg,
23
22
  x_pg_upsert,
24
23
  x_pg_upsert_check,
25
24
  x_pg_upsert_qa,
@@ -541,7 +540,9 @@ class TestFullMode:
541
540
  assert "$PG_UPSERT_FINISHED_AT" in calls
542
541
  assert "$PG_UPSERT_RESULT_JSON" in calls
543
542
 
544
- def test_qa_failure_raises_errinfo(self, mock_state):
543
+ def test_qa_failure_does_not_raise(self, mock_state):
544
+ """QA failure surfaces via $PG_UPSERT_QA_PASSED; the user's own IF /
545
+ ON ERROR HALT decides control flow."""
545
546
  state, db = mock_state
546
547
  fake_result = FakeUpsertResult(
547
548
  tables=[
@@ -559,17 +560,15 @@ class TestFullMode:
559
560
  patch("execsql.metacommands.upsert._create_pgupsert") as mock_create,
560
561
  ):
561
562
  mock_create.return_value.run.return_value = fake_result
562
- with pytest.raises(ErrInfo) as exc_info:
563
- x_pg_upsert(
564
- staging_schema="staging",
565
- base_schema="public",
566
- tail="books",
567
- metacommandline="PG_UPSERT FROM staging TO public TABLES books",
568
- )
569
- err_msg = str(exc_info.value)
570
- assert "QA failed for: books" in err_msg
571
- # Subvars should still be set before the error
572
- assert state.subvars.add_substitution.called
563
+ x_pg_upsert(
564
+ staging_schema="staging",
565
+ base_schema="public",
566
+ tail="books",
567
+ metacommandline="PG_UPSERT FROM staging TO public TABLES books",
568
+ )
569
+ calls = {c[0][0]: c[0][1] for c in state.subvars.add_substitution.call_args_list}
570
+ assert calls["$PG_UPSERT_QA_PASSED"] == "FALSE"
571
+ assert calls["$PG_UPSERT_COMMITTED"] == "FALSE"
573
572
 
574
573
  def test_commit_keyword_passed(self, mock_state):
575
574
  state, db = mock_state
@@ -868,42 +867,6 @@ class TestUserCancelled:
868
867
  assert "cancelled" in str(exc_info.value)
869
868
 
870
869
 
871
- # ---------------------------------------------------------------------------
872
- # Logging bridge test
873
- # ---------------------------------------------------------------------------
874
-
875
-
876
- class TestQAFailureMessage:
877
- def test_lists_failed_tables(self):
878
- result = FakeUpsertResult(
879
- tables=[
880
- FakeTableResult(
881
- table_name="books",
882
- _qa_findings=[FakeQAError(table="books", check_type="null", details="title (3)")],
883
- ),
884
- FakeTableResult(table_name="authors"), # passes
885
- FakeTableResult(
886
- table_name="genres",
887
- _qa_findings=[FakeQAError(table="genres", check_type="fk", details="parent_id (5)")],
888
- ),
889
- ],
890
- )
891
- msg = _qa_failure_msg(result)
892
- assert msg == "PG_UPSERT QA failed for: books, genres"
893
-
894
- def test_single_failed_table(self):
895
- result = FakeUpsertResult(
896
- tables=[
897
- FakeTableResult(
898
- table_name="books",
899
- _qa_findings=[FakeQAError(table="books", check_type="pk", details="id (2)")],
900
- ),
901
- ],
902
- )
903
- msg = _qa_failure_msg(result)
904
- assert msg == "PG_UPSERT QA failed for: books"
905
-
906
-
907
870
  # ---------------------------------------------------------------------------
908
871
  # Logging bridge tests
909
872
  # ---------------------------------------------------------------------------
@@ -934,7 +897,7 @@ class TestLoggingBridge:
934
897
 
935
898
 
936
899
  class TestQAModeFailure:
937
- def test_qa_failure_raises_errinfo(self, mock_state):
900
+ def test_qa_failure_does_not_raise(self, mock_state):
938
901
  state, db = mock_state
939
902
 
940
903
  failed_result = FakeUpsertResult(
@@ -956,18 +919,19 @@ class TestQAModeFailure:
956
919
  mock_ups = mock_create.return_value
957
920
  mock_ups.qa_all.return_value = mock_ups
958
921
  mock_build.return_value = failed_result
959
- with pytest.raises(ErrInfo) as exc_info:
960
- x_pg_upsert_qa(
961
- staging_schema="staging",
962
- base_schema="public",
963
- tail="books",
964
- metacommandline="PG_UPSERT QA FROM staging TO public TABLES books",
965
- )
966
- assert "QA failed for: books" in str(exc_info.value)
922
+ x_pg_upsert_qa(
923
+ staging_schema="staging",
924
+ base_schema="public",
925
+ tail="books",
926
+ metacommandline="PG_UPSERT QA FROM staging TO public TABLES books",
927
+ )
928
+ calls = {c[0][0]: c[0][1] for c in state.subvars.add_substitution.call_args_list}
929
+ assert calls["$PG_UPSERT_QA_PASSED"] == "FALSE"
930
+ assert calls["$PG_UPSERT_COMMITTED"] == "FALSE"
967
931
 
968
932
 
969
933
  class TestCheckModeFailure:
970
- def test_check_failure_raises_errinfo(self, mock_state):
934
+ def test_check_failure_does_not_raise(self, mock_state):
971
935
  state, db = mock_state
972
936
 
973
937
  failed_result = FakeUpsertResult(
@@ -990,14 +954,14 @@ class TestCheckModeFailure:
990
954
  mock_ups.qa_column_existence.return_value = mock_ups
991
955
  mock_ups.qa_type_mismatch.return_value = mock_ups
992
956
  mock_build.return_value = failed_result
993
- with pytest.raises(ErrInfo) as exc_info:
994
- x_pg_upsert_check(
995
- staging_schema="staging",
996
- base_schema="public",
997
- tail="books",
998
- metacommandline="PG_UPSERT CHECK FROM staging TO public TABLES books",
999
- )
1000
- assert "QA failed for: books" in str(exc_info.value)
957
+ x_pg_upsert_check(
958
+ staging_schema="staging",
959
+ base_schema="public",
960
+ tail="books",
961
+ metacommandline="PG_UPSERT CHECK FROM staging TO public TABLES books",
962
+ )
963
+ calls = {c[0][0]: c[0][1] for c in state.subvars.add_substitution.call_args_list}
964
+ assert calls["$PG_UPSERT_QA_PASSED"] == "FALSE"
1001
965
 
1002
966
 
1003
967
  # ---------------------------------------------------------------------------
@@ -1427,15 +1391,15 @@ class TestExportFailures:
1427
1391
  patch("execsql.metacommands.upsert._create_pgupsert") as mock_create,
1428
1392
  ):
1429
1393
  mock_create.return_value.run.return_value = fake_result
1430
- with pytest.raises(ErrInfo):
1431
- x_pg_upsert(
1432
- staging_schema="staging",
1433
- base_schema="public",
1434
- tail="books EXPORT_FAILURES /tmp/out",
1435
- metacommandline="PG_UPSERT FROM staging TO public TABLES books EXPORT_FAILURES /tmp/out",
1436
- )
1437
- # Export MUST have happened before the ErrInfo was raised.
1394
+ x_pg_upsert(
1395
+ staging_schema="staging",
1396
+ base_schema="public",
1397
+ tail="books EXPORT_FAILURES /tmp/out",
1398
+ metacommandline="PG_UPSERT FROM staging TO public TABLES books EXPORT_FAILURES /tmp/out",
1399
+ )
1438
1400
  assert fake_result.export_calls == [("/tmp/out", "csv")]
1401
+ calls = {c[0][0]: c[0][1] for c in state.subvars.add_substitution.call_args_list}
1402
+ assert calls["$PG_UPSERT_QA_PASSED"] == "FALSE"
1439
1403
 
1440
1404
  def test_no_export_when_keyword_absent(self, mock_state):
1441
1405
  state, db = mock_state
@@ -154,6 +154,45 @@ class TestVariableSubstitution:
154
154
  rows = _query_db(tmp_path, "SELECT val FROM t")
155
155
  assert rows == [(8,)]
156
156
 
157
+ def test_counter_in_metacommand_starts_at_one(self, tmp_path):
158
+ # Regression: the AST executor previously called substitute_vars()
159
+ # twice per metacommand (once for BREAK detection, once for dispatch),
160
+ # which double-incremented $COUNTER_N so refs returned 2, 4, 6.
161
+ result = _run_ast(
162
+ '-- !x! WRITE "First: !!$COUNTER_1!!"\n'
163
+ '-- !x! WRITE "Second: !!$COUNTER_1!!"\n'
164
+ '-- !x! WRITE "Third: !!$COUNTER_1!!"\n',
165
+ tmp_path,
166
+ )
167
+ assert result.returncode == 0, result.stderr
168
+ assert "First: 1" in result.stdout
169
+ assert "Second: 2" in result.stdout
170
+ assert "Third: 3" in result.stdout
171
+
172
+ def test_counter_in_sql_starts_at_one(self, tmp_path):
173
+ # Sibling check: SQL-statement counters were already correct, but
174
+ # pin the behavior so a future regression in the SQL path is caught.
175
+ result = _run_ast(
176
+ "CREATE TABLE t (n INT);\n"
177
+ "INSERT INTO t VALUES (!!$COUNTER_2!!);\n"
178
+ "INSERT INTO t VALUES (!!$COUNTER_2!!);\n"
179
+ "INSERT INTO t VALUES (!!$COUNTER_2!!);\n",
180
+ tmp_path,
181
+ )
182
+ assert result.returncode == 0, result.stderr
183
+ rows = _query_db(tmp_path, "SELECT n FROM t ORDER BY n")
184
+ assert rows == [(1,), (2,), (3,)]
185
+
186
+ def test_counter_same_ref_twice_in_one_metacommand(self, tmp_path):
187
+ # Per docs: multiple references in one command share the same value.
188
+ result = _run_ast(
189
+ '-- !x! WRITE "A=!!$COUNTER_3!! B=!!$COUNTER_3!!"\n-- !x! WRITE "C=!!$COUNTER_3!!"\n',
190
+ tmp_path,
191
+ )
192
+ assert result.returncode == 0, result.stderr
193
+ assert "A=1 B=1" in result.stdout
194
+ assert "C=2" in result.stdout
195
+
157
196
 
158
197
  # ---------------------------------------------------------------------------
159
198
  # IF / ELSE / ELSEIF
@@ -648,7 +648,7 @@ wheels = [
648
648
 
649
649
  [[package]]
650
650
  name = "execsql2"
651
- version = "2.16.18"
651
+ version = "2.17.0"
652
652
  source = { editable = "." }
653
653
  dependencies = [
654
654
  { name = "python-dateutil" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes