execsql2 2.22.2__tar.gz → 2.22.4__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.
- {execsql2-2.22.2 → execsql2-2.22.4}/.github/workflows/ci-cd.yml +1 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/.gitignore +3 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/CHANGELOG.md +18 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/PKG-INFO +3 -3
- {execsql2-2.22.2 → execsql2-2.22.4}/README.md +1 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/about/divergence.md +3 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/formatter.md +1 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/reference/configuration.md +1 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/reference/metacommands.md +13 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/pyproject.toml +2 -2
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/format.py +12 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/gui/base.py +56 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/gui/console.py +10 -33
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/gui/tui.py +45 -15
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/control.py +26 -0
- execsql2-2.22.4/tests/gui/test_format_table.py +85 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/test_tui_pilot.py +90 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_extended.py +125 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_format.py +96 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/uv.lock +1 -1
- {execsql2-2.22.2 → execsql2-2.22.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/.github/dependabot.yml +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/.pre-commit-config.yaml +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/.python-version +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/.readthedocs.yaml +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/CONTRIBUTING.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/LICENSE.txt +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/NOTICE +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/SECURITY.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/about/contributors.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/about/copyright.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/api/cli.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/api/db.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/api/exporters.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/api/importers.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/api/index.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/api/metacommands.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/dev/architecture.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/dev/releasing.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/getting-started/installation.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/debugging.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/documentation.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/encoding.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/examples.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/logging.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/usage.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/actions.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/actions2.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/checkboxes.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/connect.b64 +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/connect.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/create_conf.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/entry_form.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/execsql_console.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/fatals.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/logo_small.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/unmatched.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/index.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/reference/security.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/extras/plugin-template/README.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/extras/plugin-template/pyproject.toml +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/extras/plugin-template/tests/test_plugin.py.example +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/justfile +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/scripts/benchmarks/README.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/scripts/benchmarks/results/.gitignore +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/scripts/benchmarks/run_benchmarks.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/__main__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/api.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/cli/help.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/cli/run.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/config.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/data/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/data/execsql.conf.template +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/access.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/base.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/factory.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exceptions.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/importers/base.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/importers/json.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/metacommands/upsert.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/models.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/parser.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/plugins.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/py.typed +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/script/ast.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/script/control.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/script/engine.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/script/executor.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/script/parser.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/script/variables.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/state.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/state.pyi +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/types.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/README.md +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/config_settings.sqlite +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/make_config_db.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/md_compare.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/md_glossary.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/md_upsert.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/pg_compare.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/pg_glossary.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/pg_upsert.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/script_template.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/ss_compare.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/ss_glossary.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/templates/ss_upsert.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/cli/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/cli/test_cli.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/cli/test_lint.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/cli/test_ping.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/cli/test_profile.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/conftest.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_access_windows.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_base.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_db_adapters_mocked.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_db_adapters_mocked_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_dsn.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_factory.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_mysql_case_folding.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_mysql_inprocess.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_postgres.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_postgres_inprocess.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/db/test_sqlserver_inprocess.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_base.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_db.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_html_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_json.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_json_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_latex_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_ods_export.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_pretty_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_raw_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_templates_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_values_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/test_backends.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/test_backends_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/test_compare_stats.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/test_compute_row_diffs.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/test_desktop_dialogs.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/test_tui_pilot_complex.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/gui/test_utils_gui_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/importers/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/importers/test_base_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/importers/test_csv_edge_cases.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/importers/test_json_importer.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/integration/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/integration/conftest.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_conditions_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_pg_upsert.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_prompt.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/metacommands/test_show_scripts.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/audit_smoke.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/config_runtime.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/control_flow.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/error_handling.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/import_options.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/includes/helper.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/includes.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/io_formats.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/multi_db.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/predicates.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/scripts.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/smoke.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/template_export.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/timer.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/transactions.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/write_create_table.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/scripts/test_sql_scripts.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/test_env_detection.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/test_expansion_bomb.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/test_import_regex_hardening.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/test_log_redaction.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/test_odbc_injection.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/test_path_containment.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/test_substitution_injection.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/security/test_zip_bomb.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_api.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_api_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_ast.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_ast_parser.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_bundled_templates.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_config.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_config_data.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_config_extended.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_debug_repl.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_engine.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_error_messages.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_exceptions.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_executor.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_executor_inprocess.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_mail.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_models.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_package.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_parser.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_parser_params.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_plugins.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_registry.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_script.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_state.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/test_types.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/__init__.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_auth.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_encodedfile_context.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_errors.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_regex.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_strings.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_timer.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.22.2 → execsql2-2.22.4}/zensical.toml +0 -0
|
@@ -323,7 +323,7 @@ jobs:
|
|
|
323
323
|
name: ${{ github.event.repository.name }}
|
|
324
324
|
path: dist/
|
|
325
325
|
- name: Publish to PyPI
|
|
326
|
-
uses: pypa/gh-action-pypi-publish@
|
|
326
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
327
327
|
with:
|
|
328
328
|
packages-dir: dist
|
|
329
329
|
skip-existing: true
|
|
@@ -13,6 +13,24 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.22.4] - 2026-08-27
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `HALT` with a message no longer opens a dialog and blocks forever in unattended runs. Without a GUI console and below `gui_level` 2 (no `-v2`/`-v3`), the message is written to the console and the script exits with the requested `EXIT_STATUS` — as the documentation has always described. Scripts that worked around this by pairing `WRITE` with a bare `HALT` can go back to `HALT MESSAGE`.
|
|
21
|
+
- `HALT … DISPLAY` now shows its table on the console in non-interactive runs instead of discarding it; an empty table appears as its column headings and a count of zero rows.
|
|
22
|
+
- Textual (`--gui-framework textual`) `HALT` dialogs now show the single OK button the metacommand asks for, rather than a Cancel/Continue pair whose Cancel exited with status 2 instead of the requested `EXIT_STATUS`. `HALT … DISPLAY` also renders its table in the dialog, and <kbd>Esc</kbd> no longer cancels it.
|
|
23
|
+
|
|
24
|
+
______________________________________________________________________
|
|
25
|
+
|
|
26
|
+
## [2.22.3] - 2026-07-23
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `execsql-format` no longer sends a completed dollar-quoted PL/pgSQL body to sqlglot. A `CREATE FUNCTION … $$ … $$;` or `DO $$ … $$;` whose quote closed before the block was flushed was reformatted (signature exploded, `LANGUAGE … AS` reordered, body types uppercased) and drifted — gaining one space of indentation on every run, so `--check` never converged. Such statements are now kept verbatim.
|
|
31
|
+
|
|
32
|
+
______________________________________________________________________
|
|
33
|
+
|
|
16
34
|
## [2.22.2] - 2026-07-13
|
|
17
35
|
|
|
18
36
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.22.
|
|
3
|
+
Version: 2.22.4
|
|
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
|
|
@@ -384,7 +384,7 @@ execsql-format --no-sql --in-place scripts/
|
|
|
384
384
|
```yaml
|
|
385
385
|
repos:
|
|
386
386
|
- repo: https://github.com/geocoug/execsql
|
|
387
|
-
rev: v2.22.
|
|
387
|
+
rev: v2.22.4
|
|
388
388
|
hooks:
|
|
389
389
|
- id: execsql-format
|
|
390
390
|
```
|
|
@@ -111,6 +111,7 @@ Formatter correctness notes added in the 2.19.x line:
|
|
|
111
111
|
- **2.19.1** — All four substitution-variable forms (`!!var!!`, `!'!var!'!`, `!"!var!"!`, and the deferred form `!{var}!`) are hidden from the sqlglot pass. Previously, `!'!var!'!` and `!"!var!"!` were parsed as `NOT` operators and rewritten — e.g. `!'!myvar!'!` → `NOT NOT '!myvar!'`. Scripts using quoted substitution variables are now safe to run through `execsql-format`.
|
|
112
112
|
- **2.19.2** — The published pre-commit hook declares `sqlglot` as an isolated-env dependency. The 2.19.0 move of `sqlglot` to the `[formatter]` extra meant the hook env (which installs the bare package) crashed with `ModuleNotFoundError: No module named 'sqlglot'` on first run. After upgrading, downstream users should run `pre-commit clean && pre-commit install --install-hooks` once to rebuild the env.
|
|
113
113
|
- **Tagged dollar quotes** — Tagged PL/pgSQL function bodies (`$body$ … $body$`, `$func$ … $func$`, any `$tag$`) are now recognized and skipped over by the formatter. Previously the tracker only saw untagged `$$`, so tagged function bodies were sent to sqlglot which mangled `IF / END IF / LOOP / RETURN`.
|
|
114
|
+
- **Closed dollar quotes** — Any statement that *contains* a dollar-quoted region is now opaque to sqlglot, tracked with a sticky flag rather than the parser's live `in_dollar_quote` state. A complete `CREATE FUNCTION … $$ … $$;` or `DO $$ … $$;` had its quote already closed by the time the block was flushed, so the guard passed and the whole body was reformatted — and, because dropped comments were re-indented onto the nearest output line each pass, the body drifted (gaining a space of indentation) on every run, so `--check` never converged. A side effect is that a single-line `SELECT $$literal$$;` also skips the sqlglot pass (no keyword uppercasing), which is the safe, conservative behavior.
|
|
114
115
|
|
|
115
116
|
### GUI
|
|
116
117
|
|
|
@@ -314,6 +315,8 @@ These are behavioral changes driven by security or correctness issues in the ups
|
|
|
314
315
|
| DB-export type inference overrides source types | `DataTable`/`Column` inferred column types from the *string shape* of values — correct for CSV import (everything is `str`) but wrong for DB-query exports where the driver returns typed Python objects. A text column with values like `"2026-06-30 10:07:20 PM"` was inferred as `TIMESTAMP`, causing DuckDB bind errors. SQLite, DuckDB, HDF5, and JSON-schema exports now pass `infer_strings=False` to `DataTable`: string values are classified as text types only, while native Python objects (`Decimal`, `datetime`, etc.) still run through the full accumulator set for accurate schema inference. |
|
|
315
316
|
| `EXPORT FORMAT SQLITE` rejects `decimal.Decimal` | SQLite's Python adapter does not accept `Decimal` in `executemany`, raising `ProgrammingError`. SQLite exports now convert `Decimal` values to their exact string representation before binding; SQLite's NUMERIC affinity stores them without loss of precision. |
|
|
316
317
|
| Unsupported Python objects crash native exports | `EXPORT FORMAT SQLITE` and `EXPORT FORMAT DUCKDB` raised bind errors when the DB driver returned user-defined Python objects (e.g. PostGIS geometry). Both exporters now stringify any object whose type is not in the adapter's safe-binding set, preserving values as their `str()` form instead of crashing. |
|
|
318
|
+
| `HALT` with a message hangs unattended runs | The message-bearing `HALT` patterns are registered after the general one and so take priority, meaning `x_halt`'s `gui_level > 1` guard was never reached and every `HALT MESSAGE` opened a modal dialog — blocking cron and CI runs indefinitely at `gui_level` 0, contrary to the documented behavior. The guard now lives in `x_halt_msg`: without a GUI console and below `gui_level` 2, the message and any `DISPLAY` rowset are written to the error output and the script exits with the requested status. An empty `DISPLAY` table renders as headings plus a zero row count rather than nothing. |
|
|
319
|
+
| Textual `HALT` dialog ignored `button_list` | The Textual `MsgScreen` hardcoded a Cancel/Continue pair and dropped the caller's `column_headers`/`rowset`, so a `HALT` asking for a single OK button offered a Cancel that exited with status 2 instead of the requested `EXIT_STATUS`, and `HALT ... DISPLAY` showed no table. `MsgScreen` now honors `button_list` and `no_cancel` (which also suppresses <kbd>Esc</kbd>) and renders the rowset, matching the Tkinter backend. |
|
|
317
320
|
|
|
318
321
|
______________________________________________________________________
|
|
319
322
|
|
|
@@ -247,7 +247,7 @@ The section and property names that may be used in a configuration file are list
|
|
|
247
247
|
|
|
248
248
|
- 0: Do not use any optional GUI dialogs.
|
|
249
249
|
- 1: Use GUI dialogs for password prompts and for the [PAUSE](metacommands.md#pause) metacommand.
|
|
250
|
-
- 2: Also use a GUI dialog if a message is included with the [HALT](metacommands.md#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.
|
|
250
|
+
- 2: Also use a GUI dialog if a message is included with the [HALT](metacommands.md#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. At levels 0 and 1, a HALT message is written to the console instead, and the script exits without waiting for input.
|
|
251
251
|
- 3: Additionally, open a GUI console when *execsql* starts.
|
|
252
252
|
|
|
253
253
|
`gui_framework` { #gui_framework }
|
|
@@ -1361,11 +1361,23 @@ HALT [[MESSAGE] "<error_message>" [TEE TO <outfile>]]
|
|
|
1361
1361
|
[DISPLAY <table_or_view>] [EXIT_STATUS <n>]
|
|
1362
1362
|
```
|
|
1363
1363
|
|
|
1364
|
-
Script processing is halted, and the *execsql* program terminates. If
|
|
1364
|
+
Script processing is halted, and the *execsql* program terminates. If the TEE clause is used, the error message will be appended to the specified file. If an EXIT_STATUS value is specified, the [system exit status](https://en.wikipedia.org/wiki/Exit_status) is set to that value, otherwise, the system exit status is set to 3.
|
|
1365
|
+
|
|
1366
|
+
The error message and the DISPLAY table are reported in a dialog only when the run is interactive — that is, when the "-v2" or "-v3" option is used (or [`gui_level`](configuration.md#gui_level) is set to 2 or 3), or a GUI console is already open. Otherwise the message is written to the console, followed by the DISPLAY table as a plain-text table with a row count, and the script exits immediately. An unattended run therefore fails with the requested exit status rather than waiting on a dialog that nobody is present to dismiss.
|
|
1367
|
+
|
|
1368
|
+
A DISPLAY table with no rows is shown on the console as its column headings and a count of zero rows; in a dialog, an empty table is omitted.
|
|
1369
|
+
|
|
1370
|
+
```sql
|
|
1371
|
+
-- Fail the run and print the offending rows, in a cron job or in a GUI session.
|
|
1372
|
+
-- !x! if(hasrows(bad_rows)) { halt message "Validation failed." display bad_rows exit_status 7 }
|
|
1373
|
+
```
|
|
1365
1374
|
|
|
1366
1375
|
!!! warning
|
|
1367
1376
|
A backward-incompatible change to HALT MESSAGE was made in version 1.26.1.0 (2018-06-13): the default exit status was changed from 2 to 3.
|
|
1368
1377
|
|
|
1378
|
+
!!! note
|
|
1379
|
+
Through execsql 2.22.3, a HALT that carried a message always opened a dialog, even at `gui_level` 0, which caused unattended runs to block indefinitely. Scripts that worked around this by writing the message with [WRITE](#write) before a bare `HALT` do not need to change, but they can now use `HALT MESSAGE` directly.
|
|
1380
|
+
|
|
1369
1381
|
The text to be written may be enclosed in double quotes (as shown above), or in single quotes, matching square brackets, backticks, tildes, or hash marks (#).
|
|
1370
1382
|
|
|
1371
1383
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.22.
|
|
7
|
+
version = "2.22.4"
|
|
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" }
|
|
@@ -178,7 +178,7 @@ skip-magic-trailing-comma = false
|
|
|
178
178
|
line-ending = "auto"
|
|
179
179
|
|
|
180
180
|
[tool.bumpversion]
|
|
181
|
-
current_version = "2.22.
|
|
181
|
+
current_version = "2.22.4"
|
|
182
182
|
commit = true
|
|
183
183
|
tag = true
|
|
184
184
|
tag_name = "v{new_version}"
|
|
@@ -642,6 +642,12 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
|
|
|
642
642
|
# "body" for `$body$`, etc.). Nested markers with a different tag
|
|
643
643
|
# are ignored — only a matching close marker re-opens us.
|
|
644
644
|
current_dq_tag: str | None = None
|
|
645
|
+
# Sticky flag: True once the current accumulator has seen ANY dollar-quote
|
|
646
|
+
# marker. `in_dollar_quote` alone is False by the time a complete
|
|
647
|
+
# `CREATE FUNCTION ... $$ ... $$;` is flushed (the closing `$$` reset it),
|
|
648
|
+
# which would wrongly hand the PL/pgSQL body to sqlglot. This flag keeps the
|
|
649
|
+
# whole containing statement opaque to sqlglot. Reset in flush_sql().
|
|
650
|
+
sql_acc_contains_dollar_quote = False
|
|
645
651
|
in_block_comment = False
|
|
646
652
|
# Track whether we are inside an open SQL statement (last SQL line
|
|
647
653
|
# did not end with ';'). Blank lines mid-statement should NOT flush
|
|
@@ -655,15 +661,19 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
|
|
|
655
661
|
|
|
656
662
|
def flush_sql() -> None:
|
|
657
663
|
nonlocal in_dollar_quote, current_dq_tag, in_sql_statement
|
|
664
|
+
nonlocal sql_acc_contains_dollar_quote
|
|
658
665
|
if sql_acc:
|
|
659
666
|
# If any line in the accumulated block is inside a $$-delimited
|
|
660
667
|
# region, skip sqlglot formatting entirely. PL/pgSQL function
|
|
661
668
|
# bodies contain IF/END IF, LOOP, RETURN, etc. that sqlglot does
|
|
662
669
|
# not understand and will corrupt (e.g., rewriting to COMMIT).
|
|
663
|
-
|
|
670
|
+
# `sql_acc_contains_dollar_quote` catches the case where the region
|
|
671
|
+
# already closed (`... $$;`) so `in_dollar_quote` is False at flush.
|
|
672
|
+
safe_for_sqlglot = use_sql and not in_dollar_quote and not sql_acc_contains_dollar_quote
|
|
664
673
|
output.extend(format_sql_block(sql_acc, depth, indent, safe_for_sqlglot, leading_comma=leading_comma))
|
|
665
674
|
sql_acc.clear()
|
|
666
675
|
in_sql_statement = False
|
|
676
|
+
sql_acc_contains_dollar_quote = False
|
|
667
677
|
|
|
668
678
|
for raw_line in source.expandtabs(4).splitlines():
|
|
669
679
|
stripped_line = raw_line.strip()
|
|
@@ -727,6 +737,7 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
|
|
|
727
737
|
# PL/pgSQL. Walk every dollar-quote marker on the line; toggle
|
|
728
738
|
# state only when we hit the matching open or close.
|
|
729
739
|
for m in _DOLLAR_QUOTE_RE.finditer(raw_line):
|
|
740
|
+
sql_acc_contains_dollar_quote = True
|
|
730
741
|
tag = m.group(1) or ""
|
|
731
742
|
if not in_dollar_quote:
|
|
732
743
|
in_dollar_quote = True
|
|
@@ -9,11 +9,66 @@ from typing import TYPE_CHECKING, Any
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
pass
|
|
11
11
|
|
|
12
|
-
__all__ = ["DiffResult", "GuiBackend", "compare_stats", "compute_row_diffs"]
|
|
12
|
+
__all__ = ["DiffResult", "GuiBackend", "compare_stats", "compute_row_diffs", "format_table", "row_count_text"]
|
|
13
13
|
|
|
14
14
|
DIFF_MARKER = "● "
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Plain-text table rendering (shared by the console backend and by
|
|
19
|
+
# non-interactive metacommand fallbacks such as HALT ... DISPLAY)
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def row_count_text(n: int) -> str:
|
|
24
|
+
"""Return a human-readable row count string, e.g. ``'3 rows'`` or ``'1 row'``."""
|
|
25
|
+
return f"{n:,} row{'s' if n != 1 else ''}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def format_table(
|
|
29
|
+
headers: list,
|
|
30
|
+
rows: list,
|
|
31
|
+
row_states: list[str] | None = None,
|
|
32
|
+
changed_cols: list[set[str]] | None = None,
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Render *headers* and *rows* as a plain-text table.
|
|
35
|
+
|
|
36
|
+
Columns are left-aligned and padded to the widest value. When *row_states*
|
|
37
|
+
and *changed_cols* are provided, cells that differ in ``"changed"`` rows are
|
|
38
|
+
prefixed with the diff marker (``"● "``) and the marker width is included in
|
|
39
|
+
the column sizing.
|
|
40
|
+
|
|
41
|
+
Returns an empty string when *headers* is empty. The result has no trailing
|
|
42
|
+
newline.
|
|
43
|
+
"""
|
|
44
|
+
if not headers:
|
|
45
|
+
return ""
|
|
46
|
+
headers_str = [str(h) for h in headers]
|
|
47
|
+
|
|
48
|
+
# Pre-compute display values so column widths account for markers.
|
|
49
|
+
display_rows: list[list[str]] = []
|
|
50
|
+
for ridx, row in enumerate(rows):
|
|
51
|
+
cells: list[str] = []
|
|
52
|
+
state = row_states[ridx] if row_states and ridx < len(row_states) else ""
|
|
53
|
+
diff_set = changed_cols[ridx] if changed_cols and ridx < len(changed_cols) else set()
|
|
54
|
+
for ci, cell in enumerate(row):
|
|
55
|
+
val = str(cell) if cell is not None else ""
|
|
56
|
+
if state == "changed" and ci < len(headers_str) and headers_str[ci] in diff_set:
|
|
57
|
+
val = f"{DIFF_MARKER}{val}"
|
|
58
|
+
cells.append(val)
|
|
59
|
+
display_rows.append(cells)
|
|
60
|
+
|
|
61
|
+
col_widths = [len(h) for h in headers_str]
|
|
62
|
+
for cells in display_rows:
|
|
63
|
+
for i, cell in enumerate(cells):
|
|
64
|
+
col_widths[i] = max(col_widths[i], len(cell))
|
|
65
|
+
fmt = " " + " ".join(f"{{:<{w}}}" for w in col_widths)
|
|
66
|
+
sep = " " + " ".join("-" * w for w in col_widths)
|
|
67
|
+
lines = [fmt.format(*headers_str), sep]
|
|
68
|
+
lines.extend(fmt.format(*cells) for cells in display_rows)
|
|
69
|
+
return "\n".join(lines)
|
|
70
|
+
|
|
71
|
+
|
|
17
72
|
# ---------------------------------------------------------------------------
|
|
18
73
|
# Value comparison helpers
|
|
19
74
|
# ---------------------------------------------------------------------------
|
|
@@ -11,7 +11,13 @@ import sys
|
|
|
11
11
|
import time
|
|
12
12
|
from typing import Any, cast
|
|
13
13
|
|
|
14
|
-
from execsql.gui.base import
|
|
14
|
+
from execsql.gui.base import (
|
|
15
|
+
GuiBackend,
|
|
16
|
+
compare_stats as _compare_stats,
|
|
17
|
+
compute_row_diffs,
|
|
18
|
+
format_table,
|
|
19
|
+
row_count_text as _row_count_text,
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
__all__ = ["ConsoleBackend"]
|
|
17
23
|
|
|
@@ -23,11 +29,6 @@ def _print_help_url(args: dict) -> None:
|
|
|
23
29
|
print(f" Help: {url}", file=sys.stderr)
|
|
24
30
|
|
|
25
31
|
|
|
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
|
-
|
|
31
32
|
def _print_table(
|
|
32
33
|
headers: list,
|
|
33
34
|
rows: list,
|
|
@@ -42,33 +43,9 @@ def _print_table(
|
|
|
42
43
|
"""
|
|
43
44
|
if file is None:
|
|
44
45
|
file = sys.stderr
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Pre-compute display values so column widths account for markers.
|
|
50
|
-
display_rows: list[list[str]] = []
|
|
51
|
-
for ridx, row in enumerate(rows):
|
|
52
|
-
cells: list[str] = []
|
|
53
|
-
state = row_states[ridx] if row_states and ridx < len(row_states) else ""
|
|
54
|
-
diff_set = changed_cols[ridx] if changed_cols and ridx < len(changed_cols) else set()
|
|
55
|
-
for ci, cell in enumerate(row):
|
|
56
|
-
val = str(cell) if cell is not None else ""
|
|
57
|
-
if state == "changed" and ci < len(headers_str) and headers_str[ci] in diff_set:
|
|
58
|
-
val = f"{DIFF_MARKER}{val}"
|
|
59
|
-
cells.append(val)
|
|
60
|
-
display_rows.append(cells)
|
|
61
|
-
|
|
62
|
-
col_widths = [len(h) for h in headers_str]
|
|
63
|
-
for cells in display_rows:
|
|
64
|
-
for i, cell in enumerate(cells):
|
|
65
|
-
col_widths[i] = max(col_widths[i], len(cell))
|
|
66
|
-
fmt = " " + " ".join(f"{{:<{w}}}" for w in col_widths)
|
|
67
|
-
sep = " " + " ".join("-" * w for w in col_widths)
|
|
68
|
-
print(fmt.format(*headers_str), file=file)
|
|
69
|
-
print(sep, file=file)
|
|
70
|
-
for cells in display_rows:
|
|
71
|
-
print(fmt.format(*cells), file=file)
|
|
46
|
+
rendered = format_table(headers, rows, row_states=row_states, changed_cols=changed_cols)
|
|
47
|
+
if rendered:
|
|
48
|
+
print(rendered, file=file)
|
|
72
49
|
|
|
73
50
|
|
|
74
51
|
def _prompt_buttons(button_list: list) -> int | None:
|
|
@@ -89,14 +89,18 @@ def _help_button(url: str | None) -> Button | None:
|
|
|
89
89
|
return None
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def _button_row(button_list: list) -> list[Button]:
|
|
92
|
+
def _button_row(button_list: list, no_cancel: bool = False) -> list[Button]:
|
|
93
93
|
"""Create Button widgets from a button_list of (label, value, key?) tuples.
|
|
94
94
|
|
|
95
|
-
A Cancel button (id ``btn_cancel_exit``) is
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
A Cancel button (id ``btn_cancel_exit``) is prepended so it appears on the
|
|
96
|
+
left while the primary action button sits on the far right. Entries in
|
|
97
|
+
*button_list* with a falsy value (0 or None) are treated as the cancel
|
|
98
|
+
label source and are otherwise excluded from the numbered buttons so that
|
|
99
|
+
``btn_N`` indices stay aligned with ``button_list`` positions.
|
|
100
|
+
|
|
101
|
+
When *no_cancel* is set, the Cancel button is omitted — dialogs such as
|
|
102
|
+
HALT have no meaningful cancel action, and cancelling one would exit with
|
|
103
|
+
the generic status 2 rather than the status the caller asked for.
|
|
100
104
|
"""
|
|
101
105
|
cancel_label = "Cancel"
|
|
102
106
|
primary_buttons = []
|
|
@@ -107,6 +111,8 @@ def _button_row(button_list: list) -> list[Button]:
|
|
|
107
111
|
else:
|
|
108
112
|
variant: Literal["primary", "default"] = "primary" if i == 0 else "default"
|
|
109
113
|
primary_buttons.append(Button(label, id=f"btn_{i}", variant=variant))
|
|
114
|
+
if no_cancel:
|
|
115
|
+
return primary_buttons
|
|
110
116
|
return [Button(cancel_label, id="btn_cancel_exit", variant="warning")] + primary_buttons
|
|
111
117
|
|
|
112
118
|
|
|
@@ -182,7 +188,14 @@ class _BaseDialog(ModalScreen):
|
|
|
182
188
|
return self._result
|
|
183
189
|
|
|
184
190
|
def action_cancel(self) -> None:
|
|
185
|
-
"""Dismiss the dialog as cancelled (triggered by Escape key).
|
|
191
|
+
"""Dismiss the dialog as cancelled (triggered by Escape key).
|
|
192
|
+
|
|
193
|
+
Ignored for dialogs that pass ``no_cancel`` (e.g. HALT), which have no
|
|
194
|
+
meaningful cancel action. This matches the Tkinter backend, which does
|
|
195
|
+
not bind ``<Escape>`` to cancellation in that case.
|
|
196
|
+
"""
|
|
197
|
+
if self.args.get("no_cancel", False):
|
|
198
|
+
return
|
|
186
199
|
self._result = {"button": None, "cancelled": True}
|
|
187
200
|
self.dismiss(self._result)
|
|
188
201
|
|
|
@@ -210,31 +223,48 @@ class _BaseDialog(ModalScreen):
|
|
|
210
223
|
|
|
211
224
|
|
|
212
225
|
class MsgScreen(_BaseDialog):
|
|
213
|
-
"""
|
|
226
|
+
"""Message dialog with an optional data table.
|
|
227
|
+
|
|
228
|
+
Honours the caller's ``button_list`` and ``no_cancel`` arguments, and
|
|
229
|
+
renders ``column_headers``/``rowset`` when supplied (as the HALT
|
|
230
|
+
metacommand's DISPLAY clause does), matching the Tkinter MsgDialog.
|
|
231
|
+
"""
|
|
214
232
|
|
|
215
233
|
BINDINGS = [
|
|
216
234
|
*_BaseDialog.BINDINGS,
|
|
217
235
|
Binding("enter", "submit", "Continue", show=True),
|
|
218
236
|
]
|
|
219
237
|
|
|
238
|
+
def _button_list(self) -> list:
|
|
239
|
+
return cast(list, self.args.get("button_list") or [("Continue", 1, "<Return>")])
|
|
240
|
+
|
|
220
241
|
def compose(self) -> ComposeResult:
|
|
221
242
|
title = self.args.get("title", "Message")
|
|
222
243
|
message = self.args.get("message", "")
|
|
244
|
+
headers = self.args.get("column_headers")
|
|
245
|
+
rows = self.args.get("rowset")
|
|
223
246
|
with Container(id="dialog"):
|
|
224
247
|
yield Label(title, id="title")
|
|
225
248
|
yield Static(message, id="message")
|
|
249
|
+
if headers and rows:
|
|
250
|
+
with ScrollableContainer(id="table_container"):
|
|
251
|
+
yield _make_table_widget("msg_table", headers, rows)
|
|
252
|
+
yield Static(_row_count_text(len(rows)), classes="row-count")
|
|
226
253
|
with Horizontal(id="buttons"):
|
|
227
|
-
yield
|
|
228
|
-
yield Button("Continue", id="btn_close", variant="primary")
|
|
254
|
+
yield from _button_row(self._button_list(), no_cancel=self.args.get("no_cancel", False))
|
|
229
255
|
|
|
230
256
|
def action_submit(self) -> None:
|
|
231
|
-
"""
|
|
232
|
-
self.
|
|
233
|
-
|
|
257
|
+
"""Dismiss with the first non-cancel button value (triggered by Enter)."""
|
|
258
|
+
for btn in self._button_list():
|
|
259
|
+
if btn[1]:
|
|
260
|
+
self._result = {"button": btn[1]}
|
|
261
|
+
self.dismiss(self._result)
|
|
262
|
+
return
|
|
234
263
|
|
|
235
264
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
236
|
-
|
|
237
|
-
|
|
265
|
+
btn_id = event.button.id
|
|
266
|
+
if btn_id and btn_id.startswith("btn_") and btn_id[4:].isdigit():
|
|
267
|
+
self._result = {"button": self._button_list()[int(btn_id[4:])][1]}
|
|
238
268
|
self.dismiss(self._result)
|
|
239
269
|
|
|
240
270
|
|
|
@@ -33,6 +33,7 @@ from execsql.exceptions import ErrInfo
|
|
|
33
33
|
from typing import Any
|
|
34
34
|
|
|
35
35
|
import execsql.state as _state
|
|
36
|
+
from execsql.gui.base import format_table, row_count_text
|
|
36
37
|
from execsql.script import current_script_line
|
|
37
38
|
from execsql.utils.errors import exit_now
|
|
38
39
|
from execsql.utils.fileio import EncodedFile, check_dir
|
|
@@ -214,6 +215,14 @@ def x_wait_until(**kwargs: Any) -> None:
|
|
|
214
215
|
|
|
215
216
|
# x_halt_msg is also needed by x_halt - define it here
|
|
216
217
|
def x_halt_msg(**kwargs: Any) -> None:
|
|
218
|
+
"""Halt the script, reporting *errmsg* in a dialog or on the console.
|
|
219
|
+
|
|
220
|
+
The dialog is used only when the run is interactive — either a GUI console
|
|
221
|
+
is already running, or ``gui_level`` is greater than 1. Otherwise the
|
|
222
|
+
message (and any ``DISPLAY`` rowset) is written to the error output and the
|
|
223
|
+
script exits, so that unattended runs fail instead of blocking forever on a
|
|
224
|
+
modal nobody can dismiss.
|
|
225
|
+
"""
|
|
217
226
|
import queue as _queue
|
|
218
227
|
|
|
219
228
|
errmsg = kwargs["errmsg"]
|
|
@@ -242,6 +251,23 @@ def x_halt_msg(**kwargs: Any) -> None:
|
|
|
242
251
|
headers, rows = db.select_data(sql)
|
|
243
252
|
else:
|
|
244
253
|
headers, rows = None, None
|
|
254
|
+
|
|
255
|
+
# Non-interactive runs (no GUI console, gui_level 0 or 1) must not block on
|
|
256
|
+
# a modal dialog: report on the console and exit instead. This mirrors the
|
|
257
|
+
# guard in x_halt, which the message-bearing HALT forms never reach because
|
|
258
|
+
# their metacommand patterns are registered later and so take priority.
|
|
259
|
+
if not (gui_console_isrunning() or conf.gui_level > 1):
|
|
260
|
+
if errmsg:
|
|
261
|
+
_state.output.write_err(errmsg)
|
|
262
|
+
if headers:
|
|
263
|
+
# Unlike the dialog, render an empty rowset too — a HALT diagnostic
|
|
264
|
+
# showing zero rows is more useful than showing nothing at all.
|
|
265
|
+
_state.output.write_err(format_table(headers, rows or []))
|
|
266
|
+
_state.output.write_err(row_count_text(len(rows or [])))
|
|
267
|
+
_state.exec_log.log_exit_halt(*current_script_line(), msg=errmsg)
|
|
268
|
+
exit_now(errlevel, None)
|
|
269
|
+
return
|
|
270
|
+
|
|
245
271
|
enable_gui()
|
|
246
272
|
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
247
273
|
gui_args = {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Tests for the shared plain-text table renderer in execsql.gui.base.
|
|
2
|
+
|
|
3
|
+
``format_table`` backs both the console GUI backend's ``_print_table`` and the
|
|
4
|
+
non-interactive HALT fallback in ``x_halt_msg``, so its output is user-visible
|
|
5
|
+
in unattended runs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from execsql.gui.base import DIFF_MARKER, format_table, row_count_text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestRowCountText:
|
|
14
|
+
def test_plural(self):
|
|
15
|
+
assert row_count_text(2) == "2 rows"
|
|
16
|
+
|
|
17
|
+
def test_singular(self):
|
|
18
|
+
assert row_count_text(1) == "1 row"
|
|
19
|
+
|
|
20
|
+
def test_zero_is_plural(self):
|
|
21
|
+
assert row_count_text(0) == "0 rows"
|
|
22
|
+
|
|
23
|
+
def test_thousands_separator(self):
|
|
24
|
+
assert row_count_text(12345) == "12,345 rows"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestFormatTable:
|
|
28
|
+
def test_empty_headers_yield_empty_string(self):
|
|
29
|
+
assert format_table([], [("a",)]) == ""
|
|
30
|
+
|
|
31
|
+
def test_headers_only(self):
|
|
32
|
+
"""A zero-row rowset still renders headers and a separator."""
|
|
33
|
+
out = format_table(["id", "reason"], [])
|
|
34
|
+
lines = out.splitlines()
|
|
35
|
+
assert len(lines) == 2
|
|
36
|
+
assert "id" in lines[0] and "reason" in lines[0]
|
|
37
|
+
assert set(lines[1].strip()) == {"-", " "}
|
|
38
|
+
|
|
39
|
+
def test_columns_pad_to_widest_value(self):
|
|
40
|
+
out = format_table(["id"], [("1",), ("longer",)])
|
|
41
|
+
lines = out.splitlines()
|
|
42
|
+
# Header, separator, and both data rows share one column width.
|
|
43
|
+
assert len({len(line) for line in lines}) == 1
|
|
44
|
+
|
|
45
|
+
def test_none_renders_as_empty(self):
|
|
46
|
+
out = format_table(["a", "b"], [(None, "x")])
|
|
47
|
+
assert "None" not in out
|
|
48
|
+
assert "x" in out
|
|
49
|
+
|
|
50
|
+
def test_non_string_values_are_stringified(self):
|
|
51
|
+
out = format_table(["n"], [(42,)])
|
|
52
|
+
assert "42" in out
|
|
53
|
+
|
|
54
|
+
def test_no_trailing_newline(self):
|
|
55
|
+
out = format_table(["a"], [("b",)])
|
|
56
|
+
assert not out.endswith("\n")
|
|
57
|
+
|
|
58
|
+
def test_diff_marker_applied_to_changed_cells(self):
|
|
59
|
+
out = format_table(
|
|
60
|
+
["k", "v"],
|
|
61
|
+
[("1", "old")],
|
|
62
|
+
row_states=["changed"],
|
|
63
|
+
changed_cols=[{"v"}],
|
|
64
|
+
)
|
|
65
|
+
assert f"{DIFF_MARKER}old" in out
|
|
66
|
+
assert f"{DIFF_MARKER}1" not in out
|
|
67
|
+
|
|
68
|
+
def test_diff_marker_width_included_in_column_sizing(self):
|
|
69
|
+
"""Marker width must not push the marked row wider than the others."""
|
|
70
|
+
out = format_table(
|
|
71
|
+
["v"],
|
|
72
|
+
[("aaa",), ("aaa",)],
|
|
73
|
+
row_states=["changed", "unchanged"],
|
|
74
|
+
changed_cols=[{"v"}, set()],
|
|
75
|
+
)
|
|
76
|
+
assert len({len(line) for line in out.splitlines()}) == 1
|
|
77
|
+
|
|
78
|
+
def test_unchanged_rows_are_unmarked(self):
|
|
79
|
+
out = format_table(
|
|
80
|
+
["v"],
|
|
81
|
+
[("x",)],
|
|
82
|
+
row_states=["unchanged"],
|
|
83
|
+
changed_cols=[{"v"}],
|
|
84
|
+
)
|
|
85
|
+
assert DIFF_MARKER not in out
|