execsql2 2.13.2__tar.gz → 2.14.1__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 (312) hide show
  1. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/project_context.md +18 -2
  2. {execsql2-2.13.2 → execsql2-2.14.1}/CHANGELOG.md +40 -0
  3. {execsql2-2.13.2 → execsql2-2.14.1}/PKG-INFO +1 -1
  4. {execsql2-2.13.2 → execsql2-2.14.1}/docs/about/divergence.md +13 -8
  5. {execsql2-2.13.2 → execsql2-2.14.1}/docs/reference/metacommands.md +27 -10
  6. {execsql2-2.13.2 → execsql2-2.14.1}/pyproject.toml +2 -2
  7. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/gui/base.py +52 -1
  8. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/gui/console.py +86 -9
  9. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/gui/desktop.py +261 -39
  10. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/gui/tui.py +325 -51
  11. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/connect.py +5 -1
  12. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/dispatch.py +49 -6
  13. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/io_export.py +2 -2
  14. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/prompt.py +6 -11
  15. execsql2-2.14.1/tests/exporters/test_html_extended.py +295 -0
  16. execsql2-2.14.1/tests/exporters/test_json_extended.py +168 -0
  17. execsql2-2.14.1/tests/exporters/test_latex_extended.py +173 -0
  18. execsql2-2.14.1/tests/exporters/test_pretty_extended.py +100 -0
  19. execsql2-2.14.1/tests/exporters/test_raw_extended.py +71 -0
  20. execsql2-2.14.1/tests/exporters/test_templates_extended.py +181 -0
  21. execsql2-2.14.1/tests/exporters/test_values_extended.py +91 -0
  22. {execsql2-2.13.2 → execsql2-2.14.1}/tests/gui/test_backends.py +50 -12
  23. execsql2-2.14.1/tests/gui/test_compare_stats.py +396 -0
  24. execsql2-2.14.1/tests/importers/test_base_extended.py +231 -0
  25. execsql2-2.14.1/tests/test_config_extended.py +981 -0
  26. {execsql2-2.13.2 → execsql2-2.14.1}/uv.lock +1 -1
  27. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/agents/dba.md +0 -0
  28. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/agents/herald.md +0 -0
  29. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/agents/inspector.md +0 -0
  30. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/agents/liaison.md +0 -0
  31. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/agents/oracle.md +0 -0
  32. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/agents/patcher.md +0 -0
  33. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/agents/qa.md +0 -0
  34. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/agents/scribe.md +0 -0
  35. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/commands/code-oracle.md +0 -0
  36. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/commands/migrate.md +0 -0
  37. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/commands/review-changes.md +0 -0
  38. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/commands/test-module.md +0 -0
  39. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/commands/update-changelog.md +0 -0
  40. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/commands/where-is.md +0 -0
  41. {execsql2-2.13.2 → execsql2-2.14.1}/.claude/state/status.md +0 -0
  42. {execsql2-2.13.2 → execsql2-2.14.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  43. {execsql2-2.13.2 → execsql2-2.14.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  44. {execsql2-2.13.2 → execsql2-2.14.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  45. {execsql2-2.13.2 → execsql2-2.14.1}/.github/workflows/ci-cd.yml +0 -0
  46. {execsql2-2.13.2 → execsql2-2.14.1}/.gitignore +0 -0
  47. {execsql2-2.13.2 → execsql2-2.14.1}/.pre-commit-config.yaml +0 -0
  48. {execsql2-2.13.2 → execsql2-2.14.1}/.pre-commit-hooks.yaml +0 -0
  49. {execsql2-2.13.2 → execsql2-2.14.1}/.python-version +0 -0
  50. {execsql2-2.13.2 → execsql2-2.14.1}/.readthedocs.yaml +0 -0
  51. {execsql2-2.13.2 → execsql2-2.14.1}/CLAUDE.md +0 -0
  52. {execsql2-2.13.2 → execsql2-2.14.1}/CONTRIBUTING.md +0 -0
  53. {execsql2-2.13.2 → execsql2-2.14.1}/LICENSE.txt +0 -0
  54. {execsql2-2.13.2 → execsql2-2.14.1}/NOTICE +0 -0
  55. {execsql2-2.13.2 → execsql2-2.14.1}/README.md +0 -0
  56. {execsql2-2.13.2 → execsql2-2.14.1}/SECURITY.md +0 -0
  57. {execsql2-2.13.2 → execsql2-2.14.1}/docs/about/contributors.md +0 -0
  58. {execsql2-2.13.2 → execsql2-2.14.1}/docs/about/copyright.md +0 -0
  59. {execsql2-2.13.2 → execsql2-2.14.1}/docs/api/cli.md +0 -0
  60. {execsql2-2.13.2 → execsql2-2.14.1}/docs/api/db.md +0 -0
  61. {execsql2-2.13.2 → execsql2-2.14.1}/docs/api/exporters.md +0 -0
  62. {execsql2-2.13.2 → execsql2-2.14.1}/docs/api/importers.md +0 -0
  63. {execsql2-2.13.2 → execsql2-2.14.1}/docs/api/index.md +0 -0
  64. {execsql2-2.13.2 → execsql2-2.14.1}/docs/api/metacommands.md +0 -0
  65. {execsql2-2.13.2 → execsql2-2.14.1}/docs/dev/adding_db_adapters.md +0 -0
  66. {execsql2-2.13.2 → execsql2-2.14.1}/docs/dev/adding_exporters.md +0 -0
  67. {execsql2-2.13.2 → execsql2-2.14.1}/docs/dev/adding_importers.md +0 -0
  68. {execsql2-2.13.2 → execsql2-2.14.1}/docs/dev/adding_metacommands.md +0 -0
  69. {execsql2-2.13.2 → execsql2-2.14.1}/docs/dev/architecture.md +0 -0
  70. {execsql2-2.13.2 → execsql2-2.14.1}/docs/getting-started/installation.md +0 -0
  71. {execsql2-2.13.2 → execsql2-2.14.1}/docs/getting-started/requirements.md +0 -0
  72. {execsql2-2.13.2 → execsql2-2.14.1}/docs/getting-started/syntax.md +0 -0
  73. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/debugging.md +0 -0
  74. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/documentation.md +0 -0
  75. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/encoding.md +0 -0
  76. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/examples.md +0 -0
  77. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/formatter.md +0 -0
  78. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/logging.md +0 -0
  79. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/sql_syntax.md +0 -0
  80. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/usage.md +0 -0
  81. {execsql2-2.13.2 → execsql2-2.14.1}/docs/guides/using_scripts.md +0 -0
  82. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/Compare_planets.png +0 -0
  83. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/actions.png +0 -0
  84. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/actions2.png +0 -0
  85. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/checkboxes.png +0 -0
  86. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/connect.b64 +0 -0
  87. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/connect.png +0 -0
  88. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/create_conf.png +0 -0
  89. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/data_error1_screenshot.jpg +0 -0
  90. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/entry_form.png +0 -0
  91. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/execsql_console.png +0 -0
  92. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/execsql_logo_01.png +0 -0
  93. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/fatals.png +0 -0
  94. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/logo_small.png +0 -0
  95. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/pause_terminal.png +0 -0
  96. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/pause_terminal_sm.b64 +0 -0
  97. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/pause_terminal_sm.png +0 -0
  98. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/prompt_compare.png +0 -0
  99. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/set_build_commands.jpg +0 -0
  100. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/unit_conversions.b64 +0 -0
  101. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/unit_conversions_029.png +0 -0
  102. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/unmatched.png +0 -0
  103. {execsql2-2.13.2 → execsql2-2.14.1}/docs/images/vim_execsql_highlight.png +0 -0
  104. {execsql2-2.13.2 → execsql2-2.14.1}/docs/index.md +0 -0
  105. {execsql2-2.13.2 → execsql2-2.14.1}/docs/reference/configuration.md +0 -0
  106. {execsql2-2.13.2 → execsql2-2.14.1}/docs/reference/security.md +0 -0
  107. {execsql2-2.13.2 → execsql2-2.14.1}/docs/reference/substitution_vars.md +0 -0
  108. {execsql2-2.13.2 → execsql2-2.14.1}/extras/vscode-execsql/README.md +0 -0
  109. {execsql2-2.13.2 → execsql2-2.14.1}/extras/vscode-execsql/package.json +0 -0
  110. {execsql2-2.13.2 → execsql2-2.14.1}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  111. {execsql2-2.13.2 → execsql2-2.14.1}/justfile +0 -0
  112. {execsql2-2.13.2 → execsql2-2.14.1}/scripts/generate_vscode_grammar.py +0 -0
  113. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/__init__.py +0 -0
  114. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/__main__.py +0 -0
  115. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/cli/__init__.py +0 -0
  116. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/cli/dsn.py +0 -0
  117. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/cli/help.py +0 -0
  118. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/cli/lint.py +0 -0
  119. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/cli/run.py +0 -0
  120. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/config.py +0 -0
  121. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/constants.py +0 -0
  122. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/__init__.py +0 -0
  123. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/access.py +0 -0
  124. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/base.py +0 -0
  125. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/dsn.py +0 -0
  126. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/duckdb.py +0 -0
  127. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/factory.py +0 -0
  128. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/firebird.py +0 -0
  129. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/mysql.py +0 -0
  130. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/oracle.py +0 -0
  131. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/postgres.py +0 -0
  132. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/sqlite.py +0 -0
  133. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/db/sqlserver.py +0 -0
  134. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/debug/__init__.py +0 -0
  135. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/debug/repl.py +0 -0
  136. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exceptions.py +0 -0
  137. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/__init__.py +0 -0
  138. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/base.py +0 -0
  139. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/delimited.py +0 -0
  140. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/duckdb.py +0 -0
  141. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/feather.py +0 -0
  142. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/html.py +0 -0
  143. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/json.py +0 -0
  144. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/latex.py +0 -0
  145. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/markdown.py +0 -0
  146. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/ods.py +0 -0
  147. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/parquet.py +0 -0
  148. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/pretty.py +0 -0
  149. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/protocol.py +0 -0
  150. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/raw.py +0 -0
  151. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/sqlite.py +0 -0
  152. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/templates.py +0 -0
  153. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/values.py +0 -0
  154. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/xls.py +0 -0
  155. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/xlsx.py +0 -0
  156. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/xml.py +0 -0
  157. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/yaml.py +0 -0
  158. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/exporters/zip.py +0 -0
  159. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/format.py +0 -0
  160. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/gui/__init__.py +0 -0
  161. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/importers/__init__.py +0 -0
  162. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/importers/base.py +0 -0
  163. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/importers/csv.py +0 -0
  164. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/importers/feather.py +0 -0
  165. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/importers/json.py +0 -0
  166. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/importers/ods.py +0 -0
  167. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/importers/xls.py +0 -0
  168. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/__init__.py +0 -0
  169. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/conditions.py +0 -0
  170. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/control.py +0 -0
  171. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/data.py +0 -0
  172. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/debug.py +0 -0
  173. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/io.py +0 -0
  174. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/io_fileops.py +0 -0
  175. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/io_import.py +0 -0
  176. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/io_write.py +0 -0
  177. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/script_ext.py +0 -0
  178. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/system.py +0 -0
  179. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/metacommands/upsert.py +0 -0
  180. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/models.py +0 -0
  181. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/parser.py +0 -0
  182. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/py.typed +0 -0
  183. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/script/__init__.py +0 -0
  184. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/script/control.py +0 -0
  185. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/script/engine.py +0 -0
  186. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/script/variables.py +0 -0
  187. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/state.py +0 -0
  188. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/types.py +0 -0
  189. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/__init__.py +0 -0
  190. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/auth.py +0 -0
  191. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/crypto.py +0 -0
  192. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/datetime.py +0 -0
  193. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/errors.py +0 -0
  194. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/fileio.py +0 -0
  195. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/gui.py +0 -0
  196. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/mail.py +0 -0
  197. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/numeric.py +0 -0
  198. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/regex.py +0 -0
  199. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/strings.py +0 -0
  200. {execsql2-2.13.2 → execsql2-2.14.1}/src/execsql/utils/timer.py +0 -0
  201. {execsql2-2.13.2 → execsql2-2.14.1}/templates/README.md +0 -0
  202. {execsql2-2.13.2 → execsql2-2.14.1}/templates/config_settings.sqlite +0 -0
  203. {execsql2-2.13.2 → execsql2-2.14.1}/templates/example_config_prompt.sql +0 -0
  204. {execsql2-2.13.2 → execsql2-2.14.1}/templates/execsql.conf +0 -0
  205. {execsql2-2.13.2 → execsql2-2.14.1}/templates/make_config_db.sql +0 -0
  206. {execsql2-2.13.2 → execsql2-2.14.1}/templates/md_compare.sql +0 -0
  207. {execsql2-2.13.2 → execsql2-2.14.1}/templates/md_glossary.sql +0 -0
  208. {execsql2-2.13.2 → execsql2-2.14.1}/templates/md_upsert.sql +0 -0
  209. {execsql2-2.13.2 → execsql2-2.14.1}/templates/pg_compare.sql +0 -0
  210. {execsql2-2.13.2 → execsql2-2.14.1}/templates/pg_glossary.sql +0 -0
  211. {execsql2-2.13.2 → execsql2-2.14.1}/templates/pg_upsert.sql +0 -0
  212. {execsql2-2.13.2 → execsql2-2.14.1}/templates/script_template.sql +0 -0
  213. {execsql2-2.13.2 → execsql2-2.14.1}/templates/ss_compare.sql +0 -0
  214. {execsql2-2.13.2 → execsql2-2.14.1}/templates/ss_glossary.sql +0 -0
  215. {execsql2-2.13.2 → execsql2-2.14.1}/templates/ss_upsert.sql +0 -0
  216. {execsql2-2.13.2 → execsql2-2.14.1}/tests/__init__.py +0 -0
  217. {execsql2-2.13.2 → execsql2-2.14.1}/tests/cli/__init__.py +0 -0
  218. {execsql2-2.13.2 → execsql2-2.14.1}/tests/cli/test_cli.py +0 -0
  219. {execsql2-2.13.2 → execsql2-2.14.1}/tests/cli/test_cli_e2e.py +0 -0
  220. {execsql2-2.13.2 → execsql2-2.14.1}/tests/cli/test_cli_run.py +0 -0
  221. {execsql2-2.13.2 → execsql2-2.14.1}/tests/cli/test_lint.py +0 -0
  222. {execsql2-2.13.2 → execsql2-2.14.1}/tests/cli/test_ping.py +0 -0
  223. {execsql2-2.13.2 → execsql2-2.14.1}/tests/cli/test_profile.py +0 -0
  224. {execsql2-2.13.2 → execsql2-2.14.1}/tests/conftest.py +0 -0
  225. {execsql2-2.13.2 → execsql2-2.14.1}/tests/db/__init__.py +0 -0
  226. {execsql2-2.13.2 → execsql2-2.14.1}/tests/db/test_base.py +0 -0
  227. {execsql2-2.13.2 → execsql2-2.14.1}/tests/db/test_duckdb.py +0 -0
  228. {execsql2-2.13.2 → execsql2-2.14.1}/tests/db/test_factory.py +0 -0
  229. {execsql2-2.13.2 → execsql2-2.14.1}/tests/db/test_postgres.py +0 -0
  230. {execsql2-2.13.2 → execsql2-2.14.1}/tests/db/test_sqlite.py +0 -0
  231. {execsql2-2.13.2 → execsql2-2.14.1}/tests/db/test_sqlite_extra.py +0 -0
  232. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/__init__.py +0 -0
  233. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_base.py +0 -0
  234. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_db.py +0 -0
  235. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_delimited.py +0 -0
  236. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_duckdb_exporter.py +0 -0
  237. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_exporters.py +0 -0
  238. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_feather.py +0 -0
  239. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_html_latex.py +0 -0
  240. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_json.py +0 -0
  241. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_markdown.py +0 -0
  242. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_ods.py +0 -0
  243. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_parquet.py +0 -0
  244. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_sqlite_exporter.py +0 -0
  245. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_templates.py +0 -0
  246. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_xls_xlsx.py +0 -0
  247. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_xlsx.py +0 -0
  248. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_xml.py +0 -0
  249. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_yaml.py +0 -0
  250. {execsql2-2.13.2 → execsql2-2.14.1}/tests/exporters/test_zip.py +0 -0
  251. {execsql2-2.13.2 → execsql2-2.14.1}/tests/gui/__init__.py +0 -0
  252. {execsql2-2.13.2 → execsql2-2.14.1}/tests/importers/__init__.py +0 -0
  253. {execsql2-2.13.2 → execsql2-2.14.1}/tests/importers/test_csv_importer.py +0 -0
  254. {execsql2-2.13.2 → execsql2-2.14.1}/tests/importers/test_feather_importer.py +0 -0
  255. {execsql2-2.13.2 → execsql2-2.14.1}/tests/importers/test_json_importer.py +0 -0
  256. {execsql2-2.13.2 → execsql2-2.14.1}/tests/importers/test_ods_importer.py +0 -0
  257. {execsql2-2.13.2 → execsql2-2.14.1}/tests/importers/test_xls_importer.py +0 -0
  258. {execsql2-2.13.2 → execsql2-2.14.1}/tests/integration/__init__.py +0 -0
  259. {execsql2-2.13.2 → execsql2-2.14.1}/tests/integration/conftest.py +0 -0
  260. {execsql2-2.13.2 → execsql2-2.14.1}/tests/integration/test_dsn.py +0 -0
  261. {execsql2-2.13.2 → execsql2-2.14.1}/tests/integration/test_duckdb.py +0 -0
  262. {execsql2-2.13.2 → execsql2-2.14.1}/tests/integration/test_mysql.py +0 -0
  263. {execsql2-2.13.2 → execsql2-2.14.1}/tests/integration/test_postgres.py +0 -0
  264. {execsql2-2.13.2 → execsql2-2.14.1}/tests/integration/test_sqlite.py +0 -0
  265. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/__init__.py +0 -0
  266. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_assert.py +0 -0
  267. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_breakpoint.py +0 -0
  268. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_connect.py +0 -0
  269. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_io_export.py +0 -0
  270. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_io_import.py +0 -0
  271. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands.py +0 -0
  272. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_connect.py +0 -0
  273. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_data.py +0 -0
  274. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_extended.py +0 -0
  275. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  276. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_io.py +0 -0
  277. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  278. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  279. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_system.py +0 -0
  280. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  281. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_pg_upsert.py +0 -0
  282. {execsql2-2.13.2 → execsql2-2.14.1}/tests/metacommands/test_row_count.py +0 -0
  283. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_config.py +0 -0
  284. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_config_data.py +0 -0
  285. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_constants.py +0 -0
  286. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_engine.py +0 -0
  287. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_error_messages.py +0 -0
  288. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_exceptions.py +0 -0
  289. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_format.py +0 -0
  290. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_mail.py +0 -0
  291. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_models.py +0 -0
  292. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_package.py +0 -0
  293. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_parser.py +0 -0
  294. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_registry.py +0 -0
  295. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_script.py +0 -0
  296. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_state.py +0 -0
  297. {execsql2-2.13.2 → execsql2-2.14.1}/tests/test_types.py +0 -0
  298. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/__init__.py +0 -0
  299. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_auth.py +0 -0
  300. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_auth_extra.py +0 -0
  301. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_crypto.py +0 -0
  302. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_datetime.py +0 -0
  303. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_errors.py +0 -0
  304. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_errors_extra.py +0 -0
  305. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_fileio.py +0 -0
  306. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_fileio_extra.py +0 -0
  307. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_numeric.py +0 -0
  308. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_regex.py +0 -0
  309. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_strings.py +0 -0
  310. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_timer.py +0 -0
  311. {execsql2-2.13.2 → execsql2-2.14.1}/tests/utils/test_timer_extra.py +0 -0
  312. {execsql2-2.13.2 → execsql2-2.14.1}/zensical.toml +0 -0
@@ -153,7 +153,7 @@ ______________________________________________________________________
153
153
  | `pre-commit` | Git hooks | gitleaks, uv-lock, ruff, mdformat, markdownlint, typos, validate-pyproject |
154
154
  | `zensical` | Docs builder | MkDocs-compatible, Material theme, configured via `zensical.toml` |
155
155
  | `mkdocstrings` | API docs from docstrings | Wired up — `docs/api/` pages auto-generate from source |
156
- | `pytest-cov` | Coverage | `--cov-fail-under=80` enforced |
156
+ | `pytest-cov` | Coverage | `--cov-fail-under=90` enforced (raised from 80% in v2.12) |
157
157
 
158
158
  ## Package Layout Decision
159
159
 
@@ -192,7 +192,7 @@ Triggered on: push to `main`, any tag `v*.*.*`, pull requests.
192
192
 
193
193
  ## Versioning
194
194
 
195
- `bump-my-version` manages versions. Current: `2.11.0`. Bump commands:
195
+ `bump-my-version` manages versions. Current: `2.13.2`. Bump commands:
196
196
 
197
197
  - `just bump-patch` → 2.11.0 → 2.11.1
198
198
  - `just bump-minor` → 2.11.0 → 2.12.0
@@ -271,6 +271,16 @@ the foreseeable future.
271
271
  | `BREAKPOINT` debug REPL with step mode | 2.10.0 | 2026-04 |
272
272
  | Error messages restored (script location, command) | 2.11.0 | 2026-04 |
273
273
  | 16-item codebase analysis fix sweep | 2.11.x | 2026-04 |
274
+ | pg-upsert integration (`PG_UPSERT` metacommand) | 2.12.0 | 2026-04 |
275
+ | Coverage floor raised to 90% (3,600+ tests) | 2.12.x | 2026-04 |
276
+ | `--debug` step-through mode | 2.12.x | 2026-04 |
277
+ | `--lint` two-pass variables, EXECUTE SCRIPT flow | 2.13.x | 2026-04 |
278
+ | GUI: row counts, help URL button, compare diffs | unreleased | 2026-04 |
279
+ | GUI: PROMPT ENTRY_FORM all entry_type values | unreleased | 2026-04 |
280
+ | GUI: PROMPT COMPARE AND/BESIDE orientation fix | unreleased | 2026-04 |
281
+ | GUI: highlight diffs toggle (Tkinter + Textual) | unreleased | 2026-04 |
282
+ | Metacommand audit: regex/doc/handler bug fixes | unreleased | 2026-04 |
283
+ | Removed FREE keyword from PROMPT DISPLAY | unreleased | 2026-04 |
274
284
 
275
285
  ______________________________________________________________________
276
286
 
@@ -298,6 +308,11 @@ ______________________________________________________________________
298
308
 
299
309
  ### Candidate Features (unscheduled — pick and assign to milestones)
300
310
 
311
+ #### GUI — Essential
312
+
313
+ - [ ] **Textual console (`CONSOLE ON`)** — The Textual TUI backend has a stub `console_on()` that sets a flag but provides no actual GUI console. Tkinter has a full console window (ConsoleWindow) with output area, status bar, and progress bar. Textual needs an equivalent: a persistent RichLog-based panel that receives WRITE output, shows status, and progress. **This is a gap — users on `--gui-framework textual` with `CONFIG GUI_LEVEL 3` or `CONSOLE ON` get no console.**
314
+ - [ ] **Form validation** — `EntrySpec` has `validation_regex` and `validation_key_regex` fields but no backend enforces them. Tkinter should validate on focus-loss (validation_regex) and per-keystroke (validation_key_regex). Textual should validate on submit. Console should validate before accepting.
315
+
301
316
  #### Quick Wins
302
317
 
303
318
  - [x] **`--ping`** — test database connectivity and exit with a status message. Useful for CI health checks and connection debugging. (shipped v2.8.x)
@@ -359,6 +374,7 @@ ______________________________________________________________________
359
374
  ### Ongoing / No-milestone
360
375
 
361
376
  - Textual TUI polish
377
+ - Coverage target: raise from 90% → 93% (in progress)
362
378
 
363
379
  ______________________________________________________________________
364
380
 
@@ -13,6 +13,46 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.14.1] - 2026-04-07
17
+
18
+ ______________________________________________________________________
19
+
20
+ ## [2.14.0] - 2026-04-07
21
+
22
+ ### Added
23
+
24
+ - Row count footer displayed below every table in GUI dialogs (Textual TUI, Tkinter desktop, and console fallback). Shows format like "3 rows" or "1 row" with comma-separated thousands for large counts.
25
+ - Help URL button in all GUI dialogs that support the `HELP` keyword. Clicking the button opens the URL in the system browser. Console fallback prints the URL.
26
+ - Diff summary line in compare dialogs showing matching, differing, and table-exclusive row counts (e.g., "3 matching | 1 differing | 2 only in Table 1").
27
+ - `PROMPT ENTRY_FORM` now enforces `validation_regex` (on submit) and `validation_key_regex` (per-keystroke) validation across all GUI backends. Required fields are also validated on submit. Tkinter shows a messagebox on validation failure; Textual shows a notification; Console re-prompts.
28
+ - "Highlight Diffs" toggle button in compare dialogs (Textual and Tkinter) that color-codes rows: green for matching, yellow for changed, red for rows only in one table.
29
+
30
+ ### Fixed
31
+
32
+ - `CONFIG GUI_LEVEL` now accepts value `3` (open GUI console on start), matching the `gui_level` configuration file setting.
33
+ - `PROMPT COMPARE` now respects the `AND` vs `BESIDE` keyword: `AND` stacks tables vertically, `BESIDE` displays them side-by-side. Previously both orientations displayed side-by-side.
34
+ - `PROMPT ENTRY_FORM` now renders all documented `entry_type` values: `listbox` (multi-select list), `radiobuttons` (radio button group), `textarea` (multi-line text area), `inputfile` and `outputfile` (text field with file browser button). Previously only `checkbox` and `dropdown`/`select` were implemented; all others fell through to a plain text input.
35
+ - `PROMPT ENTER_SUB` HELP URL regex typo: quoted HELP URLs containing `+` characters now match correctly (was using `[^+]` instead of `[^"]`).
36
+ - PostgreSQL and DSN `CONNECT` handlers now unquote the PASSWORD parameter consistently with all other database handlers.
37
+ - SQL Server `CONNECT` handler now uses consistent keyword argument `user_name=` in all code paths.
38
+
39
+ ### Changed
40
+
41
+ - Documentation: added `CONFIG LOG_SQL` and `CONFIG SHOW_PROGRESS` sections to metacommands reference (were implemented but undocumented).
42
+ - Documentation: DuckDB `CONNECT` syntax now shows the `NEW` keyword (was supported but undocumented).
43
+ - Documentation: `EXPORT QUERY` format list now explicitly mentions PARQUET, FEATHER, YAML, MARKDOWN support.
44
+ - Documentation: added alias notes for `EXEC SCRIPT` / `RUN SCRIPT` and `APPEND SCRIPT`.
45
+ - Documentation: `RM_SUB` now documents `~` prefix for deleting local variables.
46
+ - Documentation: fixed missing bracket in `WRITE CREATE_TABLE FROM EXCEL` syntax.
47
+
48
+ ### Removed
49
+
50
+ - `FREE` keyword from `PROMPT DISPLAY` metacommand. The non-blocking display behavior was only implemented in the console backend; Textual and Tkinter backends ignored it.
51
+ - Tkinter dialog buttons are now right-aligned (matching the Textual TUI layout) instead of centered.
52
+ - Tkinter dialog message text is now left-aligned instead of center-justified.
53
+
54
+ ______________________________________________________________________
55
+
16
56
  ## [2.13.2] - 2026-04-06
17
57
 
18
58
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.13.2
3
+ Version: 2.14.1
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
@@ -79,10 +79,14 @@ New options in `execsql.conf`:
79
79
 
80
80
  ### GUI
81
81
 
82
- | Feature | Description |
83
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
- | Textual TUI backend | Full terminal-UI backend via the `textual` library. Provides all dialog types (password, pause, message, entry, compare, action, etc.) in the terminal. |
85
- | Console fallback | Text-only backend that handles GUI calls in headless environments by printing to stdout. |
82
+ | Feature | Description |
83
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | Textual TUI backend | Full terminal-UI backend via the `textual` library. Provides all dialog types (password, pause, message, entry, compare, action, etc.) in the terminal. |
85
+ | Console fallback | Text-only backend that handles GUI calls in headless environments by printing to stdout. |
86
+ | Table row counts | All GUI backends (Tkinter, Textual, Console) display a row count footer below every table widget (e.g. "3 rows", "1 row"). |
87
+ | Help URL button | Dialogs that accept the `HELP` keyword display a clickable Help button that opens the URL in the system browser. |
88
+ | Compare diff summary | The compare dialog shows a one-line summary of matching, differing, and table-exclusive rows when key columns are specified. |
89
+ | Form validation | `PROMPT ENTRY_FORM` enforces `validation_regex` (on submit) and `validation_key_regex` (per-keystroke) across all backends. Required fields validated. |
86
90
 
87
91
  ### Authentication
88
92
 
@@ -235,7 +239,8 @@ ______________________________________________________________________
235
239
 
236
240
  ## Removed Features
237
241
 
238
- | Feature | Reason |
239
- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
240
- | Airspeed template processor | The `airspeed` library (Velocity clone) is unmaintained since ~2018. Use `FORMAT jinja` instead. The `airspeed` value for `template_processor` in `execsql.conf` is no longer accepted. |
241
- | Python 2 compatibility | All Python 2 constructs (`stringtypes`, `u""` literals, `optparse`, etc.) have been removed. execsql2 requires Python 3.10+. |
242
+ | Feature | Reason |
243
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
244
+ | Airspeed template processor | The `airspeed` library (Velocity clone) is unmaintained since ~2018. Use `FORMAT jinja` instead. The `airspeed` value for `template_processor` in `execsql.conf` is no longer accepted. |
245
+ | Python 2 compatibility | All Python 2 constructs (`stringtypes`, `u""` literals, `optparse`, etc.) have been removed. execsql2 requires Python 3.10+. |
246
+ | `FREE` keyword on `PROMPT DISPLAY` | The non-blocking display behavior was only implemented in the console backend; the Textual and Tkinter GUI backends ignored it. Removed rather than partially supported. |
@@ -417,13 +417,14 @@ This setting is also applied to the conversion of spreadsheet names to table nam
417
417
  CONFIG GUI_LEVEL <n>
418
418
  ```
419
419
 
420
- The level of interaction with the user that should be carried out using GUI dialogs. The numeric value *n* must be 0, 1, or 2. The meanings of these values are:
420
+ The level of interaction with the user that should be carried out using GUI dialogs. The numeric value *n* must be 0, 1, 2, or 3. The meanings of these values are:
421
421
 
422
422
  > - 0: Do not use any optional GUI dialogs.
423
423
  > - 1: Use GUI dialogs for password prompts and for the [PAUSE](#pause) metacommand.
424
424
  > - 2: Also use a GUI dialog if a message is included with the [HALT](#halt) metacommand, and prompt for the initial database to use if no database connection parameters are specified in a configuration file or on the command line.
425
+ > - 3: Additionally, open a GUI console when *execsql* starts.
425
426
 
426
- This is equivalent to the `gui_level` [configuration setting](configuration.md#gui_level) except that the corresponding configuration setting also allows a value of 3.
427
+ This is equivalent to the `gui_level` [configuration setting](configuration.md#gui_level).
427
428
 
428
429
 
429
430
  <a id="config_hdf5"></a>
@@ -457,6 +458,13 @@ CONFIG LOG_DATAVARS YES|NO
457
458
  Controls whether data variables that are created by the [SELECT_SUB](#select_sub), [PROMPT SELECT_SUB](#prompt_selsub) and [PROMPT ACTION](#prompt_action) metacommands are written to *execsql*'s [log file](../guides/logging.md#logging). By default, all data variable assignments are logged. The performance of scripts that make extensive use of these metacommands (e.g., [Example 27](../guides/examples.md#example27)) can be improved by changing this setting to 'No'.
458
459
 
459
460
 
461
+ ```
462
+ CONFIG LOG_SQL YES|NO
463
+ ```
464
+
465
+ Enable or disable SQL query audit logging at runtime. When enabled, each SQL statement executed against the database is written to *execsql*'s [log file](../guides/logging.md#logging) as a `sql` record containing the database name, source line number, and query text. This is equivalent to the `log_sql` [configuration setting](configuration.md#config_logging). The default value is "No".
466
+
467
+
460
468
  ```
461
469
  CONFIG LOG_WRITE_MESSAGES YES|NO
462
470
  ```
@@ -506,6 +514,13 @@ CONFIG SCAN_LINES <n>
506
514
  The number of lines of a data file to scan during [IMPORT](#import) to determine the quoting character and delimiter character used. This is equivalent to the "-s" command-line option and the `scan_lines` [configuration setting](configuration.md#scan_lines).
507
515
 
508
516
 
517
+ ```
518
+ CONFIG SHOW_PROGRESS YES|NO
519
+ ```
520
+
521
+ Enable or disable the Rich progress bar for [IMPORT](#import) operations at runtime. When enabled, a progress bar is displayed showing the number of rows imported. This is equivalent to the `show_progress` [configuration setting](configuration.md#config_input) and the `--progress` CLI option. The default value is "No".
522
+
523
+
509
524
  ```
510
525
  CONFIG TRIM_COLUMN_HEADERS NONE|BOTH|LEFT|RIGHT
511
526
  ```
@@ -635,7 +650,7 @@ CONNECT USER TO MARIADB(SERVER=<server_name>, DB=<database_name>
635
650
  For DuckDB:
636
651
 
637
652
  ```
638
- CONNECT TO DUCKDB(FILE=<database_file>) AS <alias_name>
653
+ CONNECT TO DUCKDB(FILE=<database_file> [, NEW]) AS <alias_name>
639
654
  ```
640
655
 
641
656
  For Firebird:
@@ -686,7 +701,7 @@ If the command form with the "USER" keyword is used, the user name that is used
686
701
 
687
702
  The alias name that is specified in this command can be used to refer to this database in the [USE](#use) and [COPY](#copy) metacommands. Alias names can consist only of letters, digits, and underscores, and must start with a letter. The alias name "initial" is reserved for the database that is used when *execsql* starts script processing, and cannot be used with the [CONNECT](#connect) metacommand. If you re-use an alias name, the connection to the database to which that name was previously assigned will be closed, and the database will no longer be available. Using the same alias for two different databases allows for mistakes wherein script statements are run on the wrong database, and so is not recommended.
688
703
 
689
- If the "NEW" keyword is used with PostgreSQL or SQLite, a new database of the given name will be created. There must be no existing database of that name, and (for Postgres) you must have permissions assigned that allow you to create databases.
704
+ If the "NEW" keyword is used with PostgreSQL, SQLite, or DuckDB, a new database of the given name will be created. There must be no existing database of that name, and (for Postgres) you must have permissions assigned that allow you to create databases.
690
705
 
691
706
 
692
707
  ## CONSOLE
@@ -894,6 +909,8 @@ EXECUTE SCRIPT [IF EXISTS] <script_name>
894
909
  ```
895
910
 
896
911
 
912
+ `EXEC SCRIPT` and `RUN SCRIPT` are accepted as aliases for `EXECUTE SCRIPT`.
913
+
897
914
  This metacommand will execute the set of SQL statements and metacommands that was previously defined and named using the [BEGIN/END SCRIPT](#beginscript) metacommands.
898
915
 
899
916
  If the IF EXISTS clause is included, the script will be executed only if it has been defined.
@@ -1200,7 +1217,7 @@ EXPORT QUERY <<query>> [TEE] [APPEND] TO <filename>|stdout
1200
1217
 
1201
1218
  Exports data in the same manner as the [EXPORT](#export) metacommand, except that the data source is a SQL query statement that is contained in the metacommand rather than a database table or view. The SQL query statement must be terminated with a semicolon and enclosed in double angle brackets (i.e., literally "`<<`" and "`>>`").
1202
1219
 
1203
- The EXPORT QUERY metacommand does not support export to XML or HDF5 because there is no table name specified. Export to ODS supports only a single query, rather than a list as for the [EXPORT](#export) metacommand.
1220
+ The EXPORT QUERY metacommand does not support export to XML, HDF5, SQLITE, or DUCKDB because there is no table name specified. It does support all other formats including PARQUET, FEATHER, YAML, and MARKDOWN. Export to ODS supports only a single query, rather than a list as for the [EXPORT](#export) metacommand.
1204
1221
 
1205
1222
  Like all metacommands, this metacommand must appear on a single line, although the SQL statement may be quite long. To facilitate readability, the SQL statement may be saved in a [substitution
1206
1223
  variable](substitution_vars.md#substitution_vars) and that substitution variable referenced in the EXPORT QUERY metacommand.
@@ -1234,6 +1251,8 @@ EXTEND SCRIPT <script_1> WITH SCRIPT <script_2>
1234
1251
 
1235
1252
  Merges two scripts, appending the lines of *script_2* to the end of *script_1*. Both scripts must have already been defined using the [BEGIN SCRIPT](#beginscript) metacommand. Parameters for *script_2* are also added to *script_1*.
1236
1253
 
1254
+ The alternative syntax `APPEND SCRIPT <script_2> TO <script_1>` is also accepted and performs the same operation.
1255
+
1237
1256
 
1238
1257
  ## HALT
1239
1258
 
@@ -2367,7 +2386,7 @@ The selection is [logged](../guides/logging.md#logging).
2367
2386
 
2368
2387
  ```
2369
2388
  PROMPT DISPLAY <table_or_view_name> MESSAGE "<text>"
2370
- [HELP <url>] [FREE]
2389
+ [HELP <url>]
2371
2390
  ```
2372
2391
 
2373
2392
  Displays the selected view or table in a window with the specified message and both 'Continue' and 'Cancel' buttons. If the 'Continue' button is selected, the script will continue to run. If the 'Cancel' button is selected, the script will immediately halt unless [CANCEL_HALT](#cancel_halt) is set to OFF. The Enter key also carries out the action of the 'Continue' button, and the Escape key carries out the action of the 'Cancel' button.
@@ -2384,8 +2403,6 @@ If [CANCEL_HALT](#cancel_halt) is set to OFF, the [DIALOG_CANCELED](#dialog_canc
2384
2403
 
2385
2404
  If a URL is provided with the HELP keyword, the dialog box will include a button that will open that URL when clicked. The URL must be double-quoted if it contains spaces.
2386
2405
 
2387
- if the FREE keyword is used, the script will keep running when the data table is displayed. The display will have only a "Close" button. If the display is not closed manually, it will be closed automatically when *execsql* finishes processing the script.
2388
-
2389
2406
 
2390
2407
  ## PROMPT ENTER_SUB { #prompt_enter }
2391
2408
 
@@ -2627,7 +2644,7 @@ The file name provided may include wildcards to delete multiple files.
2627
2644
  RM_SUB <match_string>
2628
2645
  ```
2629
2646
 
2630
- Deletes the specified user-created substitution variable.
2647
+ Deletes the specified user-created substitution variable. If the match string is prefixed with `~`, the local variable with that name is deleted from the current script scope instead of the global substitution variable set.
2631
2648
 
2632
2649
 
2633
2650
  ## SELECT_SUB
@@ -2884,7 +2901,7 @@ For data in an Excel spreadsheet:
2884
2901
 
2885
2902
  ```
2886
2903
  WRITE CREATE_TABLE <table_name> FROM EXCEL <file_name>
2887
- SHEET <sheet_name> [SKIP <rows>] ENCODING <encoding>]
2904
+ SHEET <sheet_name> [SKIP <rows>] [ENCODING <encoding>]
2888
2905
  [COMMENT "<comment_text>"] [TO <output>]
2889
2906
  ```
2890
2907
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.13.2"
7
+ version = "2.14.1"
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" }
@@ -161,7 +161,7 @@ skip-magic-trailing-comma = false
161
161
  line-ending = "auto"
162
162
 
163
163
  [tool.bumpversion]
164
- current_version = "2.13.2"
164
+ current_version = "2.14.1"
165
165
  commit = true
166
166
  commit_args = "--no-verify"
167
167
  tag = true
@@ -8,7 +8,58 @@ from typing import TYPE_CHECKING, Any
8
8
  if TYPE_CHECKING:
9
9
  pass
10
10
 
11
- __all__ = ["GuiBackend"]
11
+ __all__ = ["GuiBackend", "compare_stats"]
12
+
13
+
14
+ def compare_stats(
15
+ headers1: list,
16
+ rows1: list,
17
+ headers2: list,
18
+ rows2: list,
19
+ keylist: list,
20
+ ) -> str:
21
+ """Return a one-line diff summary for compare dialogs.
22
+
23
+ Computes matching rows, differing rows, and rows only in one table
24
+ based on the given key columns. Returns an empty string when *keylist*
25
+ is empty (stats cannot be computed without keys).
26
+ """
27
+ if not keylist:
28
+ return ""
29
+ key_idx1 = [i for i, h in enumerate(headers1) if str(h) in keylist]
30
+ key_idx2 = [i for i, h in enumerate(headers2) if str(h) in keylist]
31
+ if not key_idx1 or not key_idx2:
32
+ return ""
33
+
34
+ def _kv(row: list | tuple, idxs: list) -> tuple:
35
+ return tuple(str(row[i]) if row[i] is not None else "" for i in idxs)
36
+
37
+ keys1 = {_kv(r, key_idx1) for r in rows1}
38
+ keys2 = {_kv(r, key_idx2) for r in rows2}
39
+ only1 = len(keys1 - keys2)
40
+ only2 = len(keys2 - keys1)
41
+ common_keys = keys1 & keys2
42
+ row_map1 = {_kv(r, key_idx1): r for r in rows1}
43
+ row_map2 = {_kv(r, key_idx2): r for r in rows2}
44
+ matching = 0
45
+ differing = 0
46
+ for k in common_keys:
47
+ r1 = [str(v) if v is not None else "" for v in row_map1[k]]
48
+ r2 = [str(v) if v is not None else "" for v in row_map2[k]]
49
+ if r1 == r2:
50
+ matching += 1
51
+ else:
52
+ differing += 1
53
+ parts: list[str] = []
54
+ if matching:
55
+ parts.append(f"{matching:,} matching")
56
+ if differing:
57
+ parts.append(f"{differing:,} differing")
58
+ if only1:
59
+ parts.append(f"{only1:,} only in Table 1")
60
+ if only2:
61
+ parts.append(f"{only2:,} only in Table 2")
62
+ return " | ".join(parts) if parts else "Tables are identical"
12
63
 
13
64
 
14
65
  class GuiBackend(ABC):
@@ -11,11 +11,23 @@ import sys
11
11
  import time
12
12
  from typing import Any
13
13
 
14
- from execsql.gui.base import GuiBackend
14
+ from execsql.gui.base import GuiBackend, compare_stats as _compare_stats
15
15
 
16
16
  __all__ = ["ConsoleBackend"]
17
17
 
18
18
 
19
+ def _print_help_url(args: dict) -> None:
20
+ """Print the help URL if present in *args*."""
21
+ url = args.get("help_url")
22
+ if url:
23
+ print(f" Help: {url}", file=sys.stderr)
24
+
25
+
26
+ def _row_count_text(n: int) -> str:
27
+ """Return a human-readable row count string, e.g. '3 rows' or '1 row'."""
28
+ return f"{n:,} row{'s' if n != 1 else ''}"
29
+
30
+
19
31
  def _print_table(headers: list, rows: list, file: Any = None) -> None:
20
32
  """Print a simple ASCII table to the given file (default stderr)."""
21
33
  if file is None:
@@ -89,6 +101,7 @@ class ConsoleBackend(GuiBackend):
89
101
  rows = args.get("rowset")
90
102
  if headers and rows:
91
103
  _print_table(headers, rows)
104
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
92
105
  input("Press Enter to acknowledge...")
93
106
  return {"button": 1}
94
107
 
@@ -119,14 +132,14 @@ class ConsoleBackend(GuiBackend):
119
132
  textentry = args.get("textentry", False)
120
133
  hidetext = args.get("hidetext", False)
121
134
  initial = args.get("initialtext", "")
122
- free = args.get("free", False)
123
-
124
135
  if title:
125
136
  print(f"\n=== {title} ===", file=sys.stderr)
126
137
  if message:
127
138
  print(message, file=sys.stderr)
139
+ _print_help_url(args)
128
140
  if headers and rows:
129
141
  _print_table(headers, rows)
142
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
130
143
 
131
144
  return_value = None
132
145
  if textentry:
@@ -137,9 +150,6 @@ class ConsoleBackend(GuiBackend):
137
150
  else:
138
151
  return_value = input(f"Enter value [{initial}]: ").strip() or initial
139
152
 
140
- if free:
141
- return {"button": 1, "return_value": return_value}
142
-
143
153
  btn = _prompt_buttons(button_list)
144
154
  return {"button": btn, "return_value": return_value}
145
155
 
@@ -154,15 +164,17 @@ class ConsoleBackend(GuiBackend):
154
164
  print(f"\n=== {title} ===", file=sys.stderr)
155
165
  if message:
156
166
  print(message, file=sys.stderr)
167
+ _print_help_url(args)
157
168
  if headers and rows:
158
169
  _print_table(headers, rows)
170
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
159
171
 
160
172
  for spec in entry_specs:
161
173
  entry_type = (spec.entry_type or "text").lower()
162
174
  initial = spec.initial_value or ""
163
175
  if entry_type == "checkbox":
164
176
  raw = input(f"{spec.label} [y/n, current={initial}]: ").strip().lower()
165
- spec.value = "True" if raw in ("y", "yes", "true", "1") else "False"
177
+ spec.value = "1" if raw in ("y", "yes", "true", "1") else "0"
166
178
  elif entry_type in ("dropdown", "select") and spec.lookup_list:
167
179
  choices = spec.lookup_list
168
180
  print(f"{spec.label}:", file=sys.stderr)
@@ -177,9 +189,58 @@ class ConsoleBackend(GuiBackend):
177
189
  spec.value = raw
178
190
  break
179
191
  print("Invalid choice.", file=sys.stderr)
180
- else:
181
- raw = input(f"{spec.label} [{initial}]: ").strip()
192
+ elif entry_type == "listbox" and spec.lookup_list:
193
+ choices = spec.lookup_list
194
+ print(f"{spec.label} (enter numbers separated by commas):", file=sys.stderr)
195
+ for i, c in enumerate(choices, 1):
196
+ print(f" [{i}] {c}", file=sys.stderr)
197
+ raw = input("Selections: ").strip()
198
+ selected = []
199
+ for part in raw.split(","):
200
+ part = part.strip()
201
+ if part.isdigit() and 1 <= int(part) <= len(choices):
202
+ selected.append(choices[int(part) - 1])
203
+ spec.value = ",".join(f"'{v.replace(chr(39), chr(39) + chr(39))}'" for v in selected)
204
+ elif entry_type == "radiobuttons":
205
+ parts = (spec.label or "").split(";")
206
+ label = parts[0] if parts else spec.label
207
+ buttons = parts[1:] if len(parts) > 1 else [spec.label or "Option"]
208
+ print(f"{label}:", file=sys.stderr)
209
+ for i, b in enumerate(buttons, 1):
210
+ print(f" [{i}] {b.strip()}", file=sys.stderr)
211
+ while True:
212
+ raw = input("Choice number: ").strip()
213
+ if raw.isdigit() and 1 <= int(raw) <= len(buttons):
214
+ spec.value = raw
215
+ break
216
+ print("Invalid choice.", file=sys.stderr)
217
+ elif entry_type == "textarea":
218
+ print(f"{spec.label} (enter text, blank line to finish):", file=sys.stderr)
219
+ lines = []
220
+ while True:
221
+ line = input()
222
+ if not line:
223
+ break
224
+ lines.append(line)
225
+ spec.value = "\n".join(lines) or initial
226
+ elif entry_type in ("inputfile", "outputfile"):
227
+ raw = input(f"{spec.label} (file path) [{initial}]: ").strip()
182
228
  spec.value = raw or initial
229
+ else:
230
+ while True:
231
+ raw = input(f"{spec.label} [{initial}]: ").strip()
232
+ val = raw or initial
233
+ if spec.required and not val:
234
+ print(" (required)", file=sys.stderr)
235
+ continue
236
+ if spec.validation_regex and val:
237
+ import re as _re
238
+
239
+ if not _re.fullmatch(spec.validation_regex, val):
240
+ print(f" (must match: {spec.validation_regex})", file=sys.stderr)
241
+ continue
242
+ spec.value = val
243
+ break
183
244
 
184
245
  print("", file=sys.stderr)
185
246
  raw = input("Submit? [y/n]: ").strip().lower()
@@ -199,10 +260,18 @@ class ConsoleBackend(GuiBackend):
199
260
  print(f"\n=== {title} ===", file=sys.stderr)
200
261
  if message:
201
262
  print(message, file=sys.stderr)
263
+ _print_help_url(args)
202
264
  print("\n--- Table 1 ---", file=sys.stderr)
203
265
  _print_table(headers1, rows1)
266
+ print(f" {_row_count_text(len(rows1))}", file=sys.stderr)
204
267
  print("\n--- Table 2 ---", file=sys.stderr)
205
268
  _print_table(headers2, rows2)
269
+ print(f" {_row_count_text(len(rows2))}", file=sys.stderr)
270
+
271
+ keylist = [str(k) for k in args.get("keylist", [])]
272
+ summary = _compare_stats(headers1, rows1, headers2, rows2, keylist)
273
+ if summary:
274
+ print(f"\n {summary}", file=sys.stderr)
206
275
 
207
276
  btn = _prompt_buttons(button_list)
208
277
  return {"button": btn}
@@ -217,7 +286,9 @@ class ConsoleBackend(GuiBackend):
217
286
  print(f"\n=== {title} ===", file=sys.stderr)
218
287
  if message:
219
288
  print(message, file=sys.stderr)
289
+ _print_help_url(args)
220
290
  _print_table(headers1, rows1)
291
+ print(f" {_row_count_text(len(rows1))}", file=sys.stderr)
221
292
  print("(Row selection requires a GUI backend; displaying source data only.)", file=sys.stderr)
222
293
 
223
294
  btn = _prompt_buttons(button_list)
@@ -255,8 +326,10 @@ class ConsoleBackend(GuiBackend):
255
326
  print(f"\n=== {title} ===", file=sys.stderr)
256
327
  if message:
257
328
  print(message, file=sys.stderr)
329
+ _print_help_url(args)
258
330
  if headers and rows:
259
331
  _print_table(headers, rows)
332
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
260
333
 
261
334
  if not button_specs:
262
335
  input("Press Enter to continue...")
@@ -293,6 +366,8 @@ class ConsoleBackend(GuiBackend):
293
366
  print(message, file=sys.stderr)
294
367
  print("(Interactive map requires a GUI backend; showing tabular data.)", file=sys.stderr)
295
368
  _print_table(headers, rows)
369
+ if rows:
370
+ print(f" {_row_count_text(len(rows))}", file=sys.stderr)
296
371
 
297
372
  if lat_col and lon_col and headers and rows:
298
373
  try:
@@ -334,6 +409,7 @@ class ConsoleBackend(GuiBackend):
334
409
  message = args.get("message", "")
335
410
  if message:
336
411
  print(message, file=sys.stderr)
412
+ _print_help_url(args)
337
413
  username = input("Username: ").strip()
338
414
  import getpass
339
415
 
@@ -344,6 +420,7 @@ class ConsoleBackend(GuiBackend):
344
420
  message = args.get("message", "")
345
421
  if message:
346
422
  print(message, file=sys.stderr)
423
+ _print_help_url(args)
347
424
  db_types = {
348
425
  "p": "PostgreSQL",
349
426
  "s": "SQL Server",