execsql2 2.15.8__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.15.8 → execsql2-2.16.0}/.gitignore +2 -0
- execsql2-2.16.0/AUDIT.md +324 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/CHANGELOG.md +92 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/PKG-INFO +59 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/README.md +58 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/about/divergence.md +48 -4
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/index.md +21 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/adding_metacommands.md +11 -3
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/architecture.md +31 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/getting-started/syntax.md +16 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/reference/configuration.md +6 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/reference/substitution_vars.md +3 -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.8 → execsql2-2.16.0}/justfile +45 -10
- {execsql2-2.15.8 → execsql2-2.16.0}/pyproject.toml +5 -3
- execsql2-2.16.0/src/execsql/__init__.py +21 -0
- execsql2-2.16.0/src/execsql/api.py +580 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/__init__.py +123 -0
- execsql2-2.16.0/src/execsql/cli/lint_ast.py +439 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/run.py +113 -102
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/config.py +29 -4
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/access.py +1 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/base.py +4 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/dsn.py +3 -2
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/duckdb.py +1 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/factory.py +3 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/firebird.py +2 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/mysql.py +2 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/oracle.py +2 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/postgres.py +2 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/sqlite.py +1 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/sqlserver.py +3 -2
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/debug/repl.py +27 -10
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/base.py +6 -4
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/delimited.py +11 -3
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/pretty.py +9 -12
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/tui.py +59 -2
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/__init__.py +3 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/conditions.py +20 -2
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/connect.py +1 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/control.py +8 -14
- {execsql2-2.15.8 → 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.8 → execsql2-2.16.0}/src/execsql/metacommands/io_fileops.py +7 -13
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/io_write.py +1 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/script_ext.py +8 -5
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/upsert.py +40 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/models.py +8 -12
- execsql2-2.16.0/src/execsql/plugins.py +414 -0
- {execsql2-2.15.8 → 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.8 → 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.8 → execsql2-2.16.0}/src/execsql/script/variables.py +11 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/state.py +55 -2
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/crypto.py +14 -10
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/errors.py +31 -8
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/gui.py +139 -17
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/mail.py +15 -12
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_cli.py +214 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_cli_run.py +18 -151
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_profile.py +4 -4
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/conftest.py +16 -1
- execsql2-2.16.0/tests/db/test_db_adapters_mocked.py +546 -0
- execsql2-2.16.0/tests/db/test_dsn.py +365 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_base.py +15 -13
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/gui/test_backends.py +16 -5
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_csv_edge_cases.py +97 -1
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_breakpoint.py +7 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands.py +4 -3
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_extended.py +8 -37
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_fileops_extra.py +4 -20
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_io.py +5 -21
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_script_ext.py +5 -11
- execsql2-2.16.0/tests/scripts/fixtures/control_flow.sql +178 -0
- execsql2-2.16.0/tests/scripts/fixtures/io_roundtrip.sql +135 -0
- execsql2-2.16.0/tests/scripts/fixtures/parse_only/parse_tree.sql +541 -0
- execsql2-2.16.0/tests/scripts/fixtures/smoke.sql +138 -0
- execsql2-2.16.0/tests/scripts/test_sql_scripts.py +67 -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.8 → execsql2-2.16.0}/tests/test_config_data.py +61 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_debug_repl.py +7 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_engine.py +15 -630
- execsql2-2.16.0/tests/test_executor.py +939 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_format.py +183 -0
- execsql2-2.16.0/tests/test_parser.py +814 -0
- execsql2-2.16.0/tests/test_plugins.py +213 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_script.py +8 -0
- execsql2-2.16.0/tests/utils/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_errors_extra.py +4 -4
- {execsql2-2.15.8 → execsql2-2.16.0}/uv.lock +1 -1
- execsql2-2.15.8/src/execsql/__init__.py +0 -16
- execsql2-2.15.8/src/execsql/metacommands/io_export.py +0 -523
- execsql2-2.15.8/tests/test_parser.py +0 -446
- {execsql2-2.15.8 → execsql2-2.16.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/.pre-commit-config.yaml +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/.python-version +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/.readthedocs.yaml +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/CONTRIBUTING.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/LICENSE.txt +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/NOTICE +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/SECURITY.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/about/contributors.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/about/copyright.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/cli.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/db.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/exporters.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/importers.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/api/metacommands.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/getting-started/installation.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/debugging.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/documentation.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/encoding.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/examples.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/formatter.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/logging.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/usage.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/actions.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/actions2.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/checkboxes.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/connect.b64 +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/connect.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/create_conf.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/entry_form.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/execsql_console.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/fatals.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/logo_small.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/unmatched.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/index.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/reference/metacommands.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/docs/reference/security.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/__main__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/help.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exceptions.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/format.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/base.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/console.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/base.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/json.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/parser.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/py.typed +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/script/control.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/types.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/README.md +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/config_settings.sqlite +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/execsql.conf +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/make_config_db.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/md_compare.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/md_glossary.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/md_upsert.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/pg_compare.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/pg_glossary.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/pg_upsert.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/script_template.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/ss_compare.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/ss_glossary.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/templates/ss_upsert.sql +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_lint.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/cli/test_ping.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_base.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_factory.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_postgres.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_db.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_html_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_json.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_json_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_latex_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_pretty_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_raw_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_templates_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_values_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/gui/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/gui/test_compare_stats.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/gui/test_compute_row_diffs.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_base_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_json_importer.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/conftest.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_pg_upsert.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.15.8/tests/utils → execsql2-2.16.0/tests/scripts}/__init__.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_config.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_config_extended.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_error_messages.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_exceptions.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_mail.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_models.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_package.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_registry.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_state.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/test_types.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_auth.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_errors.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_regex.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_strings.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_timer.py +0 -0
- {execsql2-2.15.8 → execsql2-2.16.0}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.15.8 → 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,98 @@ ______________________________________________________________________
|
|
|
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
|
+
|
|
66
|
+
## [2.15.11] - 2026-04-27
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- `PAUSE` console-mode fallback now checks `sys.platform` before attempting POSIX terminal imports, preventing hangs on Windows when stdin reports as a TTY.
|
|
71
|
+
|
|
72
|
+
______________________________________________________________________
|
|
73
|
+
|
|
74
|
+
## [2.15.10] - 2026-04-27
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
|
|
78
|
+
- `--config FILE` CLI flag to specify an explicit configuration file. The file is loaded after the implicit search paths (system, user, script-dir, working-dir) so its values take precedence, while CLI arguments still override everything.
|
|
79
|
+
- `$HOSTNAME` system substitution variable — the network name of the machine running execsql, useful for log messages and environment detection.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- Config file chaining no longer mutates a list during iteration; uses a deque for safe, predictable processing order.
|
|
84
|
+
- REPL `_use_color()` result is now cached instead of re-checking environment variables and TTY status on every colorized output.
|
|
85
|
+
- `DatabasePool.closeall()` no longer calls `self.__init__()` to reset state; fields are reset directly to avoid the re-initialization anti-pattern.
|
|
86
|
+
- `PAUSE` console mode no longer crashes on Windows CI due to unconditional `import termios`; POSIX-only imports are now guarded by the TTY fallback check.
|
|
87
|
+
- `HAS_ROWS()`, `ROW_COUNT_GT()`, `ROW_COUNT_GTE()`, `ROW_COUNT_EQ()`, and `ROW_COUNT_LT()` condition predicates now quote table names with standard SQL identifier quoting, preventing potential SQL injection when table names originate from substitution variables.
|
|
88
|
+
- Corrected `__init__.py` module docstring that incorrectly described the CLI entry point as `execsql2` (the command is `execsql`).
|
|
89
|
+
- Added note to configuration reference clarifying that `--output-dir` is a CLI-only option with no equivalent configuration file setting.
|
|
90
|
+
|
|
91
|
+
______________________________________________________________________
|
|
92
|
+
|
|
93
|
+
## [2.15.9] - 2026-04-27
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- Textual TUI now displays a progress bar and remaining-time countdown for `PROMPT PAUSE` and `PAUSE` dialogs when the `CONTINUE AFTER` or `HALT AFTER` keywords specify a timed duration (matching existing Tkinter behavior).
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
|
|
101
|
+
- `PAUSE` metacommand in console mode (no `-v`) now responds to single keypresses (Enter to continue, Esc to quit) instead of requiring Enter after every key. Uses raw-mode terminal reading on POSIX and `msvcrt` polling on Windows.
|
|
102
|
+
- `PAUSE` with `CONTINUE AFTER`/`HALT AFTER` in console mode now displays a live SIGALRM-driven progress bar showing time remaining, matching the documented behavior and terminal screenshot.
|
|
103
|
+
- `PAUSE` progress bar output no longer bleeds into subsequent script output — the progress line is cleared before returning.
|
|
104
|
+
- Fixed double minutes-to-seconds conversion in the console `PAUSE` path that caused a 1-minute pause to sleep for 60 minutes.
|
|
105
|
+
|
|
106
|
+
______________________________________________________________________
|
|
107
|
+
|
|
16
108
|
## [2.15.8] - 2026-04-20
|
|
17
109
|
|
|
18
110
|
### Added
|
|
@@ -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,9 +237,12 @@ 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 |
|
|
245
|
+
| `--config FILE` | Load an explicit config file (highest priority after CLI args) |
|
|
243
246
|
| `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
|
|
244
247
|
| `--dump-keywords` | Print metacommand keywords as JSON and exit |
|
|
245
248
|
| `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
|
|
@@ -259,6 +262,61 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
|
|
|
259
262
|
- Display query results in a GUI dialog; optionally prompt the user to select a row, enter a value, or submit a form.
|
|
260
263
|
- Write status messages or tabular output to the console or a file during execution.
|
|
261
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.
|
|
262
320
|
|
|
263
321
|
# An Illustration
|
|
264
322
|
|
|
@@ -115,9 +115,12 @@ 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 |
|
|
123
|
+
| `--config FILE` | Load an explicit config file (highest priority after CLI args) |
|
|
121
124
|
| `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
|
|
122
125
|
| `--dump-keywords` | Print metacommand keywords as JSON and exit |
|
|
123
126
|
| `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
|
|
@@ -137,6 +140,61 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
|
|
|
137
140
|
- Display query results in a GUI dialog; optionally prompt the user to select a row, enter a value, or submit a form.
|
|
138
141
|
- Write status messages or tabular output to the console or a file during execution.
|
|
139
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.
|
|
140
198
|
|
|
141
199
|
# An Illustration
|
|
142
200
|
|