execsql2 2.15.8__tar.gz → 2.15.11__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 (304) hide show
  1. {execsql2-2.15.8 → execsql2-2.15.11}/.gitignore +2 -0
  2. {execsql2-2.15.8 → execsql2-2.15.11}/CHANGELOG.md +42 -0
  3. {execsql2-2.15.8 → execsql2-2.15.11}/PKG-INFO +2 -1
  4. {execsql2-2.15.8 → execsql2-2.15.11}/README.md +1 -0
  5. {execsql2-2.15.8 → execsql2-2.15.11}/docs/about/divergence.md +2 -0
  6. {execsql2-2.15.8 → execsql2-2.15.11}/docs/reference/configuration.md +6 -0
  7. {execsql2-2.15.8 → execsql2-2.15.11}/docs/reference/substitution_vars.md +3 -0
  8. {execsql2-2.15.8 → execsql2-2.15.11}/justfile +45 -10
  9. {execsql2-2.15.8 → execsql2-2.15.11}/pyproject.toml +2 -2
  10. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/__init__.py +4 -3
  11. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/cli/__init__.py +17 -0
  12. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/cli/run.py +4 -1
  13. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/config.py +20 -4
  14. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/base.py +4 -1
  15. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/debug/repl.py +27 -10
  16. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/gui/tui.py +59 -2
  17. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/conditions.py +20 -2
  18. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/gui.py +139 -17
  19. {execsql2-2.15.8 → execsql2-2.15.11}/tests/cli/test_cli.py +51 -0
  20. execsql2-2.15.11/tests/db/test_db_adapters_mocked.py +547 -0
  21. execsql2-2.15.11/tests/db/test_dsn.py +366 -0
  22. {execsql2-2.15.8 → execsql2-2.15.11}/tests/gui/test_backends.py +16 -5
  23. {execsql2-2.15.8 → execsql2-2.15.11}/tests/importers/test_csv_edge_cases.py +97 -1
  24. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_breakpoint.py +7 -0
  25. execsql2-2.15.11/tests/scripts/fixtures/control_flow.sql +173 -0
  26. execsql2-2.15.11/tests/scripts/fixtures/io_roundtrip.sql +135 -0
  27. execsql2-2.15.11/tests/scripts/fixtures/smoke.sql +138 -0
  28. execsql2-2.15.11/tests/scripts/test_sql_scripts.py +67 -0
  29. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_config_data.py +61 -0
  30. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_debug_repl.py +7 -0
  31. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_format.py +183 -0
  32. execsql2-2.15.11/tests/test_parser.py +814 -0
  33. execsql2-2.15.11/tests/utils/__init__.py +0 -0
  34. {execsql2-2.15.8 → execsql2-2.15.11}/uv.lock +1 -1
  35. execsql2-2.15.8/tests/test_parser.py +0 -446
  36. {execsql2-2.15.8 → execsql2-2.15.11}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  37. {execsql2-2.15.8 → execsql2-2.15.11}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  38. {execsql2-2.15.8 → execsql2-2.15.11}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  39. {execsql2-2.15.8 → execsql2-2.15.11}/.github/workflows/ci-cd.yml +0 -0
  40. {execsql2-2.15.8 → execsql2-2.15.11}/.pre-commit-config.yaml +0 -0
  41. {execsql2-2.15.8 → execsql2-2.15.11}/.pre-commit-hooks.yaml +0 -0
  42. {execsql2-2.15.8 → execsql2-2.15.11}/.python-version +0 -0
  43. {execsql2-2.15.8 → execsql2-2.15.11}/.readthedocs.yaml +0 -0
  44. {execsql2-2.15.8 → execsql2-2.15.11}/CONTRIBUTING.md +0 -0
  45. {execsql2-2.15.8 → execsql2-2.15.11}/LICENSE.txt +0 -0
  46. {execsql2-2.15.8 → execsql2-2.15.11}/NOTICE +0 -0
  47. {execsql2-2.15.8 → execsql2-2.15.11}/SECURITY.md +0 -0
  48. {execsql2-2.15.8 → execsql2-2.15.11}/docs/about/contributors.md +0 -0
  49. {execsql2-2.15.8 → execsql2-2.15.11}/docs/about/copyright.md +0 -0
  50. {execsql2-2.15.8 → execsql2-2.15.11}/docs/api/cli.md +0 -0
  51. {execsql2-2.15.8 → execsql2-2.15.11}/docs/api/db.md +0 -0
  52. {execsql2-2.15.8 → execsql2-2.15.11}/docs/api/exporters.md +0 -0
  53. {execsql2-2.15.8 → execsql2-2.15.11}/docs/api/importers.md +0 -0
  54. {execsql2-2.15.8 → execsql2-2.15.11}/docs/api/index.md +0 -0
  55. {execsql2-2.15.8 → execsql2-2.15.11}/docs/api/metacommands.md +0 -0
  56. {execsql2-2.15.8 → execsql2-2.15.11}/docs/dev/adding_db_adapters.md +0 -0
  57. {execsql2-2.15.8 → execsql2-2.15.11}/docs/dev/adding_exporters.md +0 -0
  58. {execsql2-2.15.8 → execsql2-2.15.11}/docs/dev/adding_importers.md +0 -0
  59. {execsql2-2.15.8 → execsql2-2.15.11}/docs/dev/adding_metacommands.md +0 -0
  60. {execsql2-2.15.8 → execsql2-2.15.11}/docs/dev/architecture.md +0 -0
  61. {execsql2-2.15.8 → execsql2-2.15.11}/docs/getting-started/installation.md +0 -0
  62. {execsql2-2.15.8 → execsql2-2.15.11}/docs/getting-started/requirements.md +0 -0
  63. {execsql2-2.15.8 → execsql2-2.15.11}/docs/getting-started/syntax.md +0 -0
  64. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/debugging.md +0 -0
  65. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/documentation.md +0 -0
  66. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/encoding.md +0 -0
  67. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/examples.md +0 -0
  68. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/formatter.md +0 -0
  69. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/logging.md +0 -0
  70. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/sql_syntax.md +0 -0
  71. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/usage.md +0 -0
  72. {execsql2-2.15.8 → execsql2-2.15.11}/docs/guides/using_scripts.md +0 -0
  73. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/Compare_planets.png +0 -0
  74. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/actions.png +0 -0
  75. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/actions2.png +0 -0
  76. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/checkboxes.png +0 -0
  77. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/connect.b64 +0 -0
  78. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/connect.png +0 -0
  79. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/create_conf.png +0 -0
  80. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/data_error1_screenshot.jpg +0 -0
  81. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/entry_form.png +0 -0
  82. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/execsql_console.png +0 -0
  83. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/execsql_logo_01.png +0 -0
  84. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/fatals.png +0 -0
  85. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/logo_small.png +0 -0
  86. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/pause_terminal.png +0 -0
  87. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/pause_terminal_sm.b64 +0 -0
  88. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/pause_terminal_sm.png +0 -0
  89. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/prompt_compare.png +0 -0
  90. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/set_build_commands.jpg +0 -0
  91. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/unit_conversions.b64 +0 -0
  92. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/unit_conversions_029.png +0 -0
  93. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/unmatched.png +0 -0
  94. {execsql2-2.15.8 → execsql2-2.15.11}/docs/images/vim_execsql_highlight.png +0 -0
  95. {execsql2-2.15.8 → execsql2-2.15.11}/docs/index.md +0 -0
  96. {execsql2-2.15.8 → execsql2-2.15.11}/docs/reference/metacommands.md +0 -0
  97. {execsql2-2.15.8 → execsql2-2.15.11}/docs/reference/security.md +0 -0
  98. {execsql2-2.15.8 → execsql2-2.15.11}/extras/vscode-execsql/README.md +0 -0
  99. {execsql2-2.15.8 → execsql2-2.15.11}/extras/vscode-execsql/package.json +0 -0
  100. {execsql2-2.15.8 → execsql2-2.15.11}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  101. {execsql2-2.15.8 → execsql2-2.15.11}/scripts/generate_vscode_grammar.py +0 -0
  102. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/__main__.py +0 -0
  103. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/cli/dsn.py +0 -0
  104. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/cli/help.py +0 -0
  105. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/cli/lint.py +0 -0
  106. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/__init__.py +0 -0
  107. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/access.py +0 -0
  108. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/dsn.py +0 -0
  109. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/duckdb.py +0 -0
  110. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/factory.py +0 -0
  111. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/firebird.py +0 -0
  112. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/mysql.py +0 -0
  113. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/oracle.py +0 -0
  114. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/postgres.py +0 -0
  115. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/sqlite.py +0 -0
  116. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/db/sqlserver.py +0 -0
  117. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/debug/__init__.py +0 -0
  118. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exceptions.py +0 -0
  119. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/__init__.py +0 -0
  120. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/base.py +0 -0
  121. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/delimited.py +0 -0
  122. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/duckdb.py +0 -0
  123. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/feather.py +0 -0
  124. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/html.py +0 -0
  125. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/json.py +0 -0
  126. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/latex.py +0 -0
  127. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/markdown.py +0 -0
  128. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/ods.py +0 -0
  129. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/parquet.py +0 -0
  130. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/pretty.py +0 -0
  131. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/protocol.py +0 -0
  132. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/raw.py +0 -0
  133. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/sqlite.py +0 -0
  134. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/templates.py +0 -0
  135. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/values.py +0 -0
  136. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/xls.py +0 -0
  137. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/xlsx.py +0 -0
  138. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/xml.py +0 -0
  139. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/yaml.py +0 -0
  140. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/exporters/zip.py +0 -0
  141. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/format.py +0 -0
  142. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/gui/__init__.py +0 -0
  143. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/gui/base.py +0 -0
  144. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/gui/console.py +0 -0
  145. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/gui/desktop.py +0 -0
  146. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/importers/__init__.py +0 -0
  147. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/importers/base.py +0 -0
  148. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/importers/csv.py +0 -0
  149. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/importers/feather.py +0 -0
  150. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/importers/json.py +0 -0
  151. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/importers/ods.py +0 -0
  152. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/importers/xls.py +0 -0
  153. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/__init__.py +0 -0
  154. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/connect.py +0 -0
  155. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/control.py +0 -0
  156. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/data.py +0 -0
  157. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/debug.py +0 -0
  158. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/dispatch.py +0 -0
  159. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/io.py +0 -0
  160. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/io_export.py +0 -0
  161. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/io_fileops.py +0 -0
  162. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/io_import.py +0 -0
  163. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/io_write.py +0 -0
  164. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/prompt.py +0 -0
  165. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/script_ext.py +0 -0
  166. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/system.py +0 -0
  167. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/metacommands/upsert.py +0 -0
  168. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/models.py +0 -0
  169. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/parser.py +0 -0
  170. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/py.typed +0 -0
  171. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/script/__init__.py +0 -0
  172. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/script/control.py +0 -0
  173. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/script/engine.py +0 -0
  174. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/script/variables.py +0 -0
  175. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/state.py +0 -0
  176. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/types.py +0 -0
  177. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/__init__.py +0 -0
  178. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/auth.py +0 -0
  179. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/crypto.py +0 -0
  180. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/datetime.py +0 -0
  181. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/errors.py +0 -0
  182. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/fileio.py +0 -0
  183. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/mail.py +0 -0
  184. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/numeric.py +0 -0
  185. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/regex.py +0 -0
  186. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/strings.py +0 -0
  187. {execsql2-2.15.8 → execsql2-2.15.11}/src/execsql/utils/timer.py +0 -0
  188. {execsql2-2.15.8 → execsql2-2.15.11}/templates/README.md +0 -0
  189. {execsql2-2.15.8 → execsql2-2.15.11}/templates/config_settings.sqlite +0 -0
  190. {execsql2-2.15.8 → execsql2-2.15.11}/templates/example_config_prompt.sql +0 -0
  191. {execsql2-2.15.8 → execsql2-2.15.11}/templates/execsql.conf +0 -0
  192. {execsql2-2.15.8 → execsql2-2.15.11}/templates/make_config_db.sql +0 -0
  193. {execsql2-2.15.8 → execsql2-2.15.11}/templates/md_compare.sql +0 -0
  194. {execsql2-2.15.8 → execsql2-2.15.11}/templates/md_glossary.sql +0 -0
  195. {execsql2-2.15.8 → execsql2-2.15.11}/templates/md_upsert.sql +0 -0
  196. {execsql2-2.15.8 → execsql2-2.15.11}/templates/pg_compare.sql +0 -0
  197. {execsql2-2.15.8 → execsql2-2.15.11}/templates/pg_glossary.sql +0 -0
  198. {execsql2-2.15.8 → execsql2-2.15.11}/templates/pg_upsert.sql +0 -0
  199. {execsql2-2.15.8 → execsql2-2.15.11}/templates/script_template.sql +0 -0
  200. {execsql2-2.15.8 → execsql2-2.15.11}/templates/ss_compare.sql +0 -0
  201. {execsql2-2.15.8 → execsql2-2.15.11}/templates/ss_glossary.sql +0 -0
  202. {execsql2-2.15.8 → execsql2-2.15.11}/templates/ss_upsert.sql +0 -0
  203. {execsql2-2.15.8 → execsql2-2.15.11}/tests/__init__.py +0 -0
  204. {execsql2-2.15.8 → execsql2-2.15.11}/tests/cli/__init__.py +0 -0
  205. {execsql2-2.15.8 → execsql2-2.15.11}/tests/cli/test_cli_e2e.py +0 -0
  206. {execsql2-2.15.8 → execsql2-2.15.11}/tests/cli/test_cli_run.py +0 -0
  207. {execsql2-2.15.8 → execsql2-2.15.11}/tests/cli/test_lint.py +0 -0
  208. {execsql2-2.15.8 → execsql2-2.15.11}/tests/cli/test_ping.py +0 -0
  209. {execsql2-2.15.8 → execsql2-2.15.11}/tests/cli/test_profile.py +0 -0
  210. {execsql2-2.15.8 → execsql2-2.15.11}/tests/conftest.py +0 -0
  211. {execsql2-2.15.8 → execsql2-2.15.11}/tests/db/__init__.py +0 -0
  212. {execsql2-2.15.8 → execsql2-2.15.11}/tests/db/test_base.py +0 -0
  213. {execsql2-2.15.8 → execsql2-2.15.11}/tests/db/test_duckdb.py +0 -0
  214. {execsql2-2.15.8 → execsql2-2.15.11}/tests/db/test_factory.py +0 -0
  215. {execsql2-2.15.8 → execsql2-2.15.11}/tests/db/test_postgres.py +0 -0
  216. {execsql2-2.15.8 → execsql2-2.15.11}/tests/db/test_sqlite.py +0 -0
  217. {execsql2-2.15.8 → execsql2-2.15.11}/tests/db/test_sqlite_extra.py +0 -0
  218. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/__init__.py +0 -0
  219. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_base.py +0 -0
  220. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_db.py +0 -0
  221. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_delimited.py +0 -0
  222. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_duckdb_exporter.py +0 -0
  223. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_exporters.py +0 -0
  224. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_feather.py +0 -0
  225. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_html_extended.py +0 -0
  226. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_html_latex.py +0 -0
  227. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_json.py +0 -0
  228. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_json_extended.py +0 -0
  229. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_latex_extended.py +0 -0
  230. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_markdown.py +0 -0
  231. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_ods.py +0 -0
  232. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_parquet.py +0 -0
  233. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_pretty_extended.py +0 -0
  234. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_raw_extended.py +0 -0
  235. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_sqlite_exporter.py +0 -0
  236. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_templates.py +0 -0
  237. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_templates_extended.py +0 -0
  238. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_values_extended.py +0 -0
  239. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_xls_xlsx.py +0 -0
  240. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_xlsx.py +0 -0
  241. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_xml.py +0 -0
  242. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_yaml.py +0 -0
  243. {execsql2-2.15.8 → execsql2-2.15.11}/tests/exporters/test_zip.py +0 -0
  244. {execsql2-2.15.8 → execsql2-2.15.11}/tests/gui/__init__.py +0 -0
  245. {execsql2-2.15.8 → execsql2-2.15.11}/tests/gui/test_compare_stats.py +0 -0
  246. {execsql2-2.15.8 → execsql2-2.15.11}/tests/gui/test_compute_row_diffs.py +0 -0
  247. {execsql2-2.15.8 → execsql2-2.15.11}/tests/importers/__init__.py +0 -0
  248. {execsql2-2.15.8 → execsql2-2.15.11}/tests/importers/test_base_extended.py +0 -0
  249. {execsql2-2.15.8 → execsql2-2.15.11}/tests/importers/test_csv_importer.py +0 -0
  250. {execsql2-2.15.8 → execsql2-2.15.11}/tests/importers/test_feather_importer.py +0 -0
  251. {execsql2-2.15.8 → execsql2-2.15.11}/tests/importers/test_json_importer.py +0 -0
  252. {execsql2-2.15.8 → execsql2-2.15.11}/tests/importers/test_ods_importer.py +0 -0
  253. {execsql2-2.15.8 → execsql2-2.15.11}/tests/importers/test_xls_importer.py +0 -0
  254. {execsql2-2.15.8 → execsql2-2.15.11}/tests/integration/__init__.py +0 -0
  255. {execsql2-2.15.8 → execsql2-2.15.11}/tests/integration/conftest.py +0 -0
  256. {execsql2-2.15.8 → execsql2-2.15.11}/tests/integration/test_dsn.py +0 -0
  257. {execsql2-2.15.8 → execsql2-2.15.11}/tests/integration/test_duckdb.py +0 -0
  258. {execsql2-2.15.8 → execsql2-2.15.11}/tests/integration/test_mysql.py +0 -0
  259. {execsql2-2.15.8 → execsql2-2.15.11}/tests/integration/test_postgres.py +0 -0
  260. {execsql2-2.15.8 → execsql2-2.15.11}/tests/integration/test_sqlite.py +0 -0
  261. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/__init__.py +0 -0
  262. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_assert.py +0 -0
  263. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_connect.py +0 -0
  264. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_io_export.py +0 -0
  265. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_io_import.py +0 -0
  266. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands.py +0 -0
  267. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_connect.py +0 -0
  268. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_data.py +0 -0
  269. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_extended.py +0 -0
  270. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  271. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_io.py +0 -0
  272. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  273. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  274. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_system.py +0 -0
  275. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  276. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_pg_upsert.py +0 -0
  277. {execsql2-2.15.8 → execsql2-2.15.11}/tests/metacommands/test_row_count.py +0 -0
  278. {execsql2-2.15.8/tests/utils → execsql2-2.15.11/tests/scripts}/__init__.py +0 -0
  279. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_config.py +0 -0
  280. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_config_extended.py +0 -0
  281. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_engine.py +0 -0
  282. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_error_messages.py +0 -0
  283. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_exceptions.py +0 -0
  284. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_mail.py +0 -0
  285. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_models.py +0 -0
  286. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_package.py +0 -0
  287. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_registry.py +0 -0
  288. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_script.py +0 -0
  289. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_state.py +0 -0
  290. {execsql2-2.15.8 → execsql2-2.15.11}/tests/test_types.py +0 -0
  291. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_auth.py +0 -0
  292. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_auth_extra.py +0 -0
  293. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_crypto.py +0 -0
  294. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_datetime.py +0 -0
  295. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_errors.py +0 -0
  296. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_errors_extra.py +0 -0
  297. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_fileio.py +0 -0
  298. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_fileio_extra.py +0 -0
  299. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_numeric.py +0 -0
  300. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_regex.py +0 -0
  301. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_strings.py +0 -0
  302. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_timer.py +0 -0
  303. {execsql2-2.15.8 → execsql2-2.15.11}/tests/utils/test_timer_extra.py +0 -0
  304. {execsql2-2.15.8 → execsql2-2.15.11}/zensical.toml +0 -0
@@ -35,6 +35,7 @@ htmlcov/
35
35
  execsql.log
36
36
  !scripts/generate_vscode_grammar.py
37
37
  scripts/
38
+ !tests/scripts
38
39
 
39
40
  # Distribution
40
41
  *.whl
@@ -47,3 +48,4 @@ _execsql/
47
48
  .claude/
48
49
  ANALYSIS.md
49
50
  FINDINGS.md
51
+ CLAUDE.md
@@ -13,6 +13,48 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.15.11] - 2026-04-27
17
+
18
+ ### Fixed
19
+
20
+ - `PAUSE` console-mode fallback now checks `sys.platform` before attempting POSIX terminal imports, preventing hangs on Windows when stdin reports as a TTY.
21
+
22
+ ______________________________________________________________________
23
+
24
+ ## [2.15.10] - 2026-04-27
25
+
26
+ ### Added
27
+
28
+ - `--config FILE` CLI flag to specify an explicit configuration file. The file is loaded after the implicit search paths (system, user, script-dir, working-dir) so its values take precedence, while CLI arguments still override everything.
29
+ - `$HOSTNAME` system substitution variable — the network name of the machine running execsql, useful for log messages and environment detection.
30
+
31
+ ### Fixed
32
+
33
+ - Config file chaining no longer mutates a list during iteration; uses a deque for safe, predictable processing order.
34
+ - REPL `_use_color()` result is now cached instead of re-checking environment variables and TTY status on every colorized output.
35
+ - `DatabasePool.closeall()` no longer calls `self.__init__()` to reset state; fields are reset directly to avoid the re-initialization anti-pattern.
36
+ - `PAUSE` console mode no longer crashes on Windows CI due to unconditional `import termios`; POSIX-only imports are now guarded by the TTY fallback check.
37
+ - `HAS_ROWS()`, `ROW_COUNT_GT()`, `ROW_COUNT_GTE()`, `ROW_COUNT_EQ()`, and `ROW_COUNT_LT()` condition predicates now quote table names with standard SQL identifier quoting, preventing potential SQL injection when table names originate from substitution variables.
38
+ - Corrected `__init__.py` module docstring that incorrectly described the CLI entry point as `execsql2` (the command is `execsql`).
39
+ - Added note to configuration reference clarifying that `--output-dir` is a CLI-only option with no equivalent configuration file setting.
40
+
41
+ ______________________________________________________________________
42
+
43
+ ## [2.15.9] - 2026-04-27
44
+
45
+ ### Added
46
+
47
+ - Textual TUI now displays a progress bar and remaining-time countdown for `PROMPT PAUSE` and `PAUSE` dialogs when the `CONTINUE AFTER` or `HALT AFTER` keywords specify a timed duration (matching existing Tkinter behavior).
48
+
49
+ ### Fixed
50
+
51
+ - `PAUSE` metacommand in console mode (no `-v`) now responds to single keypresses (Enter to continue, Esc to quit) instead of requiring Enter after every key. Uses raw-mode terminal reading on POSIX and `msvcrt` polling on Windows.
52
+ - `PAUSE` with `CONTINUE AFTER`/`HALT AFTER` in console mode now displays a live SIGALRM-driven progress bar showing time remaining, matching the documented behavior and terminal screenshot.
53
+ - `PAUSE` progress bar output no longer bleeds into subsequent script output — the progress line is cleared before returning.
54
+ - Fixed double minutes-to-seconds conversion in the console `PAUSE` path that caused a 1-minute pause to sleep for 60 minutes.
55
+
56
+ ______________________________________________________________________
57
+
16
58
  ## [2.15.8] - 2026-04-20
17
59
 
18
60
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.15.8
3
+ Version: 2.15.11
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
@@ -240,6 +240,7 @@ execsql script.sql # read connection from config file
240
240
  | `--ping` | Test database connectivity and exit |
241
241
  | `--profile` | Show per-statement timing summary after execution |
242
242
  | `--progress` | Show a progress bar for long-running IMPORT operations |
243
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
243
244
  | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
244
245
  | `--dump-keywords` | Print metacommand keywords as JSON and exit |
245
246
  | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
@@ -118,6 +118,7 @@ execsql script.sql # read connection from config file
118
118
  | `--ping` | Test database connectivity and exit |
119
119
  | `--profile` | Show per-statement timing summary after execution |
120
120
  | `--progress` | Show a progress bar for long-running IMPORT operations |
121
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
121
122
  | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
122
123
  | `--dump-keywords` | Print metacommand keywords as JSON and exit |
123
124
  | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
@@ -21,6 +21,7 @@ ______________________________________________________________________
21
21
  | `--output-dir` | Set a default base directory for export output files. |
22
22
  | `--progress` | Show a Rich progress bar during long-running IMPORT operations. |
23
23
  | `--dump-keywords` | Emit all metacommand keywords, conditionals, config options, and export formats as structured JSON. |
24
+ | `--config FILE` | Load an explicit configuration file after the implicit search paths. Its values take precedence over system, user, script-dir, and working-dir config files; CLI arguments still override everything. The file may chain additional configs via its `[config]` section. |
24
25
  | `--gui-framework` | Select GUI backend: `tkinter` (default) or `textual` (terminal UI). |
25
26
  | `--debug` | Start in step-through debug mode. The debug REPL pauses before each statement, as if `BREAKPOINT` were at the top with `.next` always active. |
26
27
  | `--dry-run` | Parse the script and print the full command list without connecting to a database or executing anything. Substitution variables already populated at parse time (env vars, `--assign-arg` values, built-in start-time vars) are expanded in the output; execution-time variables (`$DB_NAME`, `$CURRENT_TIME`, etc.) remain unexpanded. |
@@ -175,6 +176,7 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
175
176
 
176
177
  ### Substitution Variables
177
178
 
179
+ - **`$HOSTNAME`** — Network name of the machine running execsql (`platform.node()`). Useful for log messages and environment detection.
178
180
  - **`$SYSTEM_CMD_PID`** — New system variable set to the PID of the background process when `SHELL … CONTINUE` is used.
179
181
  - **Cycle detection** — `substitute_vars()` raises an error after 100 iterations to prevent infinite loops when variables reference each other cyclically. Upstream had no protection.
180
182
  - **O(1) substitution** — Variable substitution uses a single combined regex and dict lookup instead of O(V) per-variable regex passes. Behavior is identical; performance is improved.
@@ -13,6 +13,8 @@ The name of the configuration file, in all locations, is `execsql.conf`.
13
13
 
14
14
  Configuration data is read from these files in the order listed above. Information in later files may augment or replace information in earlier files. Options and arguments specified on the command line will further augment or override information specified in the configuration files.
15
15
 
16
+ An explicit configuration file can also be specified with the `--config FILE` command-line option. This file is loaded **after** all four implicit search paths, so its values take precedence over system, user, script-directory, and working-directory config files. CLI arguments still override everything. The `--config` file may chain additional configs via its `[config]` section, just like any other config file.
17
+
16
18
  In addition, *execsql* will read additional configuration files if they are specified in any of the standard configuration files ([see below](#config_config)).
17
19
 
18
20
  Configuration files use the [INI](https://en.wikipedia.org/wiki/INI_file) file format. Section names are case sensitive and must be all in lowercase. Property names are not case sensitive. Property values are read as-is and may or may not be case sensitive, depending on their use. Comments can be included in configuration files; each comment line must start with the "#" character.
@@ -200,6 +202,10 @@ The section and property names that may be used in a configuration file are list
200
202
  `zip_buffer_mb` { #zip_buffer_mb }
201
203
  : The size of the internal buffer used when the [EXPORT](metacommands.md#export) metacommand exports data to a zipfile, in Mb. The default value is 10. The buffer should be at least as large as the largest data row to be exported. This value typically has little effect on performance, and only affects memory usage.
202
204
 
205
+ !!! note "CLI-only output options"
206
+
207
+ The [`--output-dir`](../getting-started/syntax.md) option sets a default base directory for EXPORT output files. Relative paths in EXPORT metacommands are automatically joined to this directory; absolute paths and `stdout` are unaffected. This option is **only available on the command line** — there is no equivalent configuration file setting.
208
+
203
209
  ## Section `interface`
204
210
 
205
211
  `console_height`
@@ -138,6 +138,9 @@ $ERROR_HALT_STATE
138
138
  $ERROR_MESSAGE
139
139
  : The message generated by any error, as it would be printed on the terminal by default. This is initially an empty string, and is set by any SQL error or metacommand error. If an error occurs, the error message is only accessible if the [ERROR_HALT OFF](metacommands.md#error_halt) or [METACOMMAND_ERROR_HALT OFF](metacommands.md#metacommanderrorhalt) metacommand has been used, or in an [ON ERROR_HALT EMAIL](metacommands.md#error_halt_email), [ON ERROR_HALT WRITE](metacommands.md#error_halt_write), or [ON ERROR_HALT EXECUTE SCRIPT](metacommands.md#error_halt_exec) metacommand.
140
140
 
141
+ $HOSTNAME
142
+ : The network name of the machine running execsql, as returned by Python's `platform.node()`. Useful for log messages, environment detection, and multi-host deployment scripts.
143
+
141
144
  $LAST_ERROR
142
145
  : The text of the last SQL statement or metacommand that caused an error. This value will only be available if the [ERROR_HALT OFF](metacommands.md#error_halt) or [METACOMMAND_ERROR_HALT OFF](metacommands.md#metacommanderrorhalt) metacommand has been used, or in an [ON ERROR_HALT EMAIL](metacommands.md#error_halt_email), [ON ERROR_HALT WRITE](metacommands.md#error_halt_write), or [ON ERROR_HALT EXECUTE SCRIPT](metacommands.md#error_halt_exec) metacommand.
143
146
 
@@ -1,42 +1,65 @@
1
1
  # Install: brew install just | cargo install just | uv tool install rust-just
2
2
  # Usage: just <recipe>
3
3
 
4
+ set quiet
5
+ set unstable
4
6
 
5
- # List available recipes
7
+
8
+ [private]
6
9
  default:
7
- @just --list
10
+ @just --list --unsorted
8
11
 
9
12
  # ── Dependencies ──────────────────────────────────────────────────────────────
10
13
 
11
14
  # Sync dependencies from lockfile
15
+ [group('deps')]
12
16
  sync:
13
17
  uv sync --all-extras
14
18
 
15
19
  # Update pre-commit hooks
20
+ [group('deps')]
16
21
  update-hooks:
17
22
  uv run pre-commit autoupdate
18
23
 
19
24
 
20
25
  # ── Code Quality ──────────────────────────────────────────────────────────────
21
26
 
22
- # Run linter + formatter
27
+ # Lint, spell-check, and test
28
+ [group('quality')]
29
+ check: format lint test
30
+
31
+ # Run linter
32
+ [group('quality')]
23
33
  lint:
24
34
  uv run ruff check .
35
+
36
+ # Run formatter
37
+ [group('quality')]
38
+ format:
25
39
  uv run ruff format .
26
40
 
27
41
  # Run pre-commit hooks on all files
42
+ [group('quality')]
28
43
  pre-commit:
29
44
  uv run pre-commit run --all-files
30
45
 
31
46
  # Run tests
32
- test:
33
- uv run tox -e py
47
+ [group('quality')]
48
+ test *ARGS:
49
+ uv run tox -e py -- {{ ARGS }}
34
50
 
35
51
  # Run tests across all supported Python versions
52
+ [group('quality')]
36
53
  test-all:
37
54
  uv run tox run-parallel
38
55
 
56
+ # Run tests with coverage report printed to terminal
57
+ [group('quality')]
58
+ coverage:
59
+ uv run pytest --cov-report=term-missing
60
+
39
61
  # Clean up Python build artifacts and caches
62
+ [group('quality')]
40
63
  clean:
41
64
  find . -type d -name "__pycache__" -exec rm -rf {} +
42
65
  find . -type d -name ".pytest_cache" -exec rm -rf {} +
@@ -48,11 +71,14 @@ clean:
48
71
  find . -type f -name "*.pyc" -exec rm -f {} +
49
72
  find . -type f -name "*.pyo" -exec rm -f {} +
50
73
  find . -type f -name ".coverage" -exec rm -rf {} +
74
+ find . -type f -name "coverage.xml" -exec rm -rf {} +
75
+ find . -type f -name "execsql.log" -exec rm -f {} +
51
76
 
52
77
 
53
78
  # ── VS Code Extension ────────────────────────────────────────────────────────
54
79
 
55
- # Install VS Code extension via symlink
80
+ # Regenerate grammar and install VS Code extension via symlink
81
+ [group('vscode')]
56
82
  install-vscode:
57
83
  uv run python scripts/generate_vscode_grammar.py
58
84
  ln -sfn "$(pwd)/extras/vscode-execsql" ~/.vscode/extensions/execsql-syntax
@@ -61,31 +87,40 @@ install-vscode:
61
87
 
62
88
  # ── Documentation──────────────────────────────────────────────────────────────
63
89
 
64
- # Build documentation
65
- docs:
90
+ # Copy CHANGELOG into docs source tree
91
+ [private]
92
+ _sync-changelog:
66
93
  cp CHANGELOG.md docs/about/change_log.md
94
+
95
+ # Build documentation
96
+ [group('docs')]
97
+ docs: _sync-changelog
67
98
  uv run zensical build
68
99
 
69
100
  # Serve documentation locally
70
- docs-serve:
71
- cp CHANGELOG.md docs/about/change_log.md
101
+ [group('docs')]
102
+ docs-serve: _sync-changelog
72
103
  uv run zensical serve
73
104
 
74
105
 
75
106
  # ── Versioning ────────────────────────────────────────────────────────────────
76
107
 
77
108
  # List the current version
109
+ [group('version')]
78
110
  bump:
79
111
  uv run bump-my-version show-bump
80
112
 
81
113
  # Bump patch version (e.g. 1.2.3 → 1.2.4)
114
+ [group('version')]
82
115
  bump-patch:
83
116
  uv run bump-my-version bump patch
84
117
 
85
118
  # Bump minor version (e.g. 1.2.3 → 1.3.0)
119
+ [group('version')]
86
120
  bump-minor:
87
121
  uv run bump-my-version bump minor
88
122
 
89
123
  # Bump major version (e.g. 1.2.3 → 2.0.0)
124
+ [group('version')]
90
125
  bump-major:
91
126
  uv run bump-my-version bump major
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.15.8"
7
+ version = "2.15.11"
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" }
@@ -164,7 +164,7 @@ skip-magic-trailing-comma = false
164
164
  line-ending = "auto"
165
165
 
166
166
  [tool.bumpversion]
167
- current_version = "2.15.8"
167
+ current_version = "2.15.11"
168
168
  commit = true
169
169
  commit_args = "--no-verify"
170
170
  tag = true
@@ -1,9 +1,10 @@
1
1
  """
2
2
  execsql — a maintained fork of the execsql SQL scripting tool.
3
3
 
4
- This package provides the ``execsql2`` CLI (entry point ``execsql2``) and the
5
- ``execsql`` importable module. The top-level package exposes only the package
6
- version; all public functionality lives in sub-modules.
4
+ This package provides the ``execsql`` CLI command (distributed as the
5
+ ``execsql2`` package on PyPI) and the ``execsql`` importable module.
6
+ The top-level package exposes only the package version; all public
7
+ functionality lives in sub-modules.
7
8
  """
8
9
 
9
10
  from __future__ import annotations
@@ -283,6 +283,16 @@ def main(
283
283
  "--profile-limit",
284
284
  help="Number of top statements to show in the --profile timing summary (default: 20).",
285
285
  ),
286
+ config_file: str | None = typer.Option(
287
+ None,
288
+ "--config",
289
+ metavar="FILE",
290
+ help=(
291
+ "Path to an execsql configuration file. "
292
+ "Loaded after the implicit search paths so its values take precedence. "
293
+ "The file may chain additional configs via its [cyan][config][/cyan] section."
294
+ ),
295
+ ),
286
296
  debug: bool = typer.Option(
287
297
  False,
288
298
  "--debug",
@@ -376,6 +386,12 @@ def main(
376
386
  webbrowser.open("https://execsql2.readthedocs.io/en/latest/", new=2, autoraise=True)
377
387
  raise typer.Exit()
378
388
 
389
+ if config_file and not Path(config_file).is_file():
390
+ _err_console.print(
391
+ f"[bold red]Error:[/bold red] Config file [cyan]{config_file!r}[/cyan] does not exist.",
392
+ )
393
+ raise typer.Exit(code=2)
394
+
379
395
  positional = args or []
380
396
  if command is not None:
381
397
  script_name = None # inline mode — no script file
@@ -458,6 +474,7 @@ def main(
458
474
  ping=ping,
459
475
  lint=lint,
460
476
  debug=debug,
477
+ config_file=config_file,
461
478
  )
462
479
 
463
480
 
@@ -12,6 +12,7 @@ from typing import Any
12
12
  import datetime
13
13
  import getpass
14
14
  import os
15
+ import platform
15
16
  import sys
16
17
  import traceback
17
18
  from pathlib import Path
@@ -219,6 +220,7 @@ def _run(
219
220
  ping: bool = False,
220
221
  lint: bool = False,
221
222
  debug: bool = False,
223
+ config_file: str | None = None,
222
224
  ) -> None:
223
225
  """Initialise state, connect to the database, load the script, and run it.
224
226
 
@@ -276,13 +278,14 @@ def _run(
276
278
  elif osys.startswith("win"):
277
279
  osys = "windows"
278
280
  _state.subvars.add_substitution("$OS", osys)
281
+ _state.subvars.add_substitution("$HOSTNAME", platform.node())
279
282
  _state.subvars.add_substitution("$PYTHON_EXECUTABLE", sys.executable)
280
283
 
281
284
  # ------------------------------------------------------------------
282
285
  # Read configuration file
283
286
  # ------------------------------------------------------------------
284
287
  script_path = str(Path(script_name).resolve().parent) if script_name else os.getcwd()
285
- _state.conf = ConfigData(script_path, _state.subvars)
288
+ _state.conf = ConfigData(script_path, _state.subvars, config_file=config_file)
286
289
  conf = _state.conf
287
290
 
288
291
  # ------------------------------------------------------------------
@@ -198,7 +198,13 @@ class ConfigData:
198
198
  raise ConfigError(f"Invalid {key}: {val}; must be >= {min_val}.")
199
199
  setattr(self, attr, val)
200
200
 
201
- def __init__(self, script_path: str, variable_pool: object) -> None:
201
+ def __init__(
202
+ self,
203
+ script_path: str,
204
+ variable_pool: object,
205
+ *,
206
+ config_file: str | None = None,
207
+ ) -> None:
202
208
  """Load and merge all discoverable execsql.conf files for the given script path.
203
209
 
204
210
  Args:
@@ -207,6 +213,10 @@ class ConfigData:
207
213
  variable_pool: Substitution-variable registry used to expand
208
214
  ``config_file`` path values and to populate ``[variables]``
209
215
  sections.
216
+ config_file: Optional explicit config file path (from ``--config``).
217
+ Loaded after the implicit search paths so its values take
218
+ precedence over system, user, script, and working-directory
219
+ config files.
210
220
  """
211
221
  self.db_type = "a"
212
222
  self.server = None
@@ -290,9 +300,15 @@ class ConfigData:
290
300
  config_files = [sys_config_file, user_config_file, script_config_file, startdir_config_file]
291
301
  else:
292
302
  config_files = [sys_config_file, user_config_file, startdir_config_file]
303
+ if config_file:
304
+ config_files.append(str(Path(config_file).resolve()))
305
+ from collections import deque
306
+
293
307
  _MAX_CONFIG_CHAIN = 20 # Guard against circular config_file references.
308
+ config_queue: deque[str] = deque(config_files)
294
309
  self.files_read: list = []
295
- for ix, configfile in enumerate(config_files):
310
+ while config_queue:
311
+ configfile = config_queue.popleft()
296
312
  if len(self.files_read) >= _MAX_CONFIG_CHAIN:
297
313
  break
298
314
  if configfile not in self.files_read and Path(configfile).is_file():
@@ -425,7 +441,7 @@ class ConfigData:
425
441
  conffile = str(Path(conffile) / self.config_file_name)
426
442
  if Path(conffile).is_file():
427
443
  # Silently ignore a non-existent file, for cross-OS compatibility.
428
- config_files.insert(ix + 1, conffile)
444
+ config_queue.appendleft(conffile)
429
445
  # OS-specific additional config files.
430
446
  _os_config_key: str | None = None
431
447
  if sys.platform == "linux" and cp.has_option(self._CONFIG_SECTION, "linux_config_file"):
@@ -445,7 +461,7 @@ class ConfigData:
445
461
  if not Path(conffile).is_file():
446
462
  conffile = str(Path(conffile) / self.config_file_name)
447
463
  if Path(conffile).is_file():
448
- config_files.insert(ix + 1, conffile)
464
+ config_queue.appendleft(conffile)
449
465
  self._get_bool(cp, self._CONFIG_SECTION, "user_logfile", "user_logfile")
450
466
  # dao_flush_delay_secs has a specific error message — keep inline
451
467
  if cp.has_option(self._CONFIG_SECTION, "dao_flush_delay_secs"):
@@ -747,4 +747,7 @@ class DatabasePool:
747
747
  _state.exec_log.log_status_error(
748
748
  f"Can't close database {nm} aliased as {alias}",
749
749
  )
750
- self.__init__()
750
+ self.pool = {}
751
+ self.initial_db = None
752
+ self.current_db = None
753
+ self.do_rollback = True
@@ -45,23 +45,39 @@ _YELLOW = "\033[33m"
45
45
  _CYAN = "\033[36m"
46
46
 
47
47
 
48
+ _color_cache: bool | None = None
49
+
50
+
48
51
  def _use_color() -> bool:
49
52
  """Return True if the output stream supports ANSI color.
50
53
 
51
54
  Checks ``NO_COLOR`` and ``EXECSQL_NO_COLOR`` environment variables first
52
55
  (either set → color off). Then tests whether the active output stream
53
56
  reports itself as a TTY.
57
+
58
+ The result is cached after the first call; call ``_reset_color_cache()``
59
+ to force re-evaluation (e.g. when entering the REPL).
54
60
  """
55
- if os.environ.get("NO_COLOR") is not None:
56
- return False
57
- if os.environ.get("EXECSQL_NO_COLOR") is not None:
58
- return False
59
- output = _state.output
60
- if output is not None and hasattr(output, "isatty"):
61
- return output.isatty()
62
- # WriteHooks (the default _state.output) has no isatty — fall through
63
- # to check the underlying stream it would write to.
64
- return sys.stdout.isatty()
61
+ global _color_cache # noqa: PLW0603
62
+ if _color_cache is not None:
63
+ return _color_cache
64
+ if os.environ.get("NO_COLOR") is not None or os.environ.get("EXECSQL_NO_COLOR") is not None:
65
+ _color_cache = False
66
+ else:
67
+ output = _state.output
68
+ if output is not None and hasattr(output, "isatty"):
69
+ _color_cache = output.isatty()
70
+ else:
71
+ # WriteHooks (the default _state.output) has no isatty — fall through
72
+ # to check the underlying stream it would write to.
73
+ _color_cache = sys.stdout.isatty()
74
+ return _color_cache
75
+
76
+
77
+ def _reset_color_cache() -> None:
78
+ """Clear the cached color decision so it is re-evaluated on next use."""
79
+ global _color_cache # noqa: PLW0603
80
+ _color_cache = None
65
81
 
66
82
 
67
83
  def _c(code: str, text: str) -> str:
@@ -169,6 +185,7 @@ def _debug_repl(*, step: bool = False) -> None:
169
185
  step: When ``True``, the entry banner says "Step" instead of
170
186
  "Breakpoint" to indicate the REPL was re-entered via step mode.
171
187
  """
188
+ _reset_color_cache()
172
189
  try:
173
190
  import readline as _readline # noqa: F401 — side-effect: enables history/arrow keys
174
191
  except ImportError:
@@ -244,7 +244,31 @@ class MsgScreen(_BaseDialog):
244
244
 
245
245
 
246
246
  class PauseScreen(_BaseDialog):
247
- """Pause dialog with optional countdown and Continue/Cancel buttons."""
247
+ """Pause dialog with optional countdown progress bar and Continue/Cancel buttons."""
248
+
249
+ DEFAULT_CSS = (
250
+ _BaseDialog.DEFAULT_CSS
251
+ + """
252
+ #countdown-container {
253
+ height: auto;
254
+ margin: 1 0;
255
+ }
256
+ #countdown-container ProgressBar {
257
+ width: 1fr;
258
+ }
259
+ #countdown-container Bar {
260
+ width: 1fr;
261
+ &> .bar--bar {
262
+ background: $primary 30%;
263
+ }
264
+ }
265
+ #countdown-container .countdown-label {
266
+ text-align: center;
267
+ color: $text-muted;
268
+ width: 1fr;
269
+ }
270
+ """
271
+ )
248
272
 
249
273
  BINDINGS = [
250
274
  *_BaseDialog.BINDINGS,
@@ -254,17 +278,50 @@ class PauseScreen(_BaseDialog):
254
278
  def compose(self) -> ComposeResult:
255
279
  title = self.args.get("title", "Pause")
256
280
  message = self.args.get("message", "")
281
+ countdown = self.args.get("countdown")
257
282
  with Container(id="dialog"):
258
283
  yield Label(title, id="title")
259
284
  yield Static(message, id="message")
285
+ if countdown is not None:
286
+ with Vertical(id="countdown-container"):
287
+ yield ProgressBar(
288
+ total=float(countdown),
289
+ show_percentage=False,
290
+ show_eta=False,
291
+ id="countdown-bar",
292
+ )
293
+ yield Static("", classes="countdown-label")
260
294
  with Horizontal(id="buttons"):
261
295
  yield Button("Cancel", id="btn_cancel_exit", variant="warning")
262
296
  yield Button("Continue", id="btn_continue", variant="primary")
263
297
 
264
298
  def on_mount(self) -> None:
299
+ import time
300
+
265
301
  countdown = self.args.get("countdown")
266
302
  if countdown is not None:
267
- self.set_timer(float(countdown), self._auto_continue)
303
+ self._countdown_total = float(countdown)
304
+ self._countdown_start = time.time()
305
+ self._tick_interval = self.set_interval(0.2, self._tick)
306
+
307
+ def _tick(self) -> None:
308
+ import time
309
+
310
+ elapsed = time.time() - self._countdown_start
311
+ remaining = max(0.0, self._countdown_total - elapsed)
312
+ progress = min(self._countdown_total, elapsed)
313
+ bar = self.query_one("#countdown-bar", ProgressBar)
314
+ bar.update(progress=progress)
315
+ label = self.query_one(".countdown-label", Static)
316
+ if remaining >= 60:
317
+ mins = int(remaining) // 60
318
+ secs = int(remaining) % 60
319
+ label.update(f"{mins}m {secs:02d}s remaining")
320
+ else:
321
+ label.update(f"{remaining:.0f}s remaining")
322
+ if remaining <= 0:
323
+ self._tick_interval.stop()
324
+ self._auto_continue()
268
325
 
269
326
  def _auto_continue(self) -> None:
270
327
  self._result = {"quit": False}
@@ -30,6 +30,24 @@ from execsql.utils.gui import gui_console_isrunning
30
30
  from execsql.utils.strings import unquoted
31
31
 
32
32
 
33
+ def _quote_table_name(name: str) -> str:
34
+ """Quote a potentially schema-qualified table name for safe SQL interpolation.
35
+
36
+ Splits on ``.`` and quotes each component with standard SQL double-quoting
37
+ (embedded double-quotes are escaped to ``""``).
38
+
39
+ Examples::
40
+
41
+ >>> _quote_table_name("books")
42
+ '"books"'
43
+ >>> _quote_table_name("staging.books")
44
+ '"staging"."books"'
45
+ >>> _quote_table_name('my"table')
46
+ '"my""table"'
47
+ """
48
+ return ".".join('"' + part.replace('"', '""') + '"' for part in name.split("."))
49
+
50
+
33
51
  def xf_contains(**kwargs: Any) -> bool:
34
52
  s1 = kwargs["string1"]
35
53
  s2 = kwargs["string2"]
@@ -59,7 +77,7 @@ def xf_endswith(**kwargs: Any) -> bool:
59
77
 
60
78
  def xf_hasrows(**kwargs: Any) -> bool:
61
79
  queryname = kwargs["queryname"]
62
- sql = f"select count(*) from {queryname};"
80
+ sql = f"select count(*) from {_quote_table_name(queryname)};"
63
81
  try:
64
82
  hdrs, rec = _state.dbs.current().select_data(sql)
65
83
  except ErrInfo:
@@ -84,7 +102,7 @@ def _row_count(queryname: str, sql_context: str, metacommandline: str) -> int:
84
102
  Raises:
85
103
  ErrInfo: If the query fails or the result is not numeric.
86
104
  """
87
- sql = f"select count(*) from {queryname};"
105
+ sql = f"select count(*) from {_quote_table_name(queryname)};"
88
106
  try:
89
107
  _hdrs, rec = _state.dbs.current().select_data(sql)
90
108
  except ErrInfo: