execsql2 2.9.0__tar.gz → 2.10.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.
- {execsql2-2.9.0 → execsql2-2.10.1}/CHANGELOG.md +20 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/PKG-INFO +1 -1
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/about/divergence.md +15 -5
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/reference/metacommands.md +94 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +2 -2
- {execsql2-2.9.0 → execsql2-2.10.1}/pyproject.toml +2 -2
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/__init__.py +3 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/conditions.py +148 -0
- execsql2-2.10.1/src/execsql/metacommands/debug_repl.py +227 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/dispatch.py +12 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/script/engine.py +5 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/state.py +9 -0
- execsql2-2.10.1/tests/metacommands/test_breakpoint.py +464 -0
- execsql2-2.10.1/tests/metacommands/test_row_count.py +496 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/uv.lock +1 -1
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/agents/dba.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/agents/herald.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/agents/inspector.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/agents/oracle.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/agents/patcher.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/agents/qa.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/agents/scribe.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/commands/code-oracle.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/commands/migrate.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/commands/review-changes.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/commands/test-module.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/commands/update-changelog.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/commands/where-is.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/project_context.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.claude/state/status.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.gitignore +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.pre-commit-config.yaml +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.python-version +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/.readthedocs.yaml +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/CLAUDE.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/CONTRIBUTING.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/LICENSE.txt +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/NOTICE +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/README.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/SECURITY.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/about/contributors.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/about/copyright.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/api/cli.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/api/db.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/api/exporters.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/api/importers.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/api/index.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/api/metacommands.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/dev/architecture.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/getting-started/installation.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/debugging.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/documentation.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/encoding.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/examples.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/formatter.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/logging.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/usage.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/actions.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/actions2.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/checkboxes.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/connect.b64 +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/connect.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/create_conf.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/entry_form.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/execsql_console.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/fatals.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/logo_small.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/unmatched.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/index.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/reference/configuration.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/reference/security.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/justfile +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/__main__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/cli/help.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/cli/run.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/config.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/constants.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/access.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/base.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/factory.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exceptions.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/format.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/gui/base.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/gui/console.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/importers/base.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/control.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/models.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/parser.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/py.typed +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/script/control.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/script/variables.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/types.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/README.md +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/config_settings.sqlite +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/execsql.conf +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/make_config_db.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/md_compare.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/md_glossary.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/md_upsert.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/pg_compare.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/pg_glossary.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/pg_upsert.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/script_template.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/ss_compare.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/ss_glossary.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/templates/ss_upsert.sql +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/cli/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/cli/test_cli.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/cli/test_lint.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/cli/test_ping.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/cli/test_profile.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/conftest.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/db/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/db/test_base.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/db/test_factory.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/db/test_postgres.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_base.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_db.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_json.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/gui/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/gui/test_backends.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/importers/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/integration/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/integration/conftest.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_config.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_config_data.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_constants.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_engine.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_exceptions.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_format.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_mail.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_models.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_package.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_parser.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_registry.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_script.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_state.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/test_types.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/__init__.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_auth.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_errors.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_regex.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_strings.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_timer.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.9.0 → execsql2-2.10.1}/zensical.toml +0 -0
|
@@ -13,6 +13,26 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.10.1] - 2026-04-01
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- BREAKPOINT variable lookup — `$logfile` was showing `(undefined)` because `SUB` stores keys without a sigil prefix. The debug REPL now strips `$`, `&`, `@`, `#`, `~` prefixes and retries when the exact name isn't found.
|
|
21
|
+
|
|
22
|
+
______________________________________________________________________
|
|
23
|
+
|
|
24
|
+
## [2.10.0] - 2026-04-01
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`BREAKPOINT` metacommand** — pauses script execution and drops into an interactive debug REPL. The prompt accepts `continue`/`c` to resume, `abort`/`q` to halt, `vars` to list substitution variables, `$VARNAME` to print a single variable, `SELECT ...;` to run ad-hoc SQL against the current database, `next`/`n` to step one statement at a time, `stack` to inspect the command-list stack, and `help` for a command summary. Silently skipped in non-TTY environments (CI, piped input) so automated pipelines are never blocked.
|
|
29
|
+
|
|
30
|
+
- **`step_mode` on `RuntimeContext`** — internal boolean flag set by the REPL's `next` command; the script engine re-enters the debug REPL after each subsequent statement while step mode is active.
|
|
31
|
+
|
|
32
|
+
- **`ROW_COUNT_GT(table, N)`**, **`ROW_COUNT_GTE(table, N)`**, **`ROW_COUNT_EQ(table, N)`**, **`ROW_COUNT_LT(table, N)`** conditional tests — compare the row count of any table or view against an integer threshold using `IF`, `ELSEIF`, or `ASSERT`. Each issues a `SELECT count(*)` query against the current database. An error is raised if the table does not exist or the threshold is not an integer.
|
|
33
|
+
|
|
34
|
+
______________________________________________________________________
|
|
35
|
+
|
|
16
36
|
## [2.9.0] - 2026-04-01
|
|
17
37
|
|
|
18
38
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.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: Repository, https://github.com/geocoug/execsql
|
|
6
6
|
Project-URL: Issues, https://github.com/geocoug/execsql/issues
|
|
@@ -39,11 +39,21 @@ ______________________________________________________________________
|
|
|
39
39
|
|
|
40
40
|
### Metacommands
|
|
41
41
|
|
|
42
|
-
| Metacommand | Description
|
|
43
|
-
| ---------------------- |
|
|
44
|
-
| `ASSERT` | Evaluate a condition and raise an error (halting the script) if it is false. Supports all IF conditions. Optional quoted failure message. Skipped in false IF blocks.
|
|
45
|
-
| `
|
|
46
|
-
| `CONFIG
|
|
42
|
+
| Metacommand | Description |
|
|
43
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
44
|
+
| `ASSERT` | Evaluate a condition and raise an error (halting the script) if it is false. Supports all IF conditions. Optional quoted failure message. Skipped in false IF blocks. |
|
|
45
|
+
| `BREAKPOINT` | Pause script execution and drop into an interactive debug REPL. Inspect variables, run ad-hoc SQL, and step through the script. Silently skipped in non-TTY (CI) environments. |
|
|
46
|
+
| `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
|
|
47
|
+
| `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file. |
|
|
48
|
+
|
|
49
|
+
### Conditional Tests
|
|
50
|
+
|
|
51
|
+
| Conditional | Description |
|
|
52
|
+
| ------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
53
|
+
| `ROW_COUNT_GT(table, N)` | True if the number of rows in *table* is strictly greater than *N* (integer). Queries `SELECT count(*)`. |
|
|
54
|
+
| `ROW_COUNT_GTE(table, N)` | True if the number of rows in *table* is greater than or equal to *N*. |
|
|
55
|
+
| `ROW_COUNT_EQ(table, N)` | True if the number of rows in *table* is exactly equal to *N*. |
|
|
56
|
+
| `ROW_COUNT_LT(table, N)` | True if the number of rows in *table* is strictly less than *N*. |
|
|
47
57
|
|
|
48
58
|
### Configuration Options
|
|
49
59
|
|
|
@@ -67,6 +67,7 @@ The `<condition>` supports all [conditional tests](metacommands.md#conditional_t
|
|
|
67
67
|
- `HAS_ROWS(<table>)` / `NOT HAS_ROWS(<table>)`
|
|
68
68
|
- `EQUAL(<val1>, <val2>)` / `NOT EQUAL(<val1>, <val2>)`
|
|
69
69
|
- `IS_GT(<val1>, <val2>)`, `IS_GTE(<val1>, <val2>)`, `IS_ZERO(<val>)`
|
|
70
|
+
- `ROW_COUNT_GT(<table>, N)`, `ROW_COUNT_GTE(<table>, N)`, `ROW_COUNT_EQ(<table>, N)`, `ROW_COUNT_LT(<table>, N)`
|
|
70
71
|
- `DBMS(<type>)`, `SCHEMA_EXISTS(<schema>)`, `VIEW_EXISTS(<view>)`
|
|
71
72
|
- Boolean combinators: `AND`, `OR`, `NOT`
|
|
72
73
|
|
|
@@ -116,6 +117,43 @@ The AUTOCOMMIT metacommand is database-specific, and affects only the database i
|
|
|
116
117
|
The [IMPORT](#import) and [COPY](#copy) metacommands do not commit data changes while AUTOCOMMIT is off (except when the NEW or REPLACEMENT clauses are used with Firebird; in those cases the 'create table' statement that *execsql* generates and runs will be committed). The SQL statements generated by the [IMPORT](#import) and [COPY](#copy) metacommands are sent to the database, however. Therefore the AUTOCOMMIT metacommand is recommended when explicit transaction control is to be applied to the [IMPORT](#import) and [COPY](#copy) metacommands.
|
|
117
118
|
|
|
118
119
|
|
|
120
|
+
## BREAKPOINT { #breakpoint }
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
BREAKPOINT
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Pauses script execution and drops into an interactive debug REPL (read-eval-print loop) on the console. Use this to inspect state and step through a script interactively while debugging.
|
|
127
|
+
|
|
128
|
+
**Non-interactive safety:** If `sys.stdin` is not a TTY (e.g. CI pipelines, piped input, batch execution) the metacommand is silently skipped. Scripts will never hang in automation.
|
|
129
|
+
|
|
130
|
+
**REPL commands:**
|
|
131
|
+
|
|
132
|
+
| Command | Description |
|
|
133
|
+
|---------|-------------|
|
|
134
|
+
| `continue` or `c` | Resume script execution |
|
|
135
|
+
| `abort`, `q`, or `quit` | Halt the script with exit status 1 |
|
|
136
|
+
| `vars` | List all substitution variables and their current values |
|
|
137
|
+
| `$VARNAME` | Print the value of a single variable (also `&VAR`, `@VAR`) |
|
|
138
|
+
| `SELECT ...;` | Run an ad-hoc SQL query against the current database and pretty-print results |
|
|
139
|
+
| `next` or `n` | Execute the next script statement, then pause again (step mode) |
|
|
140
|
+
| `stack` | Show the command-list stack: script name, cursor index, and nesting depth |
|
|
141
|
+
| `help` | Show the list of available REPL commands |
|
|
142
|
+
|
|
143
|
+
Pressing Ctrl-D (EOF) or Ctrl-C (KeyboardInterrupt) at the `execsql debug>` prompt resumes execution, the same as typing `continue`.
|
|
144
|
+
|
|
145
|
+
**Example:**
|
|
146
|
+
|
|
147
|
+
```sql
|
|
148
|
+
INSERT INTO staging SELECT * FROM raw_data;
|
|
149
|
+
-- !x! BREAKPOINT
|
|
150
|
+
-- Script pauses here. Inspect variables, run queries, then continue.
|
|
151
|
+
SELECT count(*) FROM staging;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
BREAKPOINT is silently skipped inside a `False` [IF](#if_cmd) block.
|
|
155
|
+
|
|
156
|
+
|
|
119
157
|
## BEGIN BATCH and END BATCH { #batch }
|
|
120
158
|
|
|
121
159
|
```
|
|
@@ -1435,6 +1473,62 @@ IS_GTE(<value1>, <value2>)
|
|
|
1435
1473
|
Evaluates whether or not the first of the specified values is greater than or equal to the second value. If the values are not numeric, an error will occur, and script processing will halt.
|
|
1436
1474
|
|
|
1437
1475
|
|
|
1476
|
+
### *ROW_COUNT_GT* { #row_count_gt }
|
|
1477
|
+
|
|
1478
|
+
```
|
|
1479
|
+
ROW_COUNT_GT(<table_name>, <N>)
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
Evaluates whether the number of rows in the specified table or view is strictly greater than the integer *N*. Issues a `SELECT count(*)` query against the current database. An error will occur if the table does not exist or is not accessible.
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
### *ROW_COUNT_GTE* { #row_count_gte }
|
|
1486
|
+
|
|
1487
|
+
```
|
|
1488
|
+
ROW_COUNT_GTE(<table_name>, <N>)
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
Evaluates whether the number of rows in the specified table or view is greater than or equal to the integer *N*.
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
### *ROW_COUNT_EQ* { #row_count_eq }
|
|
1495
|
+
|
|
1496
|
+
```
|
|
1497
|
+
ROW_COUNT_EQ(<table_name>, <N>)
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
Evaluates whether the number of rows in the specified table or view is exactly equal to the integer *N*.
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
### *ROW_COUNT_LT* { #row_count_lt }
|
|
1504
|
+
|
|
1505
|
+
```
|
|
1506
|
+
ROW_COUNT_LT(<table_name>, <N>)
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
Evaluates whether the number of rows in the specified table or view is strictly less than the integer *N*.
|
|
1510
|
+
|
|
1511
|
+
**Examples:**
|
|
1512
|
+
|
|
1513
|
+
```sql
|
|
1514
|
+
-- Halt if the orders table is empty.
|
|
1515
|
+
-- !x! ASSERT ROW_COUNT_GT(orders, 0) "orders table must not be empty"
|
|
1516
|
+
|
|
1517
|
+
-- Only process if staging has at least 1000 rows.
|
|
1518
|
+
-- !x! IF (ROW_COUNT_GTE(staging, 1000))
|
|
1519
|
+
-- !x! WRITE "staging is ready"
|
|
1520
|
+
-- !x! ENDIF
|
|
1521
|
+
|
|
1522
|
+
-- Verify exactly 12 monthly records were loaded.
|
|
1523
|
+
-- !x! ASSERT ROW_COUNT_EQ(monthly_totals, 12) "expected 12 monthly rows"
|
|
1524
|
+
|
|
1525
|
+
-- Skip cleanup if the temp table already has fewer than 100 rows.
|
|
1526
|
+
-- !x! IF (ROW_COUNT_LT(temp_work, 100))
|
|
1527
|
+
-- !x! WRITE "temp_work is small, skipping truncate"
|
|
1528
|
+
-- !x! ENDIF
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
|
|
1438
1532
|
### *IS_NULL*
|
|
1439
1533
|
|
|
1440
1534
|
```
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
},
|
|
94
94
|
"action-keywords": {
|
|
95
95
|
"comment": "sub, write, execute script, export, etc.",
|
|
96
|
-
"match": "(?i)\\b(reset\\s+dialog_canceled|write\\s+create_table|export_metadata|sub_querystring|execute\\s+script|append\\s+script|extend\\s+script|reset\\s+counter|export\\s+query|sub_tempfile|write\\s+script|import_file|set\\s+counter|sub_decrypt|sub_encrypt|autocommit|copy\\s+query|disconnect|select_sub|sub_append|system_cmd|pg_vacuum|sub_empty|sub_local|connect|include|max_int|rm_file|subdata|sub_add|sub_ini|assert|export|import|rm_sub|debug|email|serve|write|copy|log|run|sub|use|zip|cd)\\b",
|
|
96
|
+
"match": "(?i)\\b(reset\\s+dialog_canceled|write\\s+create_table|export_metadata|sub_querystring|execute\\s+script|append\\s+script|extend\\s+script|reset\\s+counter|export\\s+query|sub_tempfile|write\\s+script|import_file|set\\s+counter|sub_decrypt|sub_encrypt|autocommit|breakpoint|copy\\s+query|disconnect|select_sub|sub_append|system_cmd|pg_vacuum|sub_empty|sub_local|connect|include|max_int|rm_file|subdata|sub_add|sub_ini|assert|export|import|rm_sub|debug|email|serve|write|copy|log|run|sub|use|zip|cd)\\b",
|
|
97
97
|
"name": "keyword.other.execsql"
|
|
98
98
|
},
|
|
99
99
|
"config-event-keywords": {
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
},
|
|
109
109
|
"builtin-functions": {
|
|
110
110
|
"comment": "Conditional test functions used in if/elseif",
|
|
111
|
-
"match": "(?i)\\b(metacommand_error|directory_exists|dialog_canceled|alias_defined|column_exists|database_name|schema_exists|script_exists|table_exists|file_exists|role_exists|starts_with|sub_defined|view_exists|console_on|newer_date|newer_file|ends_with|identical|sql_error|sub_empty|contains|is_false|hasrows|is_null|is_true|is_zero|is_gte|equal|is_gt|dbms|not|or)\\b",
|
|
111
|
+
"match": "(?i)\\b(metacommand_error|directory_exists|dialog_canceled|alias_defined|column_exists|database_name|row_count_gte|schema_exists|script_exists|row_count_eq|row_count_gt|row_count_lt|table_exists|file_exists|role_exists|starts_with|sub_defined|view_exists|console_on|newer_date|newer_file|ends_with|identical|sql_error|sub_empty|contains|is_false|hasrows|is_null|is_true|is_zero|is_gte|equal|is_gt|dbms|not|or)\\b",
|
|
112
112
|
"name": "support.function.execsql"
|
|
113
113
|
},
|
|
114
114
|
"config-option-names": {
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.10.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" }
|
|
@@ -158,7 +158,7 @@ skip-magic-trailing-comma = false
|
|
|
158
158
|
line-ending = "auto"
|
|
159
159
|
|
|
160
160
|
[tool.bumpversion]
|
|
161
|
-
current_version = "2.
|
|
161
|
+
current_version = "2.10.1"
|
|
162
162
|
commit = true
|
|
163
163
|
commit_args = "--no-verify"
|
|
164
164
|
tag = true
|
|
@@ -100,6 +100,7 @@ from execsql.metacommands.debug import (
|
|
|
100
100
|
x_debug_write_subvars,
|
|
101
101
|
x_debug_write_config,
|
|
102
102
|
)
|
|
103
|
+
from execsql.metacommands.debug_repl import x_breakpoint
|
|
103
104
|
from execsql.metacommands.io import (
|
|
104
105
|
x_export,
|
|
105
106
|
x_export_query,
|
|
@@ -300,6 +301,8 @@ __all__ = [
|
|
|
300
301
|
"x_debug_log_config",
|
|
301
302
|
"x_debug_write_subvars",
|
|
302
303
|
"x_debug_write_config",
|
|
304
|
+
# debug repl handlers
|
|
305
|
+
"x_breakpoint",
|
|
303
306
|
# io handlers
|
|
304
307
|
"x_export",
|
|
305
308
|
"x_export_query",
|
|
@@ -71,6 +71,124 @@ def xf_hasrows(**kwargs: Any) -> bool:
|
|
|
71
71
|
return nrows > 0
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def _row_count(queryname: str, sql_context: str, metacommandline: str) -> int:
|
|
75
|
+
"""Return the number of rows in *queryname*, raising ErrInfo on failure.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
queryname: Table or view name to count rows in.
|
|
79
|
+
sql_context: The SQL string to include in error messages.
|
|
80
|
+
metacommandline: The full metacommand line for error context.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Integer row count.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ErrInfo: If the query fails or the result is not numeric.
|
|
87
|
+
"""
|
|
88
|
+
sql = f"select count(*) from {queryname};"
|
|
89
|
+
try:
|
|
90
|
+
_hdrs, rec = _state.dbs.current().select_data(sql)
|
|
91
|
+
except ErrInfo:
|
|
92
|
+
raise
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise ErrInfo("db", sql, exception_msg=exception_desc()) from e
|
|
95
|
+
try:
|
|
96
|
+
return int(rec[0][0])
|
|
97
|
+
except (IndexError, TypeError, ValueError) as e:
|
|
98
|
+
raise ErrInfo(
|
|
99
|
+
type="cmd",
|
|
100
|
+
command_text=metacommandline,
|
|
101
|
+
other_msg=f"Could not read row count for {queryname}.",
|
|
102
|
+
) from e
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_row_count_n(raw: str, metacommandline: str) -> int:
|
|
106
|
+
"""Parse and return the numeric threshold N from the matched group.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
raw: The raw string captured by the regex group (``n``).
|
|
110
|
+
metacommandline: The full metacommand line for error context.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Integer value of *raw*.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ErrInfo: If *raw* cannot be parsed as an integer.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
return int(raw.strip())
|
|
120
|
+
except (ValueError, TypeError) as e:
|
|
121
|
+
raise ErrInfo(
|
|
122
|
+
type="cmd",
|
|
123
|
+
command_text=metacommandline,
|
|
124
|
+
other_msg=f"ROW_COUNT threshold must be an integer; got {raw!r}.",
|
|
125
|
+
) from e
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def xf_row_count_gt(**kwargs: Any) -> bool:
|
|
129
|
+
"""Return True if the row count of *queryname* is strictly greater than N.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
**kwargs: Named groups from the regex match, plus ``metacommandline``.
|
|
133
|
+
Required keys: ``queryname``, ``n``.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if ``count(*) > N``.
|
|
137
|
+
"""
|
|
138
|
+
queryname = kwargs["queryname"]
|
|
139
|
+
mcl = kwargs["metacommandline"]
|
|
140
|
+
n = _parse_row_count_n(kwargs["n"], mcl)
|
|
141
|
+
return _row_count(queryname, f"select count(*) from {queryname};", mcl) > n
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def xf_row_count_gte(**kwargs: Any) -> bool:
|
|
145
|
+
"""Return True if the row count of *queryname* is greater than or equal to N.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
**kwargs: Named groups from the regex match, plus ``metacommandline``.
|
|
149
|
+
Required keys: ``queryname``, ``n``.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if ``count(*) >= N``.
|
|
153
|
+
"""
|
|
154
|
+
queryname = kwargs["queryname"]
|
|
155
|
+
mcl = kwargs["metacommandline"]
|
|
156
|
+
n = _parse_row_count_n(kwargs["n"], mcl)
|
|
157
|
+
return _row_count(queryname, f"select count(*) from {queryname};", mcl) >= n
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def xf_row_count_eq(**kwargs: Any) -> bool:
|
|
161
|
+
"""Return True if the row count of *queryname* equals N exactly.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
**kwargs: Named groups from the regex match, plus ``metacommandline``.
|
|
165
|
+
Required keys: ``queryname``, ``n``.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if ``count(*) == N``.
|
|
169
|
+
"""
|
|
170
|
+
queryname = kwargs["queryname"]
|
|
171
|
+
mcl = kwargs["metacommandline"]
|
|
172
|
+
n = _parse_row_count_n(kwargs["n"], mcl)
|
|
173
|
+
return _row_count(queryname, f"select count(*) from {queryname};", mcl) == n
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def xf_row_count_lt(**kwargs: Any) -> bool:
|
|
177
|
+
"""Return True if the row count of *queryname* is strictly less than N.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
**kwargs: Named groups from the regex match, plus ``metacommandline``.
|
|
181
|
+
Required keys: ``queryname``, ``n``.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if ``count(*) < N``.
|
|
185
|
+
"""
|
|
186
|
+
queryname = kwargs["queryname"]
|
|
187
|
+
mcl = kwargs["metacommandline"]
|
|
188
|
+
n = _parse_row_count_n(kwargs["n"], mcl)
|
|
189
|
+
return _row_count(queryname, f"select count(*) from {queryname};", mcl) < n
|
|
190
|
+
|
|
191
|
+
|
|
74
192
|
def xf_sqlerror(**kwargs: Any) -> bool:
|
|
75
193
|
return _state.status.sql_error
|
|
76
194
|
|
|
@@ -495,6 +613,36 @@ def build_conditional_table() -> Any:
|
|
|
495
613
|
mcl.add(r"^\s*HASROWS\((?P<queryname>[^)]+)\)", xf_hasrows, description="HASROWS", category="condition")
|
|
496
614
|
mcl.add(r"^\s*HAS_ROWS\((?P<queryname>[^)]+)\)", xf_hasrows)
|
|
497
615
|
|
|
616
|
+
# ROW_COUNT comparisons — ROW_COUNT_GT/GTE/EQ/LT(table, N)
|
|
617
|
+
# Table name: unquoted, double-quoted, or single-quoted. N: integer literal.
|
|
618
|
+
_rc_table = r"(?P<queryname>[A-Za-z0-9_.\"'\[\]]+)"
|
|
619
|
+
_rc_n = r"(?P<n>\d+)"
|
|
620
|
+
_rc_sep = r"\s*,\s*"
|
|
621
|
+
mcl.add(
|
|
622
|
+
rf"^\s*ROW_COUNT_GT\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
|
|
623
|
+
xf_row_count_gt,
|
|
624
|
+
description="ROW_COUNT_GT",
|
|
625
|
+
category="condition",
|
|
626
|
+
)
|
|
627
|
+
mcl.add(
|
|
628
|
+
rf"^\s*ROW_COUNT_GTE\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
|
|
629
|
+
xf_row_count_gte,
|
|
630
|
+
description="ROW_COUNT_GTE",
|
|
631
|
+
category="condition",
|
|
632
|
+
)
|
|
633
|
+
mcl.add(
|
|
634
|
+
rf"^\s*ROW_COUNT_EQ\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
|
|
635
|
+
xf_row_count_eq,
|
|
636
|
+
description="ROW_COUNT_EQ",
|
|
637
|
+
category="condition",
|
|
638
|
+
)
|
|
639
|
+
mcl.add(
|
|
640
|
+
rf"^\s*ROW_COUNT_LT\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
|
|
641
|
+
xf_row_count_lt,
|
|
642
|
+
description="ROW_COUNT_LT",
|
|
643
|
+
category="condition",
|
|
644
|
+
)
|
|
645
|
+
|
|
498
646
|
# Status predicates
|
|
499
647
|
mcl.add(r"^\s*sql_error\(\s*\)", xf_sqlerror, description="SQL_ERROR", category="condition")
|
|
500
648
|
mcl.add(r"^\s*dialog_canceled\(\s*\)", xf_dialogcanceled, description="DIALOG_CANCELED", category="condition")
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Interactive debug REPL metacommand handler for execsql.
|
|
5
|
+
|
|
6
|
+
Implements ``x_breakpoint`` — the ``BREAKPOINT`` metacommand — which pauses
|
|
7
|
+
script execution and drops into an interactive read-eval-print loop.
|
|
8
|
+
|
|
9
|
+
The REPL allows the user to:
|
|
10
|
+
|
|
11
|
+
- Inspect and print substitution variables.
|
|
12
|
+
- Run ad-hoc SQL queries against the current database.
|
|
13
|
+
- Step through the script one statement at a time.
|
|
14
|
+
- Resume or abort execution.
|
|
15
|
+
|
|
16
|
+
In non-interactive environments (CI, piped input, ``sys.stdin.isatty()`` is
|
|
17
|
+
``False``) the metacommand is silently skipped so automated pipelines are not
|
|
18
|
+
blocked.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import execsql.state as _state
|
|
25
|
+
|
|
26
|
+
__all__ = ["x_breakpoint"]
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Public handler
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
_HELP_TEXT = """\
|
|
33
|
+
execsql debug REPL commands:
|
|
34
|
+
continue c Resume script execution
|
|
35
|
+
abort q quit Halt the script (exit 1)
|
|
36
|
+
vars List all substitution variables and their values
|
|
37
|
+
$VARNAME Print a single variable's value (also &VAR, @VAR)
|
|
38
|
+
SELECT ...; Run ad-hoc SQL against the current database
|
|
39
|
+
next n Execute the next statement then pause again (step mode)
|
|
40
|
+
stack Show the command-list stack (script name, line, depth)
|
|
41
|
+
help Show this help text
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def x_breakpoint(**kwargs: Any) -> None:
|
|
46
|
+
"""Pause execution and enter the interactive debug REPL.
|
|
47
|
+
|
|
48
|
+
If ``sys.stdin`` is not a TTY (CI, piped input), the metacommand is
|
|
49
|
+
silently skipped — scripts will not hang in automation.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
**kwargs: Keyword arguments injected by the dispatch table (unused).
|
|
53
|
+
"""
|
|
54
|
+
if not sys.stdin.isatty():
|
|
55
|
+
return
|
|
56
|
+
_debug_repl()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# REPL core
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _debug_repl() -> None:
|
|
65
|
+
"""Interactive read-eval-print loop for script debugging.
|
|
66
|
+
|
|
67
|
+
Reads commands from stdin until the user types ``continue`` or ``abort``,
|
|
68
|
+
or until EOF / KeyboardInterrupt.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
import readline as _readline # noqa: F401 — side-effect: enables history/arrow keys
|
|
72
|
+
except ImportError:
|
|
73
|
+
pass # readline not available on Windows; continue without it
|
|
74
|
+
|
|
75
|
+
_write("\n[Breakpoint] Script paused. Type 'help' for commands, 'continue' to resume.\n")
|
|
76
|
+
|
|
77
|
+
while True:
|
|
78
|
+
try:
|
|
79
|
+
line = input("execsql debug> ").strip()
|
|
80
|
+
except EOFError:
|
|
81
|
+
_write("\n")
|
|
82
|
+
return # Ctrl-D → continue
|
|
83
|
+
except KeyboardInterrupt:
|
|
84
|
+
_write("\n")
|
|
85
|
+
return # Ctrl-C → continue
|
|
86
|
+
|
|
87
|
+
if not line:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
lower = line.lower()
|
|
91
|
+
|
|
92
|
+
if lower in ("continue", "c"):
|
|
93
|
+
return
|
|
94
|
+
elif lower in ("abort", "q", "quit"):
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
elif lower == "help":
|
|
97
|
+
_write(_HELP_TEXT)
|
|
98
|
+
elif lower == "vars":
|
|
99
|
+
_print_all_vars()
|
|
100
|
+
elif lower == "stack":
|
|
101
|
+
_print_stack()
|
|
102
|
+
elif lower in ("next", "n"):
|
|
103
|
+
_enable_step_mode()
|
|
104
|
+
return
|
|
105
|
+
elif line[0] in ("$", "&", "@"):
|
|
106
|
+
_print_var(line)
|
|
107
|
+
elif line.rstrip().endswith(";"):
|
|
108
|
+
_run_sql(line)
|
|
109
|
+
else:
|
|
110
|
+
_write(f"Unknown command: {line!r}. Type 'help' for available commands.\n")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# REPL command implementations
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _write(text: str) -> None:
|
|
119
|
+
"""Write *text* to the execsql output stream (falls back to stdout)."""
|
|
120
|
+
output = _state.output
|
|
121
|
+
if output is not None:
|
|
122
|
+
output.write(text)
|
|
123
|
+
else:
|
|
124
|
+
sys.stdout.write(text)
|
|
125
|
+
sys.stdout.flush()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _print_all_vars() -> None:
|
|
129
|
+
"""Print all substitution variables and their current values."""
|
|
130
|
+
subvars = _state.subvars
|
|
131
|
+
if subvars is None:
|
|
132
|
+
_write(" (no substitution variables defined)\n")
|
|
133
|
+
return
|
|
134
|
+
items = subvars.substitutions # list of (name, value) tuples
|
|
135
|
+
if not items:
|
|
136
|
+
_write(" (no substitution variables defined)\n")
|
|
137
|
+
return
|
|
138
|
+
# Compute column width for aligned output.
|
|
139
|
+
max_name = max((len(name) for name, _ in items), default=0)
|
|
140
|
+
for name, value in sorted(items):
|
|
141
|
+
_write(f" {name:<{max_name}} = {value!r}\n")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _print_var(varname: str) -> None:
|
|
145
|
+
"""Print the value of a single substitution variable.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
varname: The variable reference as typed by the user, e.g. ``$FOO``.
|
|
149
|
+
"""
|
|
150
|
+
subvars = _state.subvars
|
|
151
|
+
if subvars is None:
|
|
152
|
+
_write(f" {varname}: (substitution variables not initialised)\n")
|
|
153
|
+
return
|
|
154
|
+
# Try the name as typed first, then without the sigil prefix ($, &, @, #, ~).
|
|
155
|
+
# SUB creates variables without a prefix (e.g., "logfile"), but users
|
|
156
|
+
# naturally type "$logfile" at the prompt.
|
|
157
|
+
value = subvars.varvalue(varname)
|
|
158
|
+
if value is None and len(varname) > 1 and varname[0] in "$&@#~":
|
|
159
|
+
value = subvars.varvalue(varname[1:])
|
|
160
|
+
if value is None:
|
|
161
|
+
_write(f" {varname}: (undefined)\n")
|
|
162
|
+
else:
|
|
163
|
+
_write(f" {varname} = {value!r}\n")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _print_stack() -> None:
|
|
167
|
+
"""Print the current command-list stack (script name, line number, depth)."""
|
|
168
|
+
stack = _state.commandliststack
|
|
169
|
+
if not stack:
|
|
170
|
+
_write(" (command list stack is empty)\n")
|
|
171
|
+
return
|
|
172
|
+
_write(f" Stack depth: {len(stack)}\n")
|
|
173
|
+
for depth, cmdlist in enumerate(stack):
|
|
174
|
+
listname = getattr(cmdlist, "listname", "<unknown>")
|
|
175
|
+
cmdptr = getattr(cmdlist, "cmdptr", 0)
|
|
176
|
+
_write(f" [{depth}] {listname} (cursor at index {cmdptr})\n")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _run_sql(sql: str) -> None:
|
|
180
|
+
"""Execute ad-hoc SQL against the current database and pretty-print the results.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
sql: A complete SQL statement ending with a semicolon.
|
|
184
|
+
"""
|
|
185
|
+
dbs = _state.dbs
|
|
186
|
+
if dbs is None:
|
|
187
|
+
_write(" (no database connection is active)\n")
|
|
188
|
+
return
|
|
189
|
+
db = dbs.current()
|
|
190
|
+
if db is None:
|
|
191
|
+
_write(" (no database connection is active)\n")
|
|
192
|
+
return
|
|
193
|
+
try:
|
|
194
|
+
colnames, rows = db.select_data(sql)
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
_write(f" SQL error: {exc}\n")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
if not colnames:
|
|
200
|
+
_write(" (query returned no columns)\n")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# Build a simple text table.
|
|
204
|
+
col_widths = [len(c) for c in colnames]
|
|
205
|
+
str_rows: list[list[str]] = []
|
|
206
|
+
for row in rows:
|
|
207
|
+
str_row = [str(v) if v is not None else "NULL" for v in row]
|
|
208
|
+
str_rows.append(str_row)
|
|
209
|
+
for i, cell in enumerate(str_row):
|
|
210
|
+
col_widths[i] = max(col_widths[i], len(cell))
|
|
211
|
+
|
|
212
|
+
sep = "+-" + "-+-".join("-" * w for w in col_widths) + "-+"
|
|
213
|
+
header = "| " + " | ".join(c.ljust(col_widths[i]) for i, c in enumerate(colnames)) + " |"
|
|
214
|
+
_write(sep + "\n")
|
|
215
|
+
_write(header + "\n")
|
|
216
|
+
_write(sep + "\n")
|
|
217
|
+
for str_row in str_rows:
|
|
218
|
+
data_line = "| " + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(str_row)) + " |"
|
|
219
|
+
_write(data_line + "\n")
|
|
220
|
+
_write(sep + "\n")
|
|
221
|
+
row_word = "row" if len(str_rows) == 1 else "rows"
|
|
222
|
+
_write(f" ({len(str_rows)} {row_word})\n")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _enable_step_mode() -> None:
|
|
226
|
+
"""Activate step mode so the engine re-enters the REPL after the next statement."""
|
|
227
|
+
_state.step_mode = True
|
|
@@ -100,6 +100,7 @@ from execsql.metacommands.debug import (
|
|
|
100
100
|
x_debug_write_odbc_drivers,
|
|
101
101
|
x_debug_write_subvars,
|
|
102
102
|
)
|
|
103
|
+
from execsql.metacommands.debug_repl import x_breakpoint
|
|
103
104
|
from execsql.metacommands.io import (
|
|
104
105
|
x_cd,
|
|
105
106
|
x_copy,
|
|
@@ -1690,6 +1691,17 @@ def build_dispatch_table() -> MetaCommandList:
|
|
|
1690
1691
|
run_when_false=False,
|
|
1691
1692
|
)
|
|
1692
1693
|
|
|
1694
|
+
# ------------------------------------------------------------------
|
|
1695
|
+
# BREAKPOINT
|
|
1696
|
+
# ------------------------------------------------------------------
|
|
1697
|
+
mcl.add(
|
|
1698
|
+
r"^\s*BREAKPOINT\s*$",
|
|
1699
|
+
x_breakpoint,
|
|
1700
|
+
description="BREAKPOINT",
|
|
1701
|
+
category="action",
|
|
1702
|
+
run_when_false=False,
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1693
1705
|
# ------------------------------------------------------------------
|
|
1694
1706
|
# IF / ORIF / ANDIF / ELSEIF / ELSE / ENDIF
|
|
1695
1707
|
# ------------------------------------------------------------------
|
|
@@ -506,6 +506,11 @@ class CommandList:
|
|
|
506
506
|
cmditem.command.commandline()[:100],
|
|
507
507
|
),
|
|
508
508
|
)
|
|
509
|
+
if _state.step_mode:
|
|
510
|
+
_state.step_mode = False
|
|
511
|
+
from execsql.metacommands.debug_repl import _debug_repl
|
|
512
|
+
|
|
513
|
+
_debug_repl()
|
|
509
514
|
self.cmdptr += 1
|
|
510
515
|
|
|
511
516
|
def run_next(self) -> None:
|