execsql2 2.15.11__tar.gz → 2.16.0__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.16.0/AUDIT.md +324 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/CHANGELOG.md +50 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/PKG-INFO +58 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/README.md +57 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/about/divergence.md +46 -4
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/index.md +21 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/adding_metacommands.md +11 -3
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/architecture.md +31 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/getting-started/syntax.md +16 -0
- execsql2-2.16.0/extras/plugin-template/README.md +71 -0
- execsql2-2.16.0/extras/plugin-template/pyproject.toml +22 -0
- execsql2-2.16.0/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +96 -0
- execsql2-2.16.0/extras/plugin-template/tests/test_plugin.py.example +110 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/pyproject.toml +5 -3
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/__init__.py +4 -0
- execsql2-2.16.0/src/execsql/api.py +580 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/__init__.py +106 -0
- execsql2-2.16.0/src/execsql/cli/lint_ast.py +439 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/run.py +109 -101
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/config.py +9 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/access.py +1 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/dsn.py +3 -2
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/duckdb.py +1 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/factory.py +3 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/firebird.py +2 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/mysql.py +2 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/oracle.py +2 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/postgres.py +2 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/sqlite.py +1 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/sqlserver.py +3 -2
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/base.py +6 -4
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/delimited.py +11 -3
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/pretty.py +9 -12
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/__init__.py +3 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/connect.py +1 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/control.py +8 -14
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/debug.py +6 -4
- execsql2-2.16.0/src/execsql/metacommands/io_export.py +325 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/io_fileops.py +7 -13
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/io_write.py +1 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/script_ext.py +8 -5
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/upsert.py +40 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/models.py +8 -12
- execsql2-2.16.0/src/execsql/plugins.py +414 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/script/__init__.py +36 -12
- execsql2-2.16.0/src/execsql/script/ast.py +562 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/script/engine.py +59 -368
- execsql2-2.16.0/src/execsql/script/executor.py +833 -0
- execsql2-2.16.0/src/execsql/script/parser.py +663 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/script/variables.py +11 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/state.py +55 -2
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/crypto.py +14 -10
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/errors.py +31 -8
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/mail.py +15 -12
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_cli.py +163 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_cli_run.py +18 -151
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_profile.py +4 -4
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/conftest.py +16 -1
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_db_adapters_mocked.py +3 -4
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_dsn.py +5 -6
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_base.py +15 -13
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands.py +4 -3
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_extended.py +8 -37
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_fileops_extra.py +4 -20
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_io.py +5 -21
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_script_ext.py +5 -11
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/fixtures/control_flow.sql +5 -0
- execsql2-2.16.0/tests/scripts/fixtures/parse_only/parse_tree.sql +541 -0
- execsql2-2.16.0/tests/test_api.py +303 -0
- execsql2-2.16.0/tests/test_ast.py +552 -0
- execsql2-2.16.0/tests/test_ast_parser.py +829 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_engine.py +15 -630
- execsql2-2.16.0/tests/test_executor.py +939 -0
- execsql2-2.16.0/tests/test_plugins.py +213 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_script.py +8 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_errors_extra.py +4 -4
- {execsql2-2.15.11 → execsql2-2.16.0}/uv.lock +1 -1
- execsql2-2.15.11/src/execsql/metacommands/io_export.py +0 -523
- {execsql2-2.15.11 → execsql2-2.16.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/.gitignore +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/.pre-commit-config.yaml +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/.python-version +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/.readthedocs.yaml +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/CONTRIBUTING.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/LICENSE.txt +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/NOTICE +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/SECURITY.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/about/contributors.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/about/copyright.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/cli.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/db.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/exporters.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/importers.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/api/metacommands.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/getting-started/installation.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/debugging.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/documentation.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/encoding.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/examples.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/formatter.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/logging.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/usage.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/actions.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/actions2.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/checkboxes.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/connect.b64 +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/connect.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/create_conf.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/entry_form.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/execsql_console.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/fatals.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/logo_small.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/unmatched.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/index.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/reference/configuration.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/reference/metacommands.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/reference/security.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/justfile +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/__main__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/help.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/db/base.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exceptions.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/format.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/base.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/console.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/base.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/json.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/parser.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/py.typed +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/script/control.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/types.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/README.md +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/config_settings.sqlite +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/execsql.conf +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/make_config_db.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/md_compare.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/md_glossary.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/md_upsert.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/pg_compare.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/pg_glossary.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/pg_upsert.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/script_template.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/ss_compare.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/ss_glossary.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/templates/ss_upsert.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_lint.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/cli/test_ping.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_base.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_factory.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_postgres.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_db.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_html_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_json.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_json_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_latex_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_pretty_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_raw_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_templates_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_values_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/gui/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/gui/test_backends.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/gui/test_compare_stats.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/gui/test_compute_row_diffs.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_base_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_csv_edge_cases.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_json_importer.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/conftest.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_pg_upsert.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/fixtures/smoke.sql +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/scripts/test_sql_scripts.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_config.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_config_data.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_config_extended.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_debug_repl.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_error_messages.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_exceptions.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_format.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_mail.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_models.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_package.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_parser.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_registry.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_state.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/test_types.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/__init__.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_auth.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_errors.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_regex.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_strings.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_timer.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.15.11 → execsql2-2.16.0}/zensical.toml +0 -0
execsql2-2.16.0/AUDIT.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Codebase Audit — execsql2
|
|
2
|
+
|
|
3
|
+
**Original audit:** 2026-04-13
|
|
4
|
+
**Last updated:** 2026-04-29
|
|
5
|
+
|
|
6
|
+
Full audit covered 42 source files and 12 test files. 25 findings were identified (F1-F25)
|
|
7
|
+
plus 29 additional findings across security, performance, test gaps, edge cases, code quality,
|
|
8
|
+
and divergences. Of those, **25 findings have been resolved** (10 bugs, 3 perf fixes, 2 edge
|
|
9
|
+
cases, 1 security fix, 8 doc fixes, 1 CI fix, and the F-series fixes below).
|
|
10
|
+
|
|
11
|
+
**Resolved F-series:** F1 (substitute_all tuple unpack), F2 (bytes-to-stdout), F5 (export
|
|
12
|
+
dispatch duplication), F7 (commandliststack bounds), F8 (exec_cmd injection), F10 (enc_password
|
|
13
|
+
deprecation warning), F12 (env var filtering), F13 (bumpversion --no-verify), F14 (dead
|
|
14
|
+
\_DEFAULT_CTX), F17 (duplicate tuple entries), F20 (password persistence), F24 (Comment AST node
|
|
15
|
+
implemented).
|
|
16
|
+
|
|
17
|
+
**User-excluded:** F11 (--allow-shell flag) — not adding.
|
|
18
|
+
|
|
19
|
+
______________________________________________________________________
|
|
20
|
+
|
|
21
|
+
## Architecture & Code Quality
|
|
22
|
+
|
|
23
|
+
### F3. [HIGH] Global mutable state blocks threading and library API
|
|
24
|
+
|
|
25
|
+
`src/execsql/state.py:399-432` — `_ctx` is a plain module-level global. All 200+ metacommand
|
|
26
|
+
handlers, SQL execution, variable substitution, and the IF-stack go through `_state.foo` which
|
|
27
|
+
resolves to `_ctx.foo`. Two threads executing scripts simultaneously will corrupt each other's
|
|
28
|
+
state. Blocks the planned PARALLEL metacommand and safe `from execsql import run` usage.
|
|
29
|
+
|
|
30
|
+
**Fix:** Replace `_ctx` with `contextvars.ContextVar` or `threading.local()`.
|
|
31
|
+
|
|
32
|
+
### F4. [HIGH] `_run()` is a 460-line god-function with 36 parameters
|
|
33
|
+
|
|
34
|
+
`src/execsql/cli/run.py:193-660` — Performs config parsing, CLI argument merging, DSN parsing,
|
|
35
|
+
database connection, GUI init, script loading, dry-run, AST execution, legacy execution,
|
|
36
|
+
profiling, and cleanup in a single function. Estimated cyclomatic complexity ~85-90. Untestable
|
|
37
|
+
as a unit.
|
|
38
|
+
|
|
39
|
+
**Fix:** Extract into phases: `_setup_config()`, `_connect_db()`, `_load_script()`,
|
|
40
|
+
`_execute()`, `_teardown()`.
|
|
41
|
+
|
|
42
|
+
### F15. [MEDIUM] Legacy parser is a 150-line function with 6 levels of nesting
|
|
43
|
+
|
|
44
|
+
`src/execsql/script/engine.py:864-1012` — The production parser tracks 6 state variables with
|
|
45
|
+
deepest nesting at 6 levels. The AST parser is the planned replacement; accelerating that
|
|
46
|
+
migration is the real fix.
|
|
47
|
+
|
|
48
|
+
### F19. [MEDIUM] 51 near-duplicate regex patterns in conditions.py
|
|
49
|
+
|
|
50
|
+
`src/execsql/metacommands/conditions.py:426-627` — `CONTAINS`, `STARTS_WITH`, `ENDS_WITH` each
|
|
51
|
+
register 17 quoting variants as separate regex patterns. Adding a new string predicate requires
|
|
52
|
+
copying and modifying 17 patterns.
|
|
53
|
+
|
|
54
|
+
**Fix:** Consolidate into a single pattern per predicate with optional quoting alternation groups.
|
|
55
|
+
|
|
56
|
+
### CQ-1. [MEDIUM] 111 bare `except Exception` clauses across 38 files
|
|
57
|
+
|
|
58
|
+
Many suppress errors silently (`pass`). Notable: `db/base.py` (14), `utils/fileio.py` (9),
|
|
59
|
+
`db/access.py` (10). Each should be reviewed for more specific exception types.
|
|
60
|
+
|
|
61
|
+
### CQ-4. [MEDIUM] Inconsistent `_cursor()` context manager vs bare `cursor()`
|
|
62
|
+
|
|
63
|
+
The `Database` base class provides `_cursor()` for cleanup, but many adapter methods still use
|
|
64
|
+
`curs = self.cursor()` without cleanup. Standardize to `with self._cursor() as curs:`.
|
|
65
|
+
|
|
66
|
+
### F22. [LOW] Eager import of `AccessDatabase` in connect.py
|
|
67
|
+
|
|
68
|
+
`src/execsql/metacommands/connect.py:1-2` — Imports `AccessDatabase` (depends on pyodbc,
|
|
69
|
+
Windows-only) at module load. Moving to lazy import was reverted because test patch targets
|
|
70
|
+
broke. Marginal benefit.
|
|
71
|
+
|
|
72
|
+
### F23. [LOW] `conftest.py` `minimal_conf` fixture doesn't cover all attributes
|
|
73
|
+
|
|
74
|
+
`tests/conftest.py:33-56` — `SimpleNamespace` with ~16 attributes vs `ConfigData.__init__`'s
|
|
75
|
+
~50+. Tests hitting unset attributes get `AttributeError`.
|
|
76
|
+
|
|
77
|
+
### F25. [LOW] `format.py` SQL formatter hardcodes PostgreSQL dialect
|
|
78
|
+
|
|
79
|
+
`src/execsql/format.py:155,160` — `sqlglot.parse(read="postgres")` regardless of target DB.
|
|
80
|
+
|
|
81
|
+
**Fix:** Accept a `--dialect` flag or infer from `--type`.
|
|
82
|
+
|
|
83
|
+
### CQ-2. [LOW] `JsonDatatype` class uses class-level attribute assignment anti-pattern
|
|
84
|
+
|
|
85
|
+
`src/execsql/models.py:308-324` — `JsonDatatype.integer` assigned twice (lines 317 and 322).
|
|
86
|
+
Could be an enum or dict.
|
|
87
|
+
|
|
88
|
+
### CQ-3. [LOW] `Encrypt.ky` is a mutable class variable
|
|
89
|
+
|
|
90
|
+
`src/execsql/utils/crypto.py:48-57` — Dict never mutated after definition but declared as
|
|
91
|
+
mutable class attribute. Frozen dict or class constant would be cleaner.
|
|
92
|
+
|
|
93
|
+
### CQ-5. [LOW] `state.py` version parsing catches bare `Exception`
|
|
94
|
+
|
|
95
|
+
`src/execsql/state.py:148` — Should be `except (ValueError, IndexError)`.
|
|
96
|
+
|
|
97
|
+
### CQ-6. [LOW] `ConfigData.export_output_dir` is dynamically added
|
|
98
|
+
|
|
99
|
+
`src/execsql/cli/run.py:365` — Attribute doesn't exist in `ConfigData.__init__()`. Should be
|
|
100
|
+
declared with default `None`.
|
|
101
|
+
|
|
102
|
+
______________________________________________________________________
|
|
103
|
+
|
|
104
|
+
## Security
|
|
105
|
+
|
|
106
|
+
### SEC-1. [MEDIUM] SHELL metacommand passes user input to subprocess
|
|
107
|
+
|
|
108
|
+
`src/execsql/metacommands/system.py:38-56` — `shlex.split()` tokenizes but doesn't sanitize.
|
|
109
|
+
Substitution variables from `PROMPT ENTRY`, `SUB_INI`, or env vars could inject arguments.
|
|
110
|
+
Partially mitigated: `subprocess.call` with list arg doesn't invoke a shell. User excluded
|
|
111
|
+
`--allow-shell` flag (F11).
|
|
112
|
+
|
|
113
|
+
### SEC-3. [LOW] `Encrypt` class provides no real security
|
|
114
|
+
|
|
115
|
+
`src/execsql/utils/crypto.py` — Hardcoded XOR keys in source. Any `enc_password` can be
|
|
116
|
+
trivially decoded. Documented as obfuscation-only. Deprecation warning now emitted (F10 fix).
|
|
117
|
+
|
|
118
|
+
### SEC-4. [LOW] `x_include` path traversal
|
|
119
|
+
|
|
120
|
+
`src/execsql/metacommands/io_fileops.py:26-38` — `INCLUDE` accepts arbitrary file paths from
|
|
121
|
+
scripts. If variables are populated from external sources, arbitrary files could be included.
|
|
122
|
+
|
|
123
|
+
______________________________________________________________________
|
|
124
|
+
|
|
125
|
+
## Performance
|
|
126
|
+
|
|
127
|
+
### F18. [MEDIUM] `SubVarSet._substitute_nested` is O(V) per call
|
|
128
|
+
|
|
129
|
+
`src/execsql/script/variables.py:262-296` — Fallback iterates over every defined variable with
|
|
130
|
+
case-insensitive substring search. With N tokens and V variables, worst case is O(N * V).
|
|
131
|
+
|
|
132
|
+
**Fix:** Index variables by first few characters for faster fallback lookup.
|
|
133
|
+
|
|
134
|
+
### PERF-4. [LOW] `substitute_vars` creates merged `SubVarSet` per statement
|
|
135
|
+
|
|
136
|
+
`src/execsql/script/engine.py:778-783` — When `localvars` is not None, creates a new
|
|
137
|
+
`SubVarSet` with recompiled regex every statement. Could cache when local vars haven't changed.
|
|
138
|
+
|
|
139
|
+
### PERF-5. [LOW] `set_dynamic_system_vars()` called per-statement
|
|
140
|
+
|
|
141
|
+
`src/execsql/script/engine.py:738-761` — 7 `add_substitution` calls per statement for values
|
|
142
|
+
that only change on CONFIG/AUTOCOMMIT metacommands. A dirty-flag approach would avoid overhead.
|
|
143
|
+
|
|
144
|
+
### PERF-6. [LOW] `date_fmts` deque is shared module-level mutable
|
|
145
|
+
|
|
146
|
+
`src/execsql/types.py:184-204` — Currently harmless but would be a race condition under future
|
|
147
|
+
parallelism.
|
|
148
|
+
|
|
149
|
+
### PERF-7. [LOW] Pretty-print materializes entire result set
|
|
150
|
+
|
|
151
|
+
`src/execsql/exporters/pretty.py:47-49` — `list(rows)` forces the entire streaming generator
|
|
152
|
+
into memory to compute column widths. A 1M-row result set could blow up memory.
|
|
153
|
+
|
|
154
|
+
______________________________________________________________________
|
|
155
|
+
|
|
156
|
+
## Test Gaps
|
|
157
|
+
|
|
158
|
+
### F6. [HIGH] Two parsers with no equivalence tests
|
|
159
|
+
|
|
160
|
+
`src/execsql/script/engine.py:864-1012` (legacy) and `src/execsql/script/parser.py` (AST) —
|
|
161
|
+
Two independent parsers for the same grammar with no shared test verifying equivalent results.
|
|
162
|
+
The AST parser creates nested block nodes; the legacy parser treats IF/LOOP/BATCH as flat.
|
|
163
|
+
|
|
164
|
+
**Fix:** Add parametric tests running a script corpus through both parsers and asserting
|
|
165
|
+
structural equivalence.
|
|
166
|
+
|
|
167
|
+
### F9. [HIGH] Core SQL execution unit tests are theater
|
|
168
|
+
|
|
169
|
+
`tests/test_engine.py:319-412` — `SqlStmt.run()` never tested with a real DB.
|
|
170
|
+
`MetacommandStmt.run()` patches the entire dispatch table. `CommandList` patches
|
|
171
|
+
`run_and_increment`. Error-recovery paths (`WriteSpec.write()`, `MailSpec.send()`) still have
|
|
172
|
+
zero integration test coverage.
|
|
173
|
+
|
|
174
|
+
**Fix:** Unit tests for `SqlStmt.run()` with real in-memory SQLite. Integration tests for
|
|
175
|
+
`ON ERROR_HALT WRITE` and `ON ERROR_HALT EMAIL` end-to-end.
|
|
176
|
+
|
|
177
|
+
### F16. [MEDIUM] Coverage omissions exclude ~7.8K lines
|
|
178
|
+
|
|
179
|
+
`pyproject.toml:204-222` — Excludes gui/desktop.py, gui/tui.py, metacommands/prompt.py,
|
|
180
|
+
script/executor.py, all 7 DB adapters, all LSP modules. The 90% gate applies to ~27K of ~35K
|
|
181
|
+
lines.
|
|
182
|
+
|
|
183
|
+
### TEST-1. [MEDIUM] No tests for `debug/repl.py`
|
|
184
|
+
|
|
185
|
+
REPL commands (`.vars`, `.set`, `.where`, `.stack`, ad-hoc SQL) have no dedicated tests.
|
|
186
|
+
|
|
187
|
+
### TEST-5. [MEDIUM] Limited importer edge case coverage
|
|
188
|
+
|
|
189
|
+
CSV importer doesn't cover: inconsistent column counts, encoding errors, BOM markers, empty
|
|
190
|
+
files, header-only files.
|
|
191
|
+
|
|
192
|
+
### TEST-2. [LOW] No tests for `gui/desktop.py` or `gui/tui.py` (excluded from coverage)
|
|
193
|
+
|
|
194
|
+
### TEST-3. [LOW] No tests for `metacommands/prompt.py` (excluded from coverage)
|
|
195
|
+
|
|
196
|
+
### TEST-4. [LOW] No tests for `db/dsn.py` (excluded from coverage)
|
|
197
|
+
|
|
198
|
+
### TEST-6. [LOW] No `format.py` edge case tests
|
|
199
|
+
|
|
200
|
+
Doesn't cover: nested block comments, metacommands inside block comments, SQL strings
|
|
201
|
+
containing `-- !x!` patterns, very long lines.
|
|
202
|
+
|
|
203
|
+
### TEST-7. [LOW] No property-based tests for parsers
|
|
204
|
+
|
|
205
|
+
`CondParser` and `NumericParser` handle arbitrary user input. Hypothesis-based testing would
|
|
206
|
+
catch edge cases.
|
|
207
|
+
|
|
208
|
+
### TEST-8. [LOW] DB adapters have zero test coverage
|
|
209
|
+
|
|
210
|
+
`db/access.py`, `db/firebird.py`, `db/oracle.py`, `db/sqlserver.py` — all excluded from
|
|
211
|
+
coverage.
|
|
212
|
+
|
|
213
|
+
______________________________________________________________________
|
|
214
|
+
|
|
215
|
+
## Edge Cases & Robustness
|
|
216
|
+
|
|
217
|
+
### EDGE-2. [LOW] `SubVarSet.substitute_all()` has no cycle depth limit
|
|
218
|
+
|
|
219
|
+
`src/execsql/script/variables.py:254-265` — `substitute_vars()` in engine.py has a 100-iteration
|
|
220
|
+
guard, but `substitute_all()` called directly (e.g., from config loading) has no guard.
|
|
221
|
+
|
|
222
|
+
### EDGE-3. [LOW] `ScriptFile.__repr__` uses `super().filename` incorrectly
|
|
223
|
+
|
|
224
|
+
`src/execsql/script/engine.py:635` — Should be `self.filename`.
|
|
225
|
+
|
|
226
|
+
### EDGE-5. [LOW] Block comment parsing doesn't handle nested comments
|
|
227
|
+
|
|
228
|
+
`src/execsql/script/engine.py:871-881` — Simple `in_block_cmt` flag; `/* outer /* inner */ still comment */` exits at first `*/`.
|
|
229
|
+
|
|
230
|
+
### EDGE-6. [LOW] `SourceString.match_str()` parameter shadows builtin `str`
|
|
231
|
+
|
|
232
|
+
`src/execsql/parser.py:60` — Suppressed by ruff A002.
|
|
233
|
+
|
|
234
|
+
### EDGE-7. [LOW] `_import_loop` may access unbound `line` variable
|
|
235
|
+
|
|
236
|
+
`src/execsql/db/base.py:565` — If `StopIteration` raised on first row, `line` would be
|
|
237
|
+
unbound. Guarded in practice by `len(b) > 0`.
|
|
238
|
+
|
|
239
|
+
______________________________________________________________________
|
|
240
|
+
|
|
241
|
+
## Divergences from Monolith
|
|
242
|
+
|
|
243
|
+
### DIV-1. [LOW] `ScriptCmd` resolves `source_dir` at construction time
|
|
244
|
+
|
|
245
|
+
`src/execsql/script/engine.py:400-408` — Monolith resolved per-statement. New behavior is
|
|
246
|
+
arguably better. Not documented in `divergence.md`.
|
|
247
|
+
|
|
248
|
+
### DIV-2. [MEDIUM] `$CURRENT_DATABASE`/`$CURRENT_DBMS` set differently
|
|
249
|
+
|
|
250
|
+
Static (per-connect) vs dynamic (per-statement) split means these aren't updated on `USE`.
|
|
251
|
+
|
|
252
|
+
### DIV-3. [LOW] `DT_Long` maps to "hugeint" in SQLite
|
|
253
|
+
|
|
254
|
+
`src/execsql/types.py:742` — SQLite doesn't have "hugeint"; gets TEXT affinity.
|
|
255
|
+
|
|
256
|
+
### DIV-4. [LOW] `DT_DuckDB` character types all map to TEXT
|
|
257
|
+
|
|
258
|
+
`src/execsql/types.py:761-763` — Loses VARCHAR(N) length constraints.
|
|
259
|
+
|
|
260
|
+
### DIV-5. [LOW] Keyring stores password silently in GUI mode
|
|
261
|
+
|
|
262
|
+
`src/execsql/utils/auth.py:192-193` — Auto-stores without asking. Divergence doc mentions
|
|
263
|
+
keyring but not auto-store behavior.
|
|
264
|
+
|
|
265
|
+
______________________________________________________________________
|
|
266
|
+
|
|
267
|
+
## Residual Risks
|
|
268
|
+
|
|
269
|
+
1. **Thread safety (F3)** is the largest residual risk. `from execsql import run` cannot be
|
|
270
|
+
used from multiple threads. Must be addressed before the library API is promoted.
|
|
271
|
+
|
|
272
|
+
1. **Env var filter (F12 fix)** is a behavioral change — scripts relying on
|
|
273
|
+
`!!&AWS_SECRET_ACCESS_KEY!!` will silently get empty strings. No opt-out mechanism.
|
|
274
|
+
|
|
275
|
+
1. **exec_cmd quoting (F8 fix)** — `quote_identifier("schema.myproc")` produces
|
|
276
|
+
`"schema.myproc"` (single identifier), not `"schema"."myproc"`. No worse than before but
|
|
277
|
+
could break schema-qualified function calls.
|
|
278
|
+
|
|
279
|
+
1. **WriteSpec/MailSpec** error-recovery paths are now correct but still have zero dedicated
|
|
280
|
+
integration tests.
|
|
281
|
+
|
|
282
|
+
1. **Bumpversion hook removal (F13 fix)** — if pre-commit hooks reject bump-generated content,
|
|
283
|
+
bumps will fail. Watch on next version bump.
|
|
284
|
+
|
|
285
|
+
______________________________________________________________________
|
|
286
|
+
|
|
287
|
+
## Feature Ideas
|
|
288
|
+
|
|
289
|
+
### Quick wins
|
|
290
|
+
|
|
291
|
+
- `$TIMER_SECONDS` — numeric companion to `$TIMER` timedelta string
|
|
292
|
+
- `$CURRENT_DATE` — clean `YYYY-MM-DD` string (vs `$DATE_TAG`'s `YYYYMMDD`)
|
|
293
|
+
- `CONTINUE` in loops — only `BREAK` exists; users nest IF blocks as workaround
|
|
294
|
+
|
|
295
|
+
### Medium features
|
|
296
|
+
|
|
297
|
+
- `FOR <var> IN <query|list>` loop — avoids `SUBDATA` + `WHILE` + string manipulation
|
|
298
|
+
- `RETRY N [BACKOFF s]` — transient DB error handling without verbose manual patterns
|
|
299
|
+
- Native webhook/HTTP notifications — `ON ERROR_HALT WEBHOOK` instead of `SYSTEM_CMD curl`
|
|
300
|
+
- `IMPORT FROM URL` — HTTP/REST import without temp file management
|
|
301
|
+
- Entry form validation enforcement — `validation_regex` fields exist but aren't enforced
|
|
302
|
+
|
|
303
|
+
### Strategic features
|
|
304
|
+
|
|
305
|
+
- Textual TUI console — `CONSOLE ON` is a stub in the Textual backend
|
|
306
|
+
- Persistent state across runs — `~/.execsql/state.db` for run history, watermarks, checkpoints
|
|
307
|
+
- Thread-safe RuntimeContext (F3) — enables PARALLEL blocks, concurrent library API, easier testing
|
|
308
|
+
- AST migration completion (F6/F15) — foundational for LSP and long-term maintainability
|
|
309
|
+
- Plugin system via entry points — custom metacommands, community ecosystem
|
|
310
|
+
- LSP enhancements — autocomplete, hover docs, jump-to-definition, inline diagnostics
|
|
311
|
+
|
|
312
|
+
______________________________________________________________________
|
|
313
|
+
|
|
314
|
+
## Summary
|
|
315
|
+
|
|
316
|
+
| Category | High | Medium | Low | Total |
|
|
317
|
+
| ---------------------- | ----- | ------ | ------ | ------ |
|
|
318
|
+
| Architecture & Quality | 2 | 2 | 6 | 10 |
|
|
319
|
+
| Security | 0 | 1 | 2 | 3 |
|
|
320
|
+
| Performance | 0 | 1 | 4 | 5 |
|
|
321
|
+
| Test Gaps | 2 | 3 | 6 | 11 |
|
|
322
|
+
| Edge Cases | 0 | 0 | 5 | 5 |
|
|
323
|
+
| Divergences | 0 | 1 | 4 | 5 |
|
|
324
|
+
| **Total** | **4** | **8** | **27** | **39** |
|
|
@@ -13,6 +13,56 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.16.0] - 2026-04-29
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `--parse-tree` CLI flag: parse a script into an Abstract Syntax Tree and print a visual tree structure showing block nesting (IF/LOOP/BATCH/SCRIPT), source line ranges, compound conditions (ANDIF/ORIF), and all metacommands. Requires no database connection or configuration.
|
|
21
|
+
- AST parser module (`execsql.script.parser`) with `parse_script()` and `parse_string()` entry points. Produces a structured `Script` tree with typed nodes for all block constructs (IfBlock, LoopBlock, BatchBlock, ScriptBlock, SqlBlock, IncludeDirective).
|
|
22
|
+
- AST node definitions (`execsql.script.ast`) with `format_tree()` for human-readable tree output.
|
|
23
|
+
- AST-based execution engine is now the default (and only) engine. Scripts are parsed into a tree of typed nodes, then walked for execution. INCLUDE'd files are parsed and executed natively with circular-include detection. Control flow (IF/LOOP/BATCH) is driven by tree structure.
|
|
24
|
+
- `active_context()` context manager in `execsql.state` for installing an isolated `RuntimeContext` as the active global context within a `with` block.
|
|
25
|
+
- Plugin system (`execsql.plugins`) for extending execsql with custom metacommands, export formats, and import formats via Python entry points. Entry point groups: `execsql.metacommands`, `execsql.exporters`, `execsql.importers`. Plugins are discovered automatically at startup.
|
|
26
|
+
- `--list-plugins` CLI flag to show all discovered plugins and exit.
|
|
27
|
+
- Python library API: `from execsql import run` for programmatic script execution from notebooks, pipelines, and applications. Returns a `ScriptResult` with success/failure, command count, timing, errors, and final variable state. Supports DSN connection strings, pre-existing connections, substitution variables, and error control. Full RuntimeContext isolation between calls.
|
|
28
|
+
- AST `Comment` node: the parser now preserves SQL comments in the tree. Consecutive single-line `--` comments are grouped into one node; `/* */` block comments are captured as single nodes. The `--parse-tree` output includes `<CMT>` tagged comment nodes.
|
|
29
|
+
- `--parse-tree` visual improvements: color-coded type tags (`<SQL>`, `<CMD>`, `<CMT>`, `<IF>`, `<LOOP>`, etc.), dimmed line numbers, and content truncation for cleaner output.
|
|
30
|
+
- Deprecation warning emitted when `enc_password` is used in config files, advising users to switch to keyring or environment variables.
|
|
31
|
+
- Sensitive environment variables (`*SECRET*`, `*TOKEN*`, `*PASSWORD*`, etc.) are now filtered from automatic substitution variable exposure.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Execution engine replaced.** The legacy flat command-list engine has been replaced by the AST-based executor. Scripts are now parsed into a tree of typed nodes and executed by walking the tree. INCLUDE'd files are parsed and executed natively with circular-include detection. All metacommands, SQL, and control flow work identically. This change is transparent to users.
|
|
36
|
+
- **BREAK outside LOOP is now an error.** `BREAK` outside a loop block now raises an error (exit 1) instead of being silently ignored. This catches script bugs that were previously unreported.
|
|
37
|
+
- `--lint` now uses the AST parser for structural validation. Unmatched IF/LOOP/BATCH/SCRIPT blocks are caught at parse time with precise source line ranges. No database connection or runtime state initialization is required. All prior lint checks (variable analysis, INCLUDE file existence, EXECUTE SCRIPT resolution, SUB_INI reading) are preserved.
|
|
38
|
+
- Export format dispatch logic (`EXPORT` and `EXPORT QUERY` metacommands) refactored from duplicated ~180-line if/elif chains into shared `_dispatch_format()` function, eliminating code duplication and fixing missing zip-compatibility checks for `EXPORT QUERY`.
|
|
39
|
+
- `MailSpec.send()` refactored: extracted `_expand()` helper to replace 12 repetitive substitution lines.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **[Critical]** `WriteSpec.write()` and `MailSpec.send()` error-recovery paths crashed because `SubVarSet.substitute_all()` returns `(str, bool)` but callers treated the return as a plain string. All 14 call sites now unpack the tuple correctly.
|
|
44
|
+
- **[Critical]** Error-recovery fallback in `WriteSpec.write()` and `io_write` called `.encode()` producing bytes passed to `sys.stdout.write()` which expects `str`. Removed the `.encode()` calls.
|
|
45
|
+
- `WriteSpec.write()` no longer crashes with `IndexError` when `commandliststack` is empty during early initialization errors.
|
|
46
|
+
- SQL injection vector in `exec_cmd()` across all 8 database adapters — stored procedure/function/view names are now quoted with `quote_identifier()`.
|
|
47
|
+
- `DSN` and `SQL Server` adapters no longer encode SQL strings to bytes before execution.
|
|
48
|
+
- Duplicate tuple entries in export format checks (`"txt-and"` and `"text-and"` each appeared twice).
|
|
49
|
+
- Database adapters now clear `self.password` after successful connection, reducing credential exposure window.
|
|
50
|
+
- Removed unused `_DEFAULT_CTX = RuntimeContext()` allocation in `state.py`.
|
|
51
|
+
- Version bump commits no longer skip pre-commit hooks (`--no-verify` removed from bumpversion config).
|
|
52
|
+
- `SubVarSet.substitute_all()` now enforces a 100-iteration depth limit to prevent infinite loops from cyclic variable references. The per-statement guard in the executor already had this protection, but direct callers (e.g. config loading) did not.
|
|
53
|
+
- `ConfigData.export_output_dir` is now declared in `__init__` with a default of `None` instead of being dynamically added in the CLI entry point.
|
|
54
|
+
- `Encrypt.ky` key table is now an immutable `MappingProxyType` instead of a mutable class-level dict.
|
|
55
|
+
- `JsonDatatype` attributes are now declared as class variables in the class body instead of assigned externally after class definition.
|
|
56
|
+
- `minimal_conf` test fixture expanded with commonly needed attributes (`import_encoding`, `script_encoding`, `export_output_dir`, `write_prefix`, `write_suffix`, `fold_col_hdrs`, `trim_col_hdrs`, etc.) to reduce ad-hoc attribute additions in individual tests.
|
|
57
|
+
|
|
58
|
+
### Removed
|
|
59
|
+
|
|
60
|
+
- `--ast` / `--no-ast` CLI flag — the AST executor is now the only execution engine; no opt-out.
|
|
61
|
+
- Legacy flat command-list execution engine (`_parse_script_lines`, `read_sqlfile`, `read_sqlstring`, `runscripts`, `ScriptFile`, `CommandListWhileLoop`, `CommandListUntilLoop`, `ScriptExecSpec.execute()`).
|
|
62
|
+
- Legacy `_execute_script_direct()` function and `_execute_include_legacy()` fallback path.
|
|
63
|
+
|
|
64
|
+
______________________________________________________________________
|
|
65
|
+
|
|
16
66
|
## [2.15.11] - 2026-04-27
|
|
17
67
|
|
|
18
68
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.16.0
|
|
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
|
|
@@ -237,6 +237,8 @@ execsql script.sql # read connection from config file
|
|
|
237
237
|
| `--output-dir DIR` | Default base directory for EXPORT output files |
|
|
238
238
|
| `--dry-run` | Parse the script and report commands without executing |
|
|
239
239
|
| `--lint` | Static analysis: check structure and warn on issues (no DB) |
|
|
240
|
+
| `--parse-tree` | Print the script's AST structure and exit (no DB) |
|
|
241
|
+
| `--list-plugins` | List discovered plugins and exit |
|
|
240
242
|
| `--ping` | Test database connectivity and exit |
|
|
241
243
|
| `--profile` | Show per-statement timing summary after execution |
|
|
242
244
|
| `--progress` | Show a progress bar for long-running IMPORT operations |
|
|
@@ -260,6 +262,61 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
|
|
|
260
262
|
- Display query results in a GUI dialog; optionally prompt the user to select a row, enter a value, or submit a form.
|
|
261
263
|
- Write status messages or tabular output to the console or a file during execution.
|
|
262
264
|
- Automatically log each run, recording databases used, scripts executed, and user responses.
|
|
265
|
+
- Extend with custom metacommands, exporters, and importers via the plugin system.
|
|
266
|
+
|
|
267
|
+
# Library API
|
|
268
|
+
|
|
269
|
+
execsql can be used as a Python library for programmatic script execution:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from execsql import run
|
|
273
|
+
|
|
274
|
+
# Execute a script file
|
|
275
|
+
result = run(script="pipeline.sql", dsn="postgresql://user:pass@host/db")
|
|
276
|
+
|
|
277
|
+
# Execute inline SQL
|
|
278
|
+
result = run(
|
|
279
|
+
sql="CREATE TABLE t (id INT);\nINSERT INTO t VALUES (1);",
|
|
280
|
+
dsn="sqlite:///my.db",
|
|
281
|
+
new_db=True,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# With substitution variables
|
|
285
|
+
result = run(
|
|
286
|
+
script="etl.sql",
|
|
287
|
+
dsn="sqlite:///data.db",
|
|
288
|
+
variables={"SCHEMA": "public", "DATE": "2026-01-01"},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Check results
|
|
292
|
+
print(result.success) # True
|
|
293
|
+
print(result.commands_run) # 2
|
|
294
|
+
print(result.elapsed) # 0.003 (seconds)
|
|
295
|
+
print(result.variables) # {"SCHEMA": "public", ...}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Error handling:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
result = run(sql="SELECT * FROM nonexistent;", dsn="sqlite:///:memory:")
|
|
302
|
+
if not result.success:
|
|
303
|
+
for err in result.errors:
|
|
304
|
+
print(f"{err.source}:{err.line}: {err.message}")
|
|
305
|
+
|
|
306
|
+
# Or raise on failure
|
|
307
|
+
result.raise_on_error() # raises ExecSqlError
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Use a pre-existing database connection instead of a DSN:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
from execsql.db.factory import db_SQLite
|
|
314
|
+
conn = db_SQLite("my.db", new_db=True)
|
|
315
|
+
result = run(sql="SELECT 1;", connection=conn)
|
|
316
|
+
# run() does NOT close this connection — you manage its lifecycle
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Each call to `run()` uses an isolated `RuntimeContext`, so multiple calls do not share state.
|
|
263
320
|
|
|
264
321
|
# An Illustration
|
|
265
322
|
|
|
@@ -115,6 +115,8 @@ execsql script.sql # read connection from config file
|
|
|
115
115
|
| `--output-dir DIR` | Default base directory for EXPORT output files |
|
|
116
116
|
| `--dry-run` | Parse the script and report commands without executing |
|
|
117
117
|
| `--lint` | Static analysis: check structure and warn on issues (no DB) |
|
|
118
|
+
| `--parse-tree` | Print the script's AST structure and exit (no DB) |
|
|
119
|
+
| `--list-plugins` | List discovered plugins and exit |
|
|
118
120
|
| `--ping` | Test database connectivity and exit |
|
|
119
121
|
| `--profile` | Show per-statement timing summary after execution |
|
|
120
122
|
| `--progress` | Show a progress bar for long-running IMPORT operations |
|
|
@@ -138,6 +140,61 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
|
|
|
138
140
|
- Display query results in a GUI dialog; optionally prompt the user to select a row, enter a value, or submit a form.
|
|
139
141
|
- Write status messages or tabular output to the console or a file during execution.
|
|
140
142
|
- Automatically log each run, recording databases used, scripts executed, and user responses.
|
|
143
|
+
- Extend with custom metacommands, exporters, and importers via the plugin system.
|
|
144
|
+
|
|
145
|
+
# Library API
|
|
146
|
+
|
|
147
|
+
execsql can be used as a Python library for programmatic script execution:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from execsql import run
|
|
151
|
+
|
|
152
|
+
# Execute a script file
|
|
153
|
+
result = run(script="pipeline.sql", dsn="postgresql://user:pass@host/db")
|
|
154
|
+
|
|
155
|
+
# Execute inline SQL
|
|
156
|
+
result = run(
|
|
157
|
+
sql="CREATE TABLE t (id INT);\nINSERT INTO t VALUES (1);",
|
|
158
|
+
dsn="sqlite:///my.db",
|
|
159
|
+
new_db=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# With substitution variables
|
|
163
|
+
result = run(
|
|
164
|
+
script="etl.sql",
|
|
165
|
+
dsn="sqlite:///data.db",
|
|
166
|
+
variables={"SCHEMA": "public", "DATE": "2026-01-01"},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Check results
|
|
170
|
+
print(result.success) # True
|
|
171
|
+
print(result.commands_run) # 2
|
|
172
|
+
print(result.elapsed) # 0.003 (seconds)
|
|
173
|
+
print(result.variables) # {"SCHEMA": "public", ...}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Error handling:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
result = run(sql="SELECT * FROM nonexistent;", dsn="sqlite:///:memory:")
|
|
180
|
+
if not result.success:
|
|
181
|
+
for err in result.errors:
|
|
182
|
+
print(f"{err.source}:{err.line}: {err.message}")
|
|
183
|
+
|
|
184
|
+
# Or raise on failure
|
|
185
|
+
result.raise_on_error() # raises ExecSqlError
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Use a pre-existing database connection instead of a DSN:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from execsql.db.factory import db_SQLite
|
|
192
|
+
conn = db_SQLite("my.db", new_db=True)
|
|
193
|
+
result = run(sql="SELECT 1;", connection=conn)
|
|
194
|
+
# run() does NOT close this connection — you manage its lifecycle
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Each call to `run()` uses an isolated `RuntimeContext`, so multiple calls do not share state.
|
|
141
198
|
|
|
142
199
|
# An Illustration
|
|
143
200
|
|
|
@@ -29,6 +29,8 @@ ______________________________________________________________________
|
|
|
29
29
|
| `--profile-limit N` | Number of top statements to display in the `--profile` summary (default: 20). Remaining statements are counted and noted in the output footer. |
|
|
30
30
|
| `--ping` | Test database connectivity and exit. Connects using the supplied connection parameters, queries for the server version, and prints a one-line success message (exit 0) or the error (exit 1). No script file argument is required. |
|
|
31
31
|
| `--lint` | Parse the script and perform static analysis without connecting to a database. Reports unmatched IF/ENDIF, LOOP/END LOOP, and BEGIN BATCH/END BATCH blocks (errors); potentially undefined `!!$VAR!!` references (warnings); missing INCLUDE file targets (warnings); and unknown `EXECUTE SCRIPT` targets (warnings). Variable analysis uses two passes so definition order does not matter. The linter descends into named script blocks reached via `EXECUTE SCRIPT` / `EXEC SCRIPT` / `RUN SCRIPT`, reads `SUB_INI` INI files at lint time, recognizes `SUB_EMPTY` / `SUB_ADD` / `SUB_APPEND` / `SUBDATA` as definitions, suppresses false warnings for `$COUNTER_N`, and auto-discovers built-in system variables from the installed source. Exits 0 if no errors, 1 if errors found. |
|
|
32
|
+
| `--parse-tree` | Parse the script into an Abstract Syntax Tree and print a visual tree showing block nesting (IF/LOOP/BATCH/SCRIPT), source line ranges, compound conditions (ANDIF/ORIF), and all metacommands. Does not connect to a database or execute anything. Useful for understanding script structure and verifying the parser handles a script correctly. |
|
|
33
|
+
| `--list-plugins` | List all discovered plugins (metacommands, exporters, importers) from Python entry points and exit. Plugins extend execsql via `execsql.metacommands`, `execsql.exporters`, and `execsql.importers` entry point groups. |
|
|
32
34
|
|
|
33
35
|
### Export Formats
|
|
34
36
|
|
|
@@ -94,10 +96,11 @@ New options in `execsql.conf`:
|
|
|
94
96
|
|
|
95
97
|
### Authentication
|
|
96
98
|
|
|
97
|
-
| Feature
|
|
98
|
-
|
|
|
99
|
-
| OS keyring integration
|
|
100
|
-
| Keyring retry on auth failure
|
|
99
|
+
| Feature | Description |
|
|
100
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| OS keyring integration | When the `keyring` package is installed, passwords are stored in and retrieved from the OS credential store (macOS Keychain, Windows Credential Manager, Linux SecretService). |
|
|
102
|
+
| Keyring retry on auth failure | If a stored password is rejected, the stale entry is deleted, the user is re-prompted, and the new password is saved automatically. |
|
|
103
|
+
| Keyring auto-store in GUI mode | When a password is entered via a GUI prompt (Tkinter or Textual), it is stored to the OS keyring automatically without an explicit confirmation prompt. CLI password prompts behave the same way. |
|
|
101
104
|
|
|
102
105
|
### Logging Enhancements
|
|
103
106
|
|
|
@@ -158,6 +161,34 @@ execsql2 adds a full interactive debugging system that has no equivalent in upst
|
|
|
158
161
|
- **ANSI color output** — the REPL uses ANSI color on TTY outputs: bold yellow for section labels, cyan for filenames and variable names, dim for separators and `=` signs, red for error messages, bold for SQL column headers, and dim italic for `NULL` values. Color is suppressed when `NO_COLOR` or `EXECSQL_NO_COLOR` environment variables are set, or when the output stream is not a TTY.
|
|
159
162
|
- **Readline support** — on platforms where `readline` is available (macOS, Linux), the REPL supports arrow-key history navigation and line editing.
|
|
160
163
|
|
|
164
|
+
### Library API
|
|
165
|
+
|
|
166
|
+
execsql2 provides a Python library API for programmatic script execution — no CLI needed. The upstream execsql has no equivalent.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from execsql import run
|
|
170
|
+
|
|
171
|
+
result = run(
|
|
172
|
+
script="pipeline.sql",
|
|
173
|
+
dsn="postgresql://user:pass@host/db",
|
|
174
|
+
variables={"SCHEMA": "public"},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
print(result.success) # True/False
|
|
178
|
+
print(result.commands_run) # number of statements executed
|
|
179
|
+
print(result.errors) # list of ScriptError objects
|
|
180
|
+
print(result.variables) # final substitution variable state
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Key features:**
|
|
184
|
+
|
|
185
|
+
- **DSN or connection** — pass a DSN string (`dsn="sqlite:///my.db"`) or a pre-existing `Database` object (`connection=conn`).
|
|
186
|
+
- **Substitution variables** — pass a `variables` dict; keys are automatically `$`-prefixed.
|
|
187
|
+
- **Error control** — `halt_on_error=True` (default) stops on the first error; `halt_on_error=False` captures errors and continues.
|
|
188
|
+
- **Isolation** — each `run()` call uses an isolated `RuntimeContext`. Multiple calls do not share state.
|
|
189
|
+
- **Result object** — `ScriptResult` is a frozen dataclass with `success`, `commands_run`, `elapsed`, `errors`, and `variables`.
|
|
190
|
+
- **Exception convenience** — call `result.raise_on_error()` to raise `ExecSqlError` if the script failed.
|
|
191
|
+
|
|
161
192
|
______________________________________________________________________
|
|
162
193
|
|
|
163
194
|
## Changed Behavior
|
|
@@ -192,6 +223,17 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
|
|
|
192
223
|
- **`Database` is an ABC** — `open_db()` and `exec_cmd()` are abstract methods. Subclasses that omit them raise `TypeError` at instantiation instead of at call time.
|
|
193
224
|
- **Connection timeouts** — PostgreSQL and SQLite adapters accept a connection timeout parameter (default 30 seconds).
|
|
194
225
|
- **DuckDB temporal types** — `TIMESTAMPTZ`, `TIMESTAMP`, `DATE`, `TIME` now map to native DuckDB types instead of `TEXT`.
|
|
226
|
+
- **SQLite `DT_Long` mapping** — `DT_Long` maps to `"hugeint"` in the SQLite type table. SQLite does not have a native `HUGEINT` type; the value receives `TEXT` affinity. In practice this is harmless because SQLite's type affinity system handles large integers transparently, but the mapping name differs from upstream.
|
|
227
|
+
|
|
228
|
+
### Execution Engine
|
|
229
|
+
|
|
230
|
+
The legacy flat command-list engine (`_parse_script_lines` / `runscripts` / `CommandList.run_next()`) has been replaced by an AST-based execution engine. Scripts are parsed into a tree of typed nodes, then the tree is walked for execution. This change is transparent to users — all metacommands, SQL, and control flow work identically.
|
|
231
|
+
|
|
232
|
+
- **Native INCLUDE handling** — The AST executor parses INCLUDE'd files with the AST parser and executes them through the tree-walking executor. Control flow structures (IF/LOOP/BATCH/SCRIPT) in included files are fully tree-driven, with correct deferred variable handling and source-span error reporting.
|
|
233
|
+
- **Circular INCLUDE detection** — The executor tracks the full include chain and detects circular references (e.g. A includes B includes A), reporting the full chain in the error message. Upstream has no such detection.
|
|
234
|
+
- **BREAK outside LOOP is an error** — `BREAK` outside a loop block now raises an error (exit 1) instead of being silently ignored. This catches script bugs that upstream would not report.
|
|
235
|
+
- **Instance-scoped script registry** — Named SCRIPT blocks are stored on the `RuntimeContext` instance instead of a module-level dict, preventing cross-execution contamination.
|
|
236
|
+
- **Script source directory resolution** — `ScriptCmd` resolves `source_dir` at construction time (when the command is parsed) rather than per-statement at execution time. This is functionally equivalent for all normal usage since the script file does not move between parse and execution.
|
|
195
237
|
|
|
196
238
|
### Error Handling
|
|
197
239
|
|
|
@@ -4,7 +4,27 @@ The pages in this section are auto-generated from the source docstrings and show
|
|
|
4
4
|
|
|
5
5
|
If you want to **extend** execsql — add a new exporter format, support a new database, or add an importer for a file type — start with the Contributing guides, which give you step-by-step walkthroughs and copy-paste skeletons. The API pages here serve as the detailed reference those guides link to.
|
|
6
6
|
|
|
7
|
-
For a high-level overview of how all the pieces fit together, start with the [Architecture & Design Guide](../dev/architecture.md).
|
|
7
|
+
For programmatic use, see the [Library API](#library-api) section below. For a high-level overview of how all the pieces fit together, start with the [Architecture & Design Guide](../dev/architecture.md).
|
|
8
|
+
|
|
9
|
+
## Library API
|
|
10
|
+
|
|
11
|
+
The primary public API is `execsql.run()`:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from execsql import run, ScriptResult, ScriptError, ExecSqlError
|
|
15
|
+
|
|
16
|
+
result: ScriptResult = run(
|
|
17
|
+
script="pipeline.sql", # or sql="SELECT 1;"
|
|
18
|
+
dsn="sqlite:///my.db", # or connection=existing_db_object
|
|
19
|
+
variables={"KEY": "value"}, # optional substitution variables
|
|
20
|
+
halt_on_error=True, # stop on first error (default)
|
|
21
|
+
new_db=False, # create DB if missing
|
|
22
|
+
)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
See the [README](https://github.com/geocoug/execsql#library-api) for full examples.
|
|
26
|
+
|
|
27
|
+
## Extension Guides
|
|
8
28
|
|
|
9
29
|
| Extension type | Guide | API reference |
|
|
10
30
|
| -------------------- | -------------------------------------------------------- | ------------------------------- |
|