execsql2 2.16.13__tar.gz → 2.16.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. {execsql2-2.16.13 → execsql2-2.16.14}/CHANGELOG.md +10 -0
  2. {execsql2-2.16.13 → execsql2-2.16.14}/PKG-INFO +1 -1
  3. {execsql2-2.16.13 → execsql2-2.16.14}/docs/about/divergence.md +2 -0
  4. {execsql2-2.16.13 → execsql2-2.16.14}/docs/reference/metacommands.md +13 -2
  5. {execsql2-2.16.13 → execsql2-2.16.14}/pyproject.toml +2 -2
  6. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/base.py +2 -0
  7. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/io_export.py +14 -10
  8. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/io_fileops.py +4 -6
  9. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/script/ast.py +3 -0
  10. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/script/executor.py +7 -3
  11. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/script/parser.py +20 -14
  12. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_ast_parser.py +56 -0
  13. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_executor.py +75 -0
  14. {execsql2-2.16.13 → execsql2-2.16.14}/uv.lock +1 -1
  15. {execsql2-2.16.13 → execsql2-2.16.14}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {execsql2-2.16.13 → execsql2-2.16.14}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {execsql2-2.16.13 → execsql2-2.16.14}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {execsql2-2.16.13 → execsql2-2.16.14}/.github/workflows/ci-cd.yml +0 -0
  19. {execsql2-2.16.13 → execsql2-2.16.14}/.gitignore +0 -0
  20. {execsql2-2.16.13 → execsql2-2.16.14}/.pre-commit-config.yaml +0 -0
  21. {execsql2-2.16.13 → execsql2-2.16.14}/.pre-commit-hooks.yaml +0 -0
  22. {execsql2-2.16.13 → execsql2-2.16.14}/.python-version +0 -0
  23. {execsql2-2.16.13 → execsql2-2.16.14}/.readthedocs.yaml +0 -0
  24. {execsql2-2.16.13 → execsql2-2.16.14}/CONTRIBUTING.md +0 -0
  25. {execsql2-2.16.13 → execsql2-2.16.14}/LICENSE.txt +0 -0
  26. {execsql2-2.16.13 → execsql2-2.16.14}/NOTICE +0 -0
  27. {execsql2-2.16.13 → execsql2-2.16.14}/README.md +0 -0
  28. {execsql2-2.16.13 → execsql2-2.16.14}/SECURITY.md +0 -0
  29. {execsql2-2.16.13 → execsql2-2.16.14}/docs/about/contributors.md +0 -0
  30. {execsql2-2.16.13 → execsql2-2.16.14}/docs/about/copyright.md +0 -0
  31. {execsql2-2.16.13 → execsql2-2.16.14}/docs/api/cli.md +0 -0
  32. {execsql2-2.16.13 → execsql2-2.16.14}/docs/api/db.md +0 -0
  33. {execsql2-2.16.13 → execsql2-2.16.14}/docs/api/exporters.md +0 -0
  34. {execsql2-2.16.13 → execsql2-2.16.14}/docs/api/importers.md +0 -0
  35. {execsql2-2.16.13 → execsql2-2.16.14}/docs/api/index.md +0 -0
  36. {execsql2-2.16.13 → execsql2-2.16.14}/docs/api/metacommands.md +0 -0
  37. {execsql2-2.16.13 → execsql2-2.16.14}/docs/dev/adding_db_adapters.md +0 -0
  38. {execsql2-2.16.13 → execsql2-2.16.14}/docs/dev/adding_exporters.md +0 -0
  39. {execsql2-2.16.13 → execsql2-2.16.14}/docs/dev/adding_importers.md +0 -0
  40. {execsql2-2.16.13 → execsql2-2.16.14}/docs/dev/adding_metacommands.md +0 -0
  41. {execsql2-2.16.13 → execsql2-2.16.14}/docs/dev/architecture.md +0 -0
  42. {execsql2-2.16.13 → execsql2-2.16.14}/docs/getting-started/installation.md +0 -0
  43. {execsql2-2.16.13 → execsql2-2.16.14}/docs/getting-started/requirements.md +0 -0
  44. {execsql2-2.16.13 → execsql2-2.16.14}/docs/getting-started/syntax.md +0 -0
  45. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/debugging.md +0 -0
  46. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/documentation.md +0 -0
  47. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/encoding.md +0 -0
  48. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/examples.md +0 -0
  49. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/formatter.md +0 -0
  50. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/logging.md +0 -0
  51. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/sql_syntax.md +0 -0
  52. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/usage.md +0 -0
  53. {execsql2-2.16.13 → execsql2-2.16.14}/docs/guides/using_scripts.md +0 -0
  54. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/Compare_planets.png +0 -0
  55. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/actions.png +0 -0
  56. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/actions2.png +0 -0
  57. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/checkboxes.png +0 -0
  58. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/connect.b64 +0 -0
  59. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/connect.png +0 -0
  60. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/create_conf.png +0 -0
  61. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/data_error1_screenshot.jpg +0 -0
  62. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/entry_form.png +0 -0
  63. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/execsql_console.png +0 -0
  64. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/execsql_logo_01.png +0 -0
  65. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/fatals.png +0 -0
  66. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/logo_small.png +0 -0
  67. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/pause_terminal.png +0 -0
  68. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/pause_terminal_sm.b64 +0 -0
  69. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/pause_terminal_sm.png +0 -0
  70. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/prompt_compare.png +0 -0
  71. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/set_build_commands.jpg +0 -0
  72. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/unit_conversions.b64 +0 -0
  73. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/unit_conversions_029.png +0 -0
  74. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/unmatched.png +0 -0
  75. {execsql2-2.16.13 → execsql2-2.16.14}/docs/images/vim_execsql_highlight.png +0 -0
  76. {execsql2-2.16.13 → execsql2-2.16.14}/docs/index.md +0 -0
  77. {execsql2-2.16.13 → execsql2-2.16.14}/docs/reference/configuration.md +0 -0
  78. {execsql2-2.16.13 → execsql2-2.16.14}/docs/reference/security.md +0 -0
  79. {execsql2-2.16.13 → execsql2-2.16.14}/docs/reference/substitution_vars.md +0 -0
  80. {execsql2-2.16.13 → execsql2-2.16.14}/extras/plugin-template/README.md +0 -0
  81. {execsql2-2.16.13 → execsql2-2.16.14}/extras/plugin-template/pyproject.toml +0 -0
  82. {execsql2-2.16.13 → execsql2-2.16.14}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  83. {execsql2-2.16.13 → execsql2-2.16.14}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  84. {execsql2-2.16.13 → execsql2-2.16.14}/extras/vscode-execsql/README.md +0 -0
  85. {execsql2-2.16.13 → execsql2-2.16.14}/extras/vscode-execsql/package.json +0 -0
  86. {execsql2-2.16.13 → execsql2-2.16.14}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  87. {execsql2-2.16.13 → execsql2-2.16.14}/justfile +0 -0
  88. {execsql2-2.16.13 → execsql2-2.16.14}/scripts/generate_vscode_grammar.py +0 -0
  89. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/__init__.py +0 -0
  90. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/__main__.py +0 -0
  91. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/api.py +0 -0
  92. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/cli/__init__.py +0 -0
  93. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/cli/dsn.py +0 -0
  94. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/cli/help.py +0 -0
  95. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/cli/lint.py +0 -0
  96. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/cli/lint_ast.py +0 -0
  97. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/cli/run.py +0 -0
  98. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/config.py +0 -0
  99. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/__init__.py +0 -0
  100. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/access.py +0 -0
  101. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/dsn.py +0 -0
  102. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/duckdb.py +0 -0
  103. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/factory.py +0 -0
  104. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/firebird.py +0 -0
  105. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/mysql.py +0 -0
  106. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/oracle.py +0 -0
  107. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/postgres.py +0 -0
  108. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/sqlite.py +0 -0
  109. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/db/sqlserver.py +0 -0
  110. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/debug/__init__.py +0 -0
  111. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/debug/repl.py +0 -0
  112. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exceptions.py +0 -0
  113. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/__init__.py +0 -0
  114. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/base.py +0 -0
  115. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/delimited.py +0 -0
  116. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/duckdb.py +0 -0
  117. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/feather.py +0 -0
  118. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/html.py +0 -0
  119. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/json.py +0 -0
  120. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/latex.py +0 -0
  121. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/markdown.py +0 -0
  122. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/ods.py +0 -0
  123. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/parquet.py +0 -0
  124. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/pretty.py +0 -0
  125. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/protocol.py +0 -0
  126. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/raw.py +0 -0
  127. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/sqlite.py +0 -0
  128. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/templates.py +0 -0
  129. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/values.py +0 -0
  130. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/xls.py +0 -0
  131. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/xlsx.py +0 -0
  132. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/xml.py +0 -0
  133. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/yaml.py +0 -0
  134. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/exporters/zip.py +0 -0
  135. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/format.py +0 -0
  136. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/gui/__init__.py +0 -0
  137. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/gui/base.py +0 -0
  138. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/gui/console.py +0 -0
  139. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/gui/desktop.py +0 -0
  140. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/gui/tui.py +0 -0
  141. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/importers/__init__.py +0 -0
  142. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/importers/base.py +0 -0
  143. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/importers/csv.py +0 -0
  144. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/importers/feather.py +0 -0
  145. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/importers/json.py +0 -0
  146. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/importers/ods.py +0 -0
  147. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/importers/xls.py +0 -0
  148. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/__init__.py +0 -0
  149. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/conditions.py +0 -0
  150. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/connect.py +0 -0
  151. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/control.py +0 -0
  152. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/data.py +0 -0
  153. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/debug.py +0 -0
  154. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/dispatch.py +0 -0
  155. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/io.py +0 -0
  156. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/io_import.py +0 -0
  157. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/io_write.py +0 -0
  158. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/prompt.py +0 -0
  159. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/script_ext.py +0 -0
  160. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/system.py +0 -0
  161. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/metacommands/upsert.py +0 -0
  162. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/models.py +0 -0
  163. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/parser.py +0 -0
  164. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/plugins.py +0 -0
  165. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/py.typed +0 -0
  166. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/script/__init__.py +0 -0
  167. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/script/control.py +0 -0
  168. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/script/engine.py +0 -0
  169. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/script/variables.py +0 -0
  170. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/state.py +0 -0
  171. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/types.py +0 -0
  172. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/__init__.py +0 -0
  173. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/auth.py +0 -0
  174. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/crypto.py +0 -0
  175. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/datetime.py +0 -0
  176. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/errors.py +0 -0
  177. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/fileio.py +0 -0
  178. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/gui.py +0 -0
  179. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/mail.py +0 -0
  180. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/numeric.py +0 -0
  181. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/regex.py +0 -0
  182. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/strings.py +0 -0
  183. {execsql2-2.16.13 → execsql2-2.16.14}/src/execsql/utils/timer.py +0 -0
  184. {execsql2-2.16.13 → execsql2-2.16.14}/templates/README.md +0 -0
  185. {execsql2-2.16.13 → execsql2-2.16.14}/templates/config_settings.sqlite +0 -0
  186. {execsql2-2.16.13 → execsql2-2.16.14}/templates/example_config_prompt.sql +0 -0
  187. {execsql2-2.16.13 → execsql2-2.16.14}/templates/execsql.conf +0 -0
  188. {execsql2-2.16.13 → execsql2-2.16.14}/templates/make_config_db.sql +0 -0
  189. {execsql2-2.16.13 → execsql2-2.16.14}/templates/md_compare.sql +0 -0
  190. {execsql2-2.16.13 → execsql2-2.16.14}/templates/md_glossary.sql +0 -0
  191. {execsql2-2.16.13 → execsql2-2.16.14}/templates/md_upsert.sql +0 -0
  192. {execsql2-2.16.13 → execsql2-2.16.14}/templates/pg_compare.sql +0 -0
  193. {execsql2-2.16.13 → execsql2-2.16.14}/templates/pg_glossary.sql +0 -0
  194. {execsql2-2.16.13 → execsql2-2.16.14}/templates/pg_upsert.sql +0 -0
  195. {execsql2-2.16.13 → execsql2-2.16.14}/templates/script_template.sql +0 -0
  196. {execsql2-2.16.13 → execsql2-2.16.14}/templates/ss_compare.sql +0 -0
  197. {execsql2-2.16.13 → execsql2-2.16.14}/templates/ss_glossary.sql +0 -0
  198. {execsql2-2.16.13 → execsql2-2.16.14}/templates/ss_upsert.sql +0 -0
  199. {execsql2-2.16.13 → execsql2-2.16.14}/tests/__init__.py +0 -0
  200. {execsql2-2.16.13 → execsql2-2.16.14}/tests/cli/__init__.py +0 -0
  201. {execsql2-2.16.13 → execsql2-2.16.14}/tests/cli/test_cli.py +0 -0
  202. {execsql2-2.16.13 → execsql2-2.16.14}/tests/cli/test_cli_e2e.py +0 -0
  203. {execsql2-2.16.13 → execsql2-2.16.14}/tests/cli/test_cli_run.py +0 -0
  204. {execsql2-2.16.13 → execsql2-2.16.14}/tests/cli/test_lint.py +0 -0
  205. {execsql2-2.16.13 → execsql2-2.16.14}/tests/cli/test_ping.py +0 -0
  206. {execsql2-2.16.13 → execsql2-2.16.14}/tests/cli/test_profile.py +0 -0
  207. {execsql2-2.16.13 → execsql2-2.16.14}/tests/conftest.py +0 -0
  208. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/__init__.py +0 -0
  209. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/test_base.py +0 -0
  210. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/test_db_adapters_mocked.py +0 -0
  211. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/test_dsn.py +0 -0
  212. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/test_duckdb.py +0 -0
  213. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/test_factory.py +0 -0
  214. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/test_postgres.py +0 -0
  215. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/test_sqlite.py +0 -0
  216. {execsql2-2.16.13 → execsql2-2.16.14}/tests/db/test_sqlite_extra.py +0 -0
  217. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/__init__.py +0 -0
  218. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_base.py +0 -0
  219. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_db.py +0 -0
  220. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_delimited.py +0 -0
  221. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_duckdb_exporter.py +0 -0
  222. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_exporters.py +0 -0
  223. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_feather.py +0 -0
  224. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_html_extended.py +0 -0
  225. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_html_latex.py +0 -0
  226. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_json.py +0 -0
  227. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_json_extended.py +0 -0
  228. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_latex_extended.py +0 -0
  229. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_markdown.py +0 -0
  230. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_ods.py +0 -0
  231. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_parquet.py +0 -0
  232. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_pretty_extended.py +0 -0
  233. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_raw_extended.py +0 -0
  234. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_sqlite_exporter.py +0 -0
  235. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_templates.py +0 -0
  236. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_templates_extended.py +0 -0
  237. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_values_extended.py +0 -0
  238. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_xls_xlsx.py +0 -0
  239. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_xlsx.py +0 -0
  240. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_xml.py +0 -0
  241. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_yaml.py +0 -0
  242. {execsql2-2.16.13 → execsql2-2.16.14}/tests/exporters/test_zip.py +0 -0
  243. {execsql2-2.16.13 → execsql2-2.16.14}/tests/gui/__init__.py +0 -0
  244. {execsql2-2.16.13 → execsql2-2.16.14}/tests/gui/test_backends.py +0 -0
  245. {execsql2-2.16.13 → execsql2-2.16.14}/tests/gui/test_compare_stats.py +0 -0
  246. {execsql2-2.16.13 → execsql2-2.16.14}/tests/gui/test_compute_row_diffs.py +0 -0
  247. {execsql2-2.16.13 → execsql2-2.16.14}/tests/importers/__init__.py +0 -0
  248. {execsql2-2.16.13 → execsql2-2.16.14}/tests/importers/test_base_extended.py +0 -0
  249. {execsql2-2.16.13 → execsql2-2.16.14}/tests/importers/test_csv_edge_cases.py +0 -0
  250. {execsql2-2.16.13 → execsql2-2.16.14}/tests/importers/test_csv_importer.py +0 -0
  251. {execsql2-2.16.13 → execsql2-2.16.14}/tests/importers/test_feather_importer.py +0 -0
  252. {execsql2-2.16.13 → execsql2-2.16.14}/tests/importers/test_json_importer.py +0 -0
  253. {execsql2-2.16.13 → execsql2-2.16.14}/tests/importers/test_ods_importer.py +0 -0
  254. {execsql2-2.16.13 → execsql2-2.16.14}/tests/importers/test_xls_importer.py +0 -0
  255. {execsql2-2.16.13 → execsql2-2.16.14}/tests/integration/__init__.py +0 -0
  256. {execsql2-2.16.13 → execsql2-2.16.14}/tests/integration/conftest.py +0 -0
  257. {execsql2-2.16.13 → execsql2-2.16.14}/tests/integration/test_dsn.py +0 -0
  258. {execsql2-2.16.13 → execsql2-2.16.14}/tests/integration/test_duckdb.py +0 -0
  259. {execsql2-2.16.13 → execsql2-2.16.14}/tests/integration/test_mysql.py +0 -0
  260. {execsql2-2.16.13 → execsql2-2.16.14}/tests/integration/test_postgres.py +0 -0
  261. {execsql2-2.16.13 → execsql2-2.16.14}/tests/integration/test_sqlite.py +0 -0
  262. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/__init__.py +0 -0
  263. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_assert.py +0 -0
  264. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_breakpoint.py +0 -0
  265. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_connect.py +0 -0
  266. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_io_export.py +0 -0
  267. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_io_import.py +0 -0
  268. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands.py +0 -0
  269. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_connect.py +0 -0
  270. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_data.py +0 -0
  271. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_extended.py +0 -0
  272. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  273. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_io.py +0 -0
  274. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  275. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  276. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_system.py +0 -0
  277. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  278. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_pg_upsert.py +0 -0
  279. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_row_count.py +0 -0
  280. {execsql2-2.16.13 → execsql2-2.16.14}/tests/metacommands/test_show_scripts.py +0 -0
  281. {execsql2-2.16.13 → execsql2-2.16.14}/tests/scripts/__init__.py +0 -0
  282. {execsql2-2.16.13 → execsql2-2.16.14}/tests/scripts/fixtures/control_flow.sql +0 -0
  283. {execsql2-2.16.13 → execsql2-2.16.14}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  284. {execsql2-2.16.13 → execsql2-2.16.14}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  285. {execsql2-2.16.13 → execsql2-2.16.14}/tests/scripts/fixtures/smoke.sql +0 -0
  286. {execsql2-2.16.13 → execsql2-2.16.14}/tests/scripts/test_sql_scripts.py +0 -0
  287. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_api.py +0 -0
  288. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_ast.py +0 -0
  289. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_config.py +0 -0
  290. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_config_data.py +0 -0
  291. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_config_extended.py +0 -0
  292. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_debug_repl.py +0 -0
  293. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_engine.py +0 -0
  294. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_error_messages.py +0 -0
  295. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_exceptions.py +0 -0
  296. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_format.py +0 -0
  297. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_mail.py +0 -0
  298. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_models.py +0 -0
  299. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_package.py +0 -0
  300. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_parser.py +0 -0
  301. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_parser_params.py +0 -0
  302. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_plugins.py +0 -0
  303. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_registry.py +0 -0
  304. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_script.py +0 -0
  305. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_state.py +0 -0
  306. {execsql2-2.16.13 → execsql2-2.16.14}/tests/test_types.py +0 -0
  307. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/__init__.py +0 -0
  308. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_auth.py +0 -0
  309. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_auth_extra.py +0 -0
  310. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_crypto.py +0 -0
  311. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_datetime.py +0 -0
  312. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_errors.py +0 -0
  313. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_errors_extra.py +0 -0
  314. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_fileio.py +0 -0
  315. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_fileio_extra.py +0 -0
  316. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_numeric.py +0 -0
  317. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_regex.py +0 -0
  318. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_strings.py +0 -0
  319. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_timer.py +0 -0
  320. {execsql2-2.16.13 → execsql2-2.16.14}/tests/utils/test_timer_extra.py +0 -0
  321. {execsql2-2.16.13 → execsql2-2.16.14}/zensical.toml +0 -0
@@ -13,6 +13,16 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.16.14] - 2026-05-01
17
+
18
+ ### Fixed
19
+
20
+ - ELSEIF conditions now support ANDIF/ORIF modifiers. Previously, ANDIF/ORIF after an ELSEIF were silently attached to the parent IF condition instead of the ELSEIF clause, meaning the compound condition was never evaluated correctly. ELSEIF + ANDIF/ORIF now works the same way as IF + ANDIF/ORIF.
21
+ - Unknown AST node types now raise an error instead of being silently ignored during execution.
22
+ - Cursor leak in `select_rowsource()` and `select_rowdict()`: cursor is now closed on query execution failure. High-traffic callers (EXPORT, COPY) now explicitly close the row generator on error instead of relying on garbage collection.
23
+
24
+ ______________________________________________________________________
25
+
16
26
  ## [2.16.13] - 2026-05-01
17
27
 
18
28
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.16.13
3
+ Version: 2.16.14
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
@@ -310,6 +310,8 @@ These are behavioral changes driven by security or correctness issues in the ups
310
310
  | Empty script name in error msg | `_execute_script_direct()` and `_execute_script_textual_console()` no longer append "in script , line 0" to uncaught-exception messages when `current_script_line()` returns an empty string. |
311
311
  | `PROMPT COMPARE` diff comparison | Diff engine uses native Python equality instead of string comparison — numeric types, Decimals, and booleans compare correctly. `None` is distinguished from empty string. Columns are matched by name (not position), key columns are excluded from comparison, and duplicate PKs keep the first row. |
312
312
  | `win_config_file` broken | Checked `os.name == "windows"` which Python never returns (correct value is `"nt"`). Fixed to `os.name == "nt"`. Inherited from upstream. |
313
+ | ELSEIF + ANDIF/ORIF | ANDIF/ORIF after an ELSEIF were silently attached to the parent IF condition instead of the ELSEIF clause. The compound condition was never evaluated for the ELSEIF branch. Fixed in the AST parser and executor. |
314
+ | Cursor leak in `select_rowsource()` | Cursor was not closed on query execution failure. Row generators in EXPORT and COPY paths were not explicitly closed on error, relying on garbage collection for cleanup. |
313
315
  | `NumericParser` right-associative | Arithmetic operators were parsed right-to-left. `10 - 3 - 2` evaluated as `9` instead of `5`. Fixed to left-associative parsing. Inherited from upstream. |
314
316
  | Empty-column check precedence | `DataTable` and `Database.populate_table()` had an operator precedence bug in the extra-column emptiness check — a redundant `and conf.del_empty_cols` caused incorrect short-circuit evaluation. Inherited from upstream. |
315
317
  | SQLite import string processing | `SQLiteDatabase.populate_table()` applied `trim_strings`, `replace_newlines`, and `empty_strings` after copying row data, so processing never reached the INSERT. Fixed to process before extraction. Inherited from upstream. |
@@ -1372,7 +1372,7 @@ ENDIF
1372
1372
 
1373
1373
  Multiple ELSEIF clauses can be used within a single multi-line IF metacommand. An ELSE clause can be used in combination with ELSEIF clauses, but this is not recommended because the results are not likely to be what you expect---the ELSE keyword only inverts the current truth state, it does not provide an alternative to all preceding ELSEIF clauses. To achieve the effect of a case or switch statement, use only ELSEIF clauses without a final ELSE clause.
1374
1374
 
1375
- The ANDIF metacommand allows you to test for the conjunction of two conditional expressions using two separate metacommands instead of one. This may be beneficial for clarity. The simplest form of usage of the ANDIF clause is:
1375
+ The ANDIF metacommand allows you to test for the conjunction of two conditional expressions using two separate metacommands instead of one. This may be beneficial for clarity. ANDIF and ORIF can follow either an IF or an ELSEIF metacommand. The simplest form of usage of the ANDIF clause is:
1376
1376
 
1377
1377
  ```
1378
1378
  IF(<conditional expression>)
@@ -1381,7 +1381,7 @@ ANDIF(<conditional expression>)
1381
1381
  ENDIF
1382
1382
  ```
1383
1383
 
1384
- The ANDIF metacommand does not have to immediately follow the IF metacommand. It could instead follow an ELSE statement, or appear anywhere at all within a multi-line IF metacommand. Usage patterns other than that illustrated above may be difficult to interpret, however, and nested IF metacommands may be preferable to complex uses of the ANDIF clause.
1384
+ The ANDIF metacommand does not have to immediately follow the IF metacommand. It could instead follow an ELSEIF statement. Usage patterns other than those illustrated here may be difficult to interpret, however, and nested IF metacommands may be preferable to complex uses of the ANDIF clause.
1385
1385
 
1386
1386
  The ORIF metacommand is similar to the ANDIF clause, but allows you to test for the disjunction of two conditional expressions using two different metacommands. The simplest form of usage of the ORIF clause is:
1387
1387
 
@@ -1392,6 +1392,17 @@ ORIF(<conditional expression>)
1392
1392
  ENDIF
1393
1393
  ```
1394
1394
 
1395
+ ANDIF and ORIF can also compound an ELSEIF condition:
1396
+
1397
+ ```
1398
+ IF(<conditional expression>)
1399
+ <SQL statements and metacommands>
1400
+ ELSEIF(<conditional expression>)
1401
+ ANDIF(<conditional expression>)
1402
+ <SQL statements and metacommands>
1403
+ ENDIF
1404
+ ```
1405
+
1395
1406
  The IF metacommands can be used not only to control a single stream of script commands, but also to loop over sets of SQL statements and metacommands, as shown in [Example 6](../guides/examples.md#example6).
1396
1407
 
1397
1408
  The IF metacommands cannot be used within a SQL statement (nor can any other metacommands). This restriction prohibits constructions such as:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.16.13"
7
+ version = "2.16.14"
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.13"
168
+ current_version = "2.16.14"
169
169
  commit = true
170
170
  tag = true
171
171
  tag_name = "v{new_version}"
@@ -226,6 +226,7 @@ class Database(ABC):
226
226
  try:
227
227
  curs.execute(sql)
228
228
  except Exception:
229
+ curs.close()
229
230
  self.rollback()
230
231
  raise
231
232
  try:
@@ -264,6 +265,7 @@ class Database(ABC):
264
265
  try:
265
266
  curs.execute(sql)
266
267
  except Exception:
268
+ curs.close()
267
269
  self.rollback()
268
270
  raise
269
271
  try:
@@ -136,16 +136,20 @@ def _dispatch_format(
136
136
  raise
137
137
  except Exception as e:
138
138
  raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
139
- if filefmt == "raw":
140
- write_query_raw(outfile, rows, db.encoding, append, zipfile=zipfilename)
141
- elif filefmt == "b64":
142
- write_query_b64(outfile, rows, append, zipfile=zipfilename)
143
- elif filefmt == "feather":
144
- write_query_to_feather(outfile, hdrs, rows)
145
- elif filefmt == "parquet":
146
- write_query_to_parquet(outfile, hdrs, rows)
147
- else:
148
- write_delimited_file(outfile, filefmt, hdrs, rows, _state.conf.output_encoding, append, zipfilename)
139
+ try:
140
+ if filefmt == "raw":
141
+ write_query_raw(outfile, rows, db.encoding, append, zipfile=zipfilename)
142
+ elif filefmt == "b64":
143
+ write_query_b64(outfile, rows, append, zipfile=zipfilename)
144
+ elif filefmt == "feather":
145
+ write_query_to_feather(outfile, hdrs, rows)
146
+ elif filefmt == "parquet":
147
+ write_query_to_parquet(outfile, hdrs, rows)
148
+ else:
149
+ write_delimited_file(outfile, filefmt, hdrs, rows, _state.conf.output_encoding, append, zipfilename)
150
+ except BaseException:
151
+ rows.close()
152
+ raise
149
153
 
150
154
 
151
155
  # ---------------------------------------------------------------------------
@@ -106,10 +106,9 @@ def x_copy(**kwargs: Any) -> None:
106
106
  try:
107
107
  db2.populate_table(schema2, table2, rows, hdrs, get_ts)
108
108
  db2.commit()
109
- except ErrInfo:
109
+ except BaseException:
110
+ rows.close()
110
111
  raise
111
- except Exception as e:
112
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
113
112
 
114
113
 
115
114
  def x_copy_query(**kwargs: Any) -> None:
@@ -181,10 +180,9 @@ def x_copy_query(**kwargs: Any) -> None:
181
180
  try:
182
181
  db2.populate_table(schema2, table2, rows, hdrs, get_ts)
183
182
  db2.commit()
184
- except ErrInfo:
183
+ except BaseException:
184
+ rows.close()
185
185
  raise
186
- except Exception as e:
187
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
188
186
 
189
187
 
190
188
  def x_zip(**kwargs: Any) -> None:
@@ -209,11 +209,14 @@ class ElseIfClause:
209
209
  Attributes:
210
210
  condition: The condition expression text (e.g. ``"HAS_ROWS"``).
211
211
  span: Source location of the ELSEIF line itself.
212
+ condition_modifiers: ANDIF/ORIF modifiers that compound the ELSEIF
213
+ condition, evaluated left-to-right at runtime.
212
214
  body: Nodes executed when this condition is true.
213
215
  """
214
216
 
215
217
  condition: str
216
218
  span: SourceSpan
219
+ condition_modifiers: list[ConditionModifier] = field(default_factory=list)
217
220
  body: list[Node] = field(default_factory=list)
218
221
 
219
222
 
@@ -384,6 +384,12 @@ def _execute_node(
384
384
  ctx.last_command = _FakeScriptCmd(node)
385
385
  _execute_include(ctx, node, localvars)
386
386
 
387
+ else:
388
+ raise ErrInfo(
389
+ type="error",
390
+ other_msg=f"Unhandled AST node type: {type(node).__name__} at {node.span}",
391
+ )
392
+
387
393
 
388
394
  # ---------------------------------------------------------------------------
389
395
  # Block executors
@@ -404,9 +410,7 @@ def _execute_if(
404
410
 
405
411
  # Try ELSEIF clauses
406
412
  for clause in node.elseif_clauses:
407
- effective_locals = _stack_localvars(ctx)
408
- expanded = substitute_vars(clause.condition, effective_locals, ctx=ctx)
409
- if xcmd_test(expanded):
413
+ if _eval_condition(ctx, clause.condition, clause.condition_modifiers):
410
414
  _execute_nodes(ctx, clause.body, node.span.file, localvars, in_loop=in_loop)
411
415
  return
412
416
 
@@ -580,14 +580,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
580
580
  command_text=line,
581
581
  other_msg=f"ANDIF without matching IF on line {file_lineno} of {source_name}.",
582
582
  )
583
- if_node = block_stack[-1].node
584
- if_node.condition_modifiers.append( # type: ignore[union-attr]
585
- ConditionModifier(
586
- kind="AND",
587
- condition=m.group("cond").strip(),
588
- span=SourceSpan(source_name, file_lineno),
589
- ),
583
+ modifier = ConditionModifier(
584
+ kind="AND",
585
+ condition=m.group("cond").strip(),
586
+ span=SourceSpan(source_name, file_lineno),
590
587
  )
588
+ frame = block_stack[-1]
589
+ if_node = frame.node
590
+ if frame._in_elseif and if_node.elseif_clauses: # type: ignore[union-attr]
591
+ if_node.elseif_clauses[-1].condition_modifiers.append(modifier) # type: ignore[union-attr]
592
+ else:
593
+ if_node.condition_modifiers.append(modifier) # type: ignore[union-attr]
591
594
  continue
592
595
 
593
596
  # -- ORIF --
@@ -599,14 +602,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
599
602
  command_text=line,
600
603
  other_msg=f"ORIF without matching IF on line {file_lineno} of {source_name}.",
601
604
  )
602
- if_node = block_stack[-1].node
603
- if_node.condition_modifiers.append( # type: ignore[union-attr]
604
- ConditionModifier(
605
- kind="OR",
606
- condition=m.group("cond").strip(),
607
- span=SourceSpan(source_name, file_lineno),
608
- ),
605
+ modifier = ConditionModifier(
606
+ kind="OR",
607
+ condition=m.group("cond").strip(),
608
+ span=SourceSpan(source_name, file_lineno),
609
609
  )
610
+ frame = block_stack[-1]
611
+ if_node = frame.node
612
+ if frame._in_elseif and if_node.elseif_clauses: # type: ignore[union-attr]
613
+ if_node.elseif_clauses[-1].condition_modifiers.append(modifier) # type: ignore[union-attr]
614
+ else:
615
+ if_node.condition_modifiers.append(modifier) # type: ignore[union-attr]
610
616
  continue
611
617
 
612
618
  # -- ELSE --
@@ -295,6 +295,62 @@ class TestIfBlock:
295
295
  assert node.condition_modifiers[1].kind == "OR"
296
296
  assert len(node.body) == 1
297
297
 
298
+ def test_elseif_andif_modifier(self):
299
+ """ANDIF after ELSEIF is stored on the ElseIfClause, not the IfBlock."""
300
+ script = "-- !x! IF (COND_A)\nSELECT 1;\n-- !x! ELSEIF (COND_B)\n-- !x! ANDIF (COND_C)\nSELECT 2;\n-- !x! ENDIF"
301
+ node = _first(script)
302
+ assert isinstance(node, IfBlock)
303
+ assert len(node.condition_modifiers) == 0
304
+ assert len(node.elseif_clauses) == 1
305
+ clause = node.elseif_clauses[0]
306
+ assert clause.condition == "COND_B"
307
+ assert len(clause.condition_modifiers) == 1
308
+ assert clause.condition_modifiers[0].kind == "AND"
309
+ assert clause.condition_modifiers[0].condition == "COND_C"
310
+ assert len(clause.body) == 1
311
+
312
+ def test_elseif_orif_modifier(self):
313
+ """ORIF after ELSEIF is stored on the ElseIfClause."""
314
+ script = "-- !x! IF (COND_A)\nSELECT 1;\n-- !x! ELSEIF (COND_B)\n-- !x! ORIF (COND_C)\nSELECT 2;\n-- !x! ENDIF"
315
+ node = _first(script)
316
+ assert isinstance(node, IfBlock)
317
+ assert len(node.condition_modifiers) == 0
318
+ clause = node.elseif_clauses[0]
319
+ assert len(clause.condition_modifiers) == 1
320
+ assert clause.condition_modifiers[0].kind == "OR"
321
+ assert clause.condition_modifiers[0].condition == "COND_C"
322
+
323
+ def test_elseif_multiple_modifiers(self):
324
+ """Multiple ANDIF/ORIF after ELSEIF."""
325
+ script = (
326
+ "-- !x! IF (COND_A)\nSELECT 1;\n"
327
+ "-- !x! ELSEIF (COND_B)\n-- !x! ANDIF (COND_C)\n-- !x! ORIF (COND_D)\nSELECT 2;\n"
328
+ "-- !x! ENDIF"
329
+ )
330
+ node = _first(script)
331
+ clause = node.elseif_clauses[0]
332
+ assert len(clause.condition_modifiers) == 2
333
+ assert clause.condition_modifiers[0].kind == "AND"
334
+ assert clause.condition_modifiers[0].condition == "COND_C"
335
+ assert clause.condition_modifiers[1].kind == "OR"
336
+ assert clause.condition_modifiers[1].condition == "COND_D"
337
+
338
+ def test_if_andif_then_elseif_andif(self):
339
+ """ANDIF on IF stays on IF; ANDIF on ELSEIF stays on ELSEIF."""
340
+ script = (
341
+ "-- !x! IF (COND_A)\n-- !x! ANDIF (COND_B)\nSELECT 1;\n"
342
+ "-- !x! ELSEIF (COND_C)\n-- !x! ANDIF (COND_D)\nSELECT 2;\n"
343
+ "-- !x! ENDIF"
344
+ )
345
+ node = _first(script)
346
+ assert isinstance(node, IfBlock)
347
+ assert len(node.condition_modifiers) == 1
348
+ assert node.condition_modifiers[0].condition == "COND_B"
349
+ assert len(node.elseif_clauses) == 1
350
+ clause = node.elseif_clauses[0]
351
+ assert len(clause.condition_modifiers) == 1
352
+ assert clause.condition_modifiers[0].condition == "COND_D"
353
+
298
354
  def test_andif_without_if_raises(self):
299
355
  with pytest.raises(ErrInfo, match="ANDIF without matching IF"):
300
356
  parse_string("-- !x! ANDIF (COND)")
@@ -238,6 +238,81 @@ class TestIfBlock:
238
238
  assert result.returncode == 0
239
239
  assert _query_db(tmp_path, "SELECT x FROM t") == [(1,)]
240
240
 
241
+ def test_elseif_andif(self, tmp_path):
242
+ """ELSEIF with ANDIF modifier — both conditions must be true."""
243
+ result = _run_ast(
244
+ "-- !x! SUB a 1\n"
245
+ "-- !x! SUB b 2\n"
246
+ "CREATE TABLE t (x INT);\n"
247
+ "-- !x! IF (EQUALS(!!a!!, 99))\n"
248
+ "INSERT INTO t VALUES (1);\n"
249
+ "-- !x! ELSEIF (EQUALS(!!a!!, 1))\n"
250
+ "-- !x! ANDIF (EQUALS(!!b!!, 2))\n"
251
+ "INSERT INTO t VALUES (2);\n"
252
+ "-- !x! ELSE\n"
253
+ "INSERT INTO t VALUES (3);\n"
254
+ "-- !x! ENDIF",
255
+ tmp_path,
256
+ )
257
+ assert result.returncode == 0
258
+ assert _query_db(tmp_path, "SELECT x FROM t") == [(2,)]
259
+
260
+ def test_elseif_andif_false(self, tmp_path):
261
+ """ELSEIF with ANDIF where the ANDIF is false — should fall to ELSE."""
262
+ result = _run_ast(
263
+ "-- !x! SUB a 1\n"
264
+ "-- !x! SUB b 2\n"
265
+ "CREATE TABLE t (x INT);\n"
266
+ "-- !x! IF (EQUALS(!!a!!, 99))\n"
267
+ "INSERT INTO t VALUES (1);\n"
268
+ "-- !x! ELSEIF (EQUALS(!!a!!, 1))\n"
269
+ "-- !x! ANDIF (EQUALS(!!b!!, 99))\n"
270
+ "INSERT INTO t VALUES (2);\n"
271
+ "-- !x! ELSE\n"
272
+ "INSERT INTO t VALUES (3);\n"
273
+ "-- !x! ENDIF",
274
+ tmp_path,
275
+ )
276
+ assert result.returncode == 0
277
+ assert _query_db(tmp_path, "SELECT x FROM t") == [(3,)]
278
+
279
+ def test_elseif_orif(self, tmp_path):
280
+ """ELSEIF with ORIF modifier — either condition true enters the branch."""
281
+ result = _run_ast(
282
+ "-- !x! SUB a 1\n"
283
+ "-- !x! SUB b 2\n"
284
+ "CREATE TABLE t (x INT);\n"
285
+ "-- !x! IF (EQUALS(!!a!!, 99))\n"
286
+ "INSERT INTO t VALUES (1);\n"
287
+ "-- !x! ELSEIF (EQUALS(!!a!!, 99))\n"
288
+ "-- !x! ORIF (EQUALS(!!b!!, 2))\n"
289
+ "INSERT INTO t VALUES (2);\n"
290
+ "-- !x! ELSE\n"
291
+ "INSERT INTO t VALUES (3);\n"
292
+ "-- !x! ENDIF",
293
+ tmp_path,
294
+ )
295
+ assert result.returncode == 0
296
+ assert _query_db(tmp_path, "SELECT x FROM t") == [(2,)]
297
+
298
+ def test_elseif_orif_both_false(self, tmp_path):
299
+ """ELSEIF with ORIF where both are false — should fall to ELSE."""
300
+ result = _run_ast(
301
+ "-- !x! SUB a 1\n"
302
+ "CREATE TABLE t (x INT);\n"
303
+ "-- !x! IF (EQUALS(!!a!!, 99))\n"
304
+ "INSERT INTO t VALUES (1);\n"
305
+ "-- !x! ELSEIF (EQUALS(!!a!!, 88))\n"
306
+ "-- !x! ORIF (EQUALS(!!a!!, 77))\n"
307
+ "INSERT INTO t VALUES (2);\n"
308
+ "-- !x! ELSE\n"
309
+ "INSERT INTO t VALUES (3);\n"
310
+ "-- !x! ENDIF",
311
+ tmp_path,
312
+ )
313
+ assert result.returncode == 0
314
+ assert _query_db(tmp_path, "SELECT x FROM t") == [(3,)]
315
+
241
316
  def test_inline_if(self, tmp_path):
242
317
  result = _run_ast(
243
318
  "-- !x! SUB found no\n"
@@ -648,7 +648,7 @@ wheels = [
648
648
 
649
649
  [[package]]
650
650
  name = "execsql2"
651
- version = "2.16.13"
651
+ version = "2.16.14"
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
File without changes