execsql2 2.12.0__tar.gz → 2.12.2__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.12.2/.claude/agents/liaison.md +107 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/CHANGELOG.md +28 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/CLAUDE.md +1 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/PKG-INFO +2 -1
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/about/divergence.md +1 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/getting-started/installation.md +4 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/reference/metacommands.md +3 -1
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/reference/security.md +59 -1
- {execsql2-2.12.0 → execsql2-2.12.2}/pyproject.toml +3 -2
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/base.py +37 -23
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exceptions.py +2 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/control.py +1 -1
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/script/engine.py +13 -4
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/script/variables.py +2 -25
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_base.py +5 -5
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_assert.py +14 -1
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_engine.py +3 -4
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_exceptions.py +1 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_script.py +12 -16
- {execsql2-2.12.0 → execsql2-2.12.2}/uv.lock +1 -1
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/dba.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/herald.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/inspector.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/oracle.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/patcher.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/qa.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/scribe.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/code-oracle.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/migrate.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/review-changes.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/test-module.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/update-changelog.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/where-is.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/project_context.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.claude/state/status.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.gitignore +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.pre-commit-config.yaml +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.python-version +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/.readthedocs.yaml +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/CONTRIBUTING.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/LICENSE.txt +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/NOTICE +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/README.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/SECURITY.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/about/contributors.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/about/copyright.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/cli.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/db.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/exporters.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/importers.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/index.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/metacommands.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/architecture.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/debugging.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/documentation.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/encoding.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/examples.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/formatter.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/logging.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/usage.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/actions.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/actions2.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/checkboxes.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/connect.b64 +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/connect.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/create_conf.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/entry_form.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/execsql_console.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/fatals.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/logo_small.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/unmatched.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/index.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/reference/configuration.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/justfile +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/__main__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/help.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/run.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/config.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/constants.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/access.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/factory.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/format.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/base.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/console.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/base.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/models.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/parser.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/py.typed +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/script/control.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/state.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/types.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/README.md +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/config_settings.sqlite +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/execsql.conf +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/make_config_db.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/md_compare.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/md_glossary.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/md_upsert.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/pg_compare.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/pg_glossary.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/pg_upsert.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/script_template.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/ss_compare.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/ss_glossary.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/templates/ss_upsert.sql +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_cli.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_lint.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_ping.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_profile.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/conftest.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_factory.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_postgres.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_base.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_db.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_json.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/gui/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/gui/test_backends.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/conftest.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_config.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_config_data.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_constants.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_error_messages.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_format.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_mail.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_models.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_package.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_parser.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_registry.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_state.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_types.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/__init__.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_auth.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_errors.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_regex.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_strings.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_timer.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.12.0 → execsql2-2.12.2}/zensical.toml +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: The Liaison
|
|
3
|
+
description: Integration specialist for pg-upsert and execsql2. Tracks the pg-upsert codebase (../pg-upsert), designs the optional dependency integration, and owns the UPSERT metacommand. Reads both codebases — writes only in execsql2.
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: yellow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a senior Python engineer who specializes in integrating the **pg-upsert** library with the **execsql2** project. You are the bridge between two codebases maintained by the same author:
|
|
9
|
+
|
|
10
|
+
- **execsql2** — `/Users/cgrant/GitHub/geocoug/execsql/` (this repo)
|
|
11
|
+
- **pg-upsert** — `/Users/cgrant/GitHub/geocoug/pg-upsert/` (sibling repo)
|
|
12
|
+
|
|
13
|
+
Your job is to keep the integration plan current, design the `UPSERT` metacommand, and ensure changes in either codebase don't break the integration path.
|
|
14
|
+
|
|
15
|
+
## Expertise
|
|
16
|
+
|
|
17
|
+
You have deep, working knowledge of:
|
|
18
|
+
|
|
19
|
+
**pg-upsert internals:** The `PgUpsert` class (QA checks: null, PK, FK, check constraint validation), `PostgresDB` connection wrapper, the staging-to-base upsert workflow, topological table ordering, the `ups_control` temporary table, and the `psycopg2.sql` parameterized SQL generation patterns.
|
|
20
|
+
|
|
21
|
+
**execsql2 extension points:** The metacommand dispatch system (`MetaCommandList`, `build_dispatch_table()`, `x_*` handlers), `DatabasePool` connection management, substitution variables (`SubVarSet`), transaction/autocommit model, the `PostgresDatabase` adapter, and the existing upsert SQL templates in `templates/pg_upsert.sql`.
|
|
22
|
+
|
|
23
|
+
**Integration patterns:** Optional dependency detection (`importlib.util.find_spec`), connection sharing between libraries, adapter patterns for bridging different transaction models, and graceful degradation when an optional package is not installed.
|
|
24
|
+
|
|
25
|
+
______________________________________________________________________
|
|
26
|
+
|
|
27
|
+
## First Actions (always, before doing any work)
|
|
28
|
+
|
|
29
|
+
1. **Read `.claude/project_context.md`** — load the execsql2 module layout and architectural overview.
|
|
30
|
+
2. **Read your briefing** if one exists at `.claude/comms/briefings/liaison-*.md` — follow the DBA's specific instructions.
|
|
31
|
+
3. **Read the integration plan** at `../pg-upsert/.claude/plans/refactor-and-execsql-integration.md` — this is the canonical plan. Understand the current phase and what's been decided.
|
|
32
|
+
4. **Check pg-upsert's current state** — read `../pg-upsert/src/pg_upsert/upsert.py` and `../pg-upsert/src/pg_upsert/postgres.py` to understand the latest API surface. pg-upsert is under active refactoring; the API may have changed since the plan was written.
|
|
33
|
+
5. **Check execsql2's metacommand system** — read `src/execsql/metacommands/dispatch.py` to understand registration patterns.
|
|
34
|
+
|
|
35
|
+
______________________________________________________________________
|
|
36
|
+
|
|
37
|
+
## Key Design Decisions (established)
|
|
38
|
+
|
|
39
|
+
These have been agreed upon. Do not revisit unless the human asks:
|
|
40
|
+
|
|
41
|
+
1. **pg-upsert is an optional dependency** — execsql2 must work without it installed. The `UPSERT` metacommand raises a clear error if pg-upsert is missing.
|
|
42
|
+
2. **Connection sharing** — the metacommand passes execsql's existing `psycopg2` connection to `PgUpsert(conn=...)`. No second connection.
|
|
43
|
+
3. **Results map to substitution variables** — upsert stats (rows updated, inserted, QA errors) are exposed as `!!$UPSERT_*!!` substitution variables.
|
|
44
|
+
4. **execsql's transaction model governs** — the metacommand respects execsql's `AUTOCOMMIT ON/OFF` state, not pg-upsert's `do_commit` flag.
|
|
45
|
+
5. **No Tkinter dependency** — the interactive GUI is not used when called from execsql. QA results go to execsql's logging/debug REPL instead.
|
|
46
|
+
|
|
47
|
+
______________________________________________________________________
|
|
48
|
+
|
|
49
|
+
## pg-upsert API Surface (reference)
|
|
50
|
+
|
|
51
|
+
The integration depends on these pg-upsert interfaces. If any change, the integration plan must be updated.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Core class — this is what the metacommand will call
|
|
55
|
+
PgUpsert(
|
|
56
|
+
conn=psycopg2_connection, # Shared from execsql
|
|
57
|
+
tables=["t1", "t2"],
|
|
58
|
+
staging_schema="staging",
|
|
59
|
+
base_schema="public",
|
|
60
|
+
do_commit=False, # execsql controls commits
|
|
61
|
+
interactive=False, # No GUI from execsql
|
|
62
|
+
upsert_method="upsert", # "upsert" | "update" | "insert"
|
|
63
|
+
exclude_cols=["col1"],
|
|
64
|
+
exclude_null_check_cols=["col2"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Methods the metacommand will call
|
|
68
|
+
.qa_all() # Run all QA checks, returns self
|
|
69
|
+
.upsert_all() # Run upsert on all tables, returns self
|
|
70
|
+
.commit() # Commit/rollback based on do_commit + qa_passed
|
|
71
|
+
.run() # qa_all() → upsert_all() → commit()
|
|
72
|
+
|
|
73
|
+
# State the metacommand will read
|
|
74
|
+
.qa_passed # bool — did all QA checks pass?
|
|
75
|
+
.control_table # str — name of temp table with per-table results
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
______________________________________________________________________
|
|
79
|
+
|
|
80
|
+
## What to Produce
|
|
81
|
+
|
|
82
|
+
Depending on the task, you may produce:
|
|
83
|
+
|
|
84
|
+
1. **Integration status reports** — what's changed in pg-upsert, what that means for execsql integration
|
|
85
|
+
2. **Metacommand design** — regex patterns, handler signatures, SQL syntax for `UPSERT` metacommand
|
|
86
|
+
3. **Implementation code** — `src/execsql/metacommands/upsert.py` and dispatch registration
|
|
87
|
+
4. **Compatibility notes** — API changes in pg-upsert that require adaptation
|
|
88
|
+
5. **Test specifications** — what the QA agent should test for the integration
|
|
89
|
+
|
|
90
|
+
______________________________________________________________________
|
|
91
|
+
|
|
92
|
+
## Syndicate Protocol
|
|
93
|
+
|
|
94
|
+
When working as part of the SQL Syndicate:
|
|
95
|
+
|
|
96
|
+
1. Read your briefing from `.claude/comms/briefings/liaison-*.md`
|
|
97
|
+
2. Do your research across both codebases
|
|
98
|
+
3. Write your report to `.claude/comms/reports/liaison-{YYYY-MM-DD}.md`
|
|
99
|
+
4. Write integration artifacts to `.claude/plans/` (for design) or `.claude/patches/` (for implementation)
|
|
100
|
+
|
|
101
|
+
## Constraints
|
|
102
|
+
|
|
103
|
+
- **Writes only in execsql2.** You may read `../pg-upsert/` freely but never modify files there.
|
|
104
|
+
- **pg-upsert is a moving target.** Always re-read the current pg-upsert source before making claims about its API — don't rely on cached knowledge.
|
|
105
|
+
- **Optional means optional.** Never add pg-upsert to execsql2's required dependencies. Use `extras_require` / optional dependency groups.
|
|
106
|
+
- **Preserve existing upsert templates.** The SQL templates in `templates/pg_upsert.sql` must continue to work. The metacommand is an alternative, not a replacement.
|
|
107
|
+
- Follow all execsql2 project constraints from CLAUDE.md (ruff, Python 3.10+, coverage floor, changelog, docs, divergence tracking).
|
|
@@ -13,6 +13,34 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.12.2] - 2026-04-02
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Documentation: keyring setup guide for headless Linux servers (encrypted and plaintext file backends) in the Security reference page, with a cross-reference from the Installation page.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `ASSERT` failures now report `**** Assertion failed.` instead of `**** Error in metacommand.` to distinguish intentional script-level checks from actual metacommand errors.
|
|
25
|
+
|
|
26
|
+
______________________________________________________________________
|
|
27
|
+
|
|
28
|
+
## [2.12.1] - 2026-04-02
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Performance: removed dead `_compiled_patterns` dict from `SubVarSet` — eliminated 3 unused regex compilations per `add_substitution` call (~20 calls per statement in typical scripts).
|
|
33
|
+
- Performance: cached `source_dir` and `source_name` on `ScriptCmd` at construction time — eliminated per-statement `Path.resolve()` filesystem calls.
|
|
34
|
+
- Performance: `select_rowdict()` now uses batched `fetchmany()` instead of row-at-a-time `fetchone()`, matching `select_rowsource()` behavior for template exports.
|
|
35
|
+
- Performance: removed redundant `$CURRENT_TIME` set in `set_system_vars()` — now set once per statement in `run_and_increment()`.
|
|
36
|
+
- Performance: removed no-op `copy.copy()` on immutable string in `substitute_vars()`.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Fixed cursor leak in `select_rowsource()` — generator now closes the cursor in a `finally` block when exhausted or abandoned.
|
|
41
|
+
|
|
42
|
+
______________________________________________________________________
|
|
43
|
+
|
|
16
44
|
## [2.12.0] - 2026-04-01
|
|
17
45
|
|
|
18
46
|
### Added
|
|
@@ -22,6 +22,7 @@ A multi-agent system where specialized agents collaborate to improve, extend, de
|
|
|
22
22
|
- `.claude/docs-drafts/` — Scribe's documentation drafts
|
|
23
23
|
- `.claude/releases/` — Herald's release notes and changelog entries
|
|
24
24
|
- `.claude/state/` — Shared state (current phase, active task, agent status)
|
|
25
|
+
- `../pg-upsert/.claude/plans/` — pg-upsert integration plan (read by The Liaison)
|
|
25
26
|
|
|
26
27
|
## Communication Protocol
|
|
27
28
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.12.
|
|
3
|
+
Version: 2.12.2
|
|
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
|
+
Project-URL: Homepage, https://execsql2.readthedocs.io
|
|
5
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
6
7
|
Project-URL: Issues, https://github.com/geocoug/execsql/issues
|
|
7
8
|
Author-email: Dreas Nielsen <cortice@tutanota.com>
|
|
@@ -174,6 +174,7 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
|
|
|
174
174
|
|
|
175
175
|
- **Exception hierarchy** — All custom exceptions inherit from `ExecSqlError`, enabling `except ExecSqlError` to catch any execsql-originated error.
|
|
176
176
|
- **Exception chaining** — All `raise` statements inside `except` blocks preserve the original traceback via `from`.
|
|
177
|
+
- **ASSERT error type** — `ASSERT` failures now use a dedicated `"assert"` error type that produces `**** Assertion failed.` instead of `**** Error in metacommand.`. This distinguishes intentional script-level checks from actual metacommand errors. Upstream did not have `ASSERT`.
|
|
177
178
|
|
|
178
179
|
______________________________________________________________________
|
|
179
180
|
|
|
@@ -30,3 +30,7 @@ pip install "execsql2[all]" # Everything
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
In addition to the *execsql* program itself, additional Python libraries may need to be installed to use *execsql* with specific types of DBMSs and spreadsheets. The additional libraries that may be needed are listed in the [Requirements](requirements.md#requirements) section.
|
|
33
|
+
|
|
34
|
+
!!! tip "Keyring on headless Linux"
|
|
35
|
+
|
|
36
|
+
If you install `execsql2[auth]` on a headless Linux server (no desktop environment), the keyring backend needs manual configuration. See [Keyring Platform Setup](../reference/security.md#keyring_setup) for instructions.
|
|
@@ -56,7 +56,9 @@ ASSERT <condition> "<failure message>"
|
|
|
56
56
|
ASSERT <condition> '<failure message>'
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Evaluates `<condition>` using the same expression engine as [IF](#if_cmd). If the condition is `True`, execution continues silently (and the result is written to the log). If the condition is `False`, an error is raised with the provided failure message. If no message is supplied, the default message is `Assertion failed: <condition>`.
|
|
59
|
+
Evaluates `<condition>` using the same expression engine as [IF](#if_cmd). If the condition is `True`, execution continues silently (and the result is written to the log). If the condition is `False`, an assertion error is raised with the provided failure message. If no message is supplied, the default message is `Assertion failed: <condition>`.
|
|
60
|
+
|
|
61
|
+
A failed assertion produces `**** Assertion failed.` (not `**** Error in metacommand.`) to make it clear that the script's own check caught a problem, not that execsql encountered an internal error.
|
|
60
62
|
|
|
61
63
|
When [HALT_ON_METACOMMAND_ERROR](#config) is `ON` (the default), a failed assertion halts the script. When it is `OFF`, execution continues after the failure is logged.
|
|
62
64
|
|
|
@@ -29,7 +29,7 @@ When execsql needs a database password and none is stored or configured, it prom
|
|
|
29
29
|
|
|
30
30
|
### OS credential store (keyring)
|
|
31
31
|
|
|
32
|
-
When the optional `keyring` package is installed (`pip install execsql2[auth]`), execsql checks the OS credential store before prompting. After a successful interactive prompt, the password is automatically stored for future use.
|
|
32
|
+
When the optional `keyring` package is installed (`pip install execsql2[auth]`), execsql checks the OS credential store before prompting. After a successful interactive prompt, the password is automatically stored for future use. Keyring service names follow the pattern:
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
35
|
execsql/<db_type>/<server>/<database>
|
|
@@ -37,6 +37,64 @@ execsql/<db_type>/<server>/<database>
|
|
|
37
37
|
|
|
38
38
|
To disable keyring integration, set `use_keyring = No` in the `[connect]` section of `execsql.conf`.
|
|
39
39
|
|
|
40
|
+
#### Platform setup { #keyring_setup }
|
|
41
|
+
|
|
42
|
+
**macOS** — Works out of the box. Keyring uses the macOS Keychain automatically.
|
|
43
|
+
|
|
44
|
+
**Windows** — Works out of the box. Keyring uses the Windows Credential Manager automatically.
|
|
45
|
+
|
|
46
|
+
**Linux (desktop with GNOME/KDE)** — Works out of the box if a SecretService provider (GNOME Keyring or KWallet) is running.
|
|
47
|
+
|
|
48
|
+
**Linux (headless / remote / server)** — No secret service is typically available, so keyring silently fails to store passwords. You need to configure a file-based backend. There are two options:
|
|
49
|
+
|
|
50
|
+
##### Option A: Encrypted file backend (recommended) { #keyring_encrypted }
|
|
51
|
+
|
|
52
|
+
Passwords are stored encrypted on disk. Requires a master password the first time keyring is used per session.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install keyrings.alt pycryptodome
|
|
56
|
+
mkdir -p ~/.config/python_keyring
|
|
57
|
+
cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
|
|
58
|
+
[backend]
|
|
59
|
+
default-keyring=keyrings.alt.file.EncryptedKeyring
|
|
60
|
+
EOF
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The encrypted keyring file is stored at `~/.local/share/python_keyring/crypted_pass.cfg`. You will be prompted for a master password once per session (e.g., when you first run execsql after logging in).
|
|
64
|
+
|
|
65
|
+
##### Option B: Plaintext file backend (no prompts) { #keyring_plaintext }
|
|
66
|
+
|
|
67
|
+
Passwords are stored in plain text on disk. No master password is needed — execsql will never prompt for a password after the first successful entry.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install keyrings.alt
|
|
71
|
+
mkdir -p ~/.config/python_keyring
|
|
72
|
+
cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
|
|
73
|
+
[backend]
|
|
74
|
+
default-keyring=keyrings.alt.file.PlaintextKeyring
|
|
75
|
+
EOF
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Passwords are stored at `~/.local/share/python_keyring/keyring_pass.cfg`. Restrict access to this file:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
chmod 600 ~/.local/share/python_keyring/keyring_pass.cfg
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
!!! warning "Plaintext storage"
|
|
85
|
+
|
|
86
|
+
The plaintext backend stores passwords without encryption. Only use this on machines where the filesystem is already secured (encrypted disk, single-user access, restricted permissions). Prefer the encrypted backend when possible.
|
|
87
|
+
|
|
88
|
+
##### Verifying keyring setup { #keyring_verify }
|
|
89
|
+
|
|
90
|
+
After configuring a backend, verify it works:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -c "import keyring; keyring.set_password('test', 'user', 'pw'); print(keyring.get_password('test', 'user'))"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This should print `pw` without errors. If it does, the next time execsql prompts for a database password, it will store it and skip the prompt on future runs.
|
|
97
|
+
|
|
40
98
|
### `enc_password` in execsql.conf
|
|
41
99
|
|
|
42
100
|
!!! warning "Obfuscation only — not encryption"
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.12.
|
|
7
|
+
version = "2.12.2"
|
|
8
8
|
description = "Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = { file = "LICENSE.txt" }
|
|
@@ -91,6 +91,7 @@ execsql = "execsql.cli:_legacy_main"
|
|
|
91
91
|
execsql-format = "execsql.format:main"
|
|
92
92
|
|
|
93
93
|
[project.urls]
|
|
94
|
+
Homepage = "https://execsql2.readthedocs.io"
|
|
94
95
|
Repository = "https://github.com/geocoug/execsql"
|
|
95
96
|
Issues = "https://github.com/geocoug/execsql/issues"
|
|
96
97
|
|
|
@@ -158,7 +159,7 @@ skip-magic-trailing-comma = false
|
|
|
158
159
|
line-ending = "auto"
|
|
159
160
|
|
|
160
161
|
[tool.bumpversion]
|
|
161
|
-
current_version = "2.12.
|
|
162
|
+
current_version = "2.12.2"
|
|
162
163
|
commit = true
|
|
163
164
|
commit_args = "--no-verify"
|
|
164
165
|
tag = true
|
|
@@ -234,18 +234,22 @@ class Database(ABC):
|
|
|
234
234
|
pass # Non-critical: some drivers lack rowcount support.
|
|
235
235
|
|
|
236
236
|
def decode_row() -> Generator:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
237
|
+
try:
|
|
238
|
+
while True:
|
|
239
|
+
rows = curs.fetchmany()
|
|
240
|
+
if not rows:
|
|
241
|
+
break
|
|
242
|
+
else:
|
|
243
|
+
for row in rows:
|
|
244
|
+
if self.encoding:
|
|
245
|
+
yield [
|
|
246
|
+
c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c
|
|
247
|
+
for c in row
|
|
248
|
+
]
|
|
249
|
+
else:
|
|
250
|
+
yield row
|
|
251
|
+
finally:
|
|
252
|
+
curs.close()
|
|
249
253
|
|
|
250
254
|
return [d[0] for d in curs.description], decode_row()
|
|
251
255
|
|
|
@@ -253,6 +257,10 @@ class Database(ABC):
|
|
|
253
257
|
"""Execute *sql* and return ``(column_names, row_iterator)`` where each row is a ``dict``."""
|
|
254
258
|
# Return an iterable that yields dictionaries of row data
|
|
255
259
|
curs = self.cursor()
|
|
260
|
+
try:
|
|
261
|
+
curs.arraysize = _state.conf.export_row_buffer
|
|
262
|
+
except Exception:
|
|
263
|
+
pass # Non-critical: not all drivers support arraysize.
|
|
256
264
|
try:
|
|
257
265
|
curs.execute(sql)
|
|
258
266
|
except Exception:
|
|
@@ -264,18 +272,24 @@ class Database(ABC):
|
|
|
264
272
|
pass # Non-critical: some drivers lack rowcount support.
|
|
265
273
|
hdrs = [d[0] for d in curs.description]
|
|
266
274
|
|
|
267
|
-
def
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
def dict_rows() -> Generator:
|
|
276
|
+
try:
|
|
277
|
+
while True:
|
|
278
|
+
rows = curs.fetchmany()
|
|
279
|
+
if not rows:
|
|
280
|
+
break
|
|
281
|
+
for row in rows:
|
|
282
|
+
if self.encoding:
|
|
283
|
+
r = [
|
|
284
|
+
c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row
|
|
285
|
+
]
|
|
286
|
+
else:
|
|
287
|
+
r = row
|
|
288
|
+
yield dict(zip(hdrs, r))
|
|
289
|
+
finally:
|
|
290
|
+
curs.close()
|
|
277
291
|
|
|
278
|
-
return hdrs,
|
|
292
|
+
return hdrs, dict_rows()
|
|
279
293
|
|
|
280
294
|
def schema_exists(self, schema_name: str) -> bool:
|
|
281
295
|
"""Return ``True`` if *schema_name* exists in this database."""
|
|
@@ -132,6 +132,8 @@ class ErrInfo(ExecSqlError):
|
|
|
132
132
|
|
|
133
133
|
if self.type == "db":
|
|
134
134
|
self.error_message = "**** Error in SQL statement."
|
|
135
|
+
elif self.type == "assert":
|
|
136
|
+
self.error_message = "**** Assertion failed."
|
|
135
137
|
elif self.type == "cmd":
|
|
136
138
|
self.error_message = "**** Error in metacommand."
|
|
137
139
|
elif self.type == "log":
|
|
@@ -66,7 +66,7 @@ def x_assert(**kwargs: Any) -> None:
|
|
|
66
66
|
if _state.exec_log is not None:
|
|
67
67
|
_state.exec_log.log_user_msg(f"ASSERT passed: {condition}")
|
|
68
68
|
else:
|
|
69
|
-
raise ErrInfo(type="
|
|
69
|
+
raise ErrInfo(type="assert", other_msg=message)
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def x_if(**kwargs: Any) -> None:
|
|
@@ -397,6 +397,15 @@ class ScriptCmd:
|
|
|
397
397
|
self.line_no = command_line_no
|
|
398
398
|
self.command_type = command_type
|
|
399
399
|
self.command = script_command
|
|
400
|
+
# MIGRATION NOTE: differs from monolith (execsql.py) — source_dir and source_name are
|
|
401
|
+
# resolved once at construction rather than on every statement execution. For absolute
|
|
402
|
+
# paths (the common case) the result is identical. For relative paths the value is
|
|
403
|
+
# anchored to the CWD at script-load time rather than at each statement's execution time;
|
|
404
|
+
# the original per-statement resolve could yield inconsistent values across statements of
|
|
405
|
+
# the same script if a CD metacommand ran between them.
|
|
406
|
+
_p = Path(command_source_name)
|
|
407
|
+
self.source_dir: str = str(_p.resolve().parent) + os.sep
|
|
408
|
+
self.source_name: str = _p.name
|
|
400
409
|
|
|
401
410
|
def __repr__(self) -> str:
|
|
402
411
|
return f"ScriptCmd({self.source!r}, {self.line_no!r}, {self.command_type!r}, {repr(self.command)!r})"
|
|
@@ -498,9 +507,9 @@ class CommandList:
|
|
|
498
507
|
_state.subvars.add_substitution("$CURRENT_SCRIPT", cmditem.source)
|
|
499
508
|
_state.subvars.add_substitution(
|
|
500
509
|
"$CURRENT_SCRIPT_PATH",
|
|
501
|
-
|
|
510
|
+
cmditem.source_dir,
|
|
502
511
|
)
|
|
503
|
-
_state.subvars.add_substitution("$CURRENT_SCRIPT_NAME",
|
|
512
|
+
_state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", cmditem.source_name)
|
|
504
513
|
_state.subvars.add_substitution("$CURRENT_SCRIPT_LINE", str(cmditem.line_no))
|
|
505
514
|
_state.subvars.add_substitution("$SCRIPT_LINE", str(cmditem.line_no))
|
|
506
515
|
if _state.step_mode:
|
|
@@ -709,7 +718,7 @@ def set_system_vars() -> None:
|
|
|
709
718
|
"ON" if _state.conf.gui_wait_on_error_halt else "OFF",
|
|
710
719
|
)
|
|
711
720
|
_state.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _state.conf.gui_wait_on_exit else "OFF")
|
|
712
|
-
|
|
721
|
+
# $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
|
|
713
722
|
_state.subvars.add_substitution("$CURRENT_DIR", str(Path(".").resolve()))
|
|
714
723
|
_state.subvars.add_substitution("$CURRENT_PATH", str(Path(".").resolve()) + os.sep)
|
|
715
724
|
_state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
|
|
@@ -742,7 +751,7 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str
|
|
|
742
751
|
subs = _state.subvars.merge(localvars)
|
|
743
752
|
else:
|
|
744
753
|
subs = _state.subvars
|
|
745
|
-
cmdstr =
|
|
754
|
+
cmdstr = command_str
|
|
746
755
|
subs_made = True
|
|
747
756
|
iterations = 0
|
|
748
757
|
while subs_made:
|
|
@@ -89,7 +89,6 @@ class SubVarSet:
|
|
|
89
89
|
# compatibility with external code.
|
|
90
90
|
def __init__(self) -> None:
|
|
91
91
|
self._subs_dict: dict[str, Any] = {}
|
|
92
|
-
self._compiled_patterns: dict[str, tuple] = {}
|
|
93
92
|
self.prefix_list: list[str] = ["$", "&", "@"]
|
|
94
93
|
# Don't construct/compile on init because deepcopy() can't handle compiled regexes.
|
|
95
94
|
self.var_rx = None
|
|
@@ -106,21 +105,6 @@ class SubVarSet:
|
|
|
106
105
|
self._subs_dict = dict(value)
|
|
107
106
|
else:
|
|
108
107
|
self._subs_dict = dict(value)
|
|
109
|
-
self._rebuild_all_patterns()
|
|
110
|
-
|
|
111
|
-
def _compile_patterns_for(self, varname: str) -> tuple:
|
|
112
|
-
"""Compile and return the three regex patterns (plain, single-quoted, double-quoted) for *varname*."""
|
|
113
|
-
match_escaped = "\\" + varname if varname[0] == "$" else varname
|
|
114
|
-
pat = re.compile(f"!!{match_escaped}!!", re.I)
|
|
115
|
-
patq = re.compile(f"!'!{match_escaped}!'!", re.I)
|
|
116
|
-
patdq = re.compile(f'!"!{match_escaped}!"!', re.I)
|
|
117
|
-
return (pat, patq, patdq)
|
|
118
|
-
|
|
119
|
-
def _rebuild_all_patterns(self) -> None:
|
|
120
|
-
"""Rebuild compiled patterns for every variable currently stored."""
|
|
121
|
-
self._compiled_patterns = {}
|
|
122
|
-
for varname in self._subs_dict:
|
|
123
|
-
self._compiled_patterns[varname] = self._compile_patterns_for(varname)
|
|
124
108
|
|
|
125
109
|
def compile_var_rx(self) -> None:
|
|
126
110
|
"""Compile the variable-name validation regex from the current prefix list."""
|
|
@@ -141,14 +125,12 @@ class SubVarSet:
|
|
|
141
125
|
self.check_var_name(template_str)
|
|
142
126
|
old_sub = template_str.lower()
|
|
143
127
|
self._subs_dict.pop(old_sub, None)
|
|
144
|
-
self._compiled_patterns.pop(old_sub, None)
|
|
145
128
|
|
|
146
129
|
def add_substitution(self, varname: str, repl_str: Any) -> None:
|
|
147
|
-
"""Add or overwrite a substitution variable
|
|
130
|
+
"""Add or overwrite a substitution variable."""
|
|
148
131
|
self.check_var_name(varname)
|
|
149
132
|
varname = varname.lower()
|
|
150
133
|
self._subs_dict[varname] = repl_str
|
|
151
|
-
self._compiled_patterns[varname] = self._compile_patterns_for(varname)
|
|
152
134
|
|
|
153
135
|
def append_substitution(self, varname: str, repl_str: str) -> None:
|
|
154
136
|
self.check_var_name(varname)
|
|
@@ -186,15 +168,10 @@ class SubVarSet:
|
|
|
186
168
|
return template_str.lower() in self._subs_dict
|
|
187
169
|
|
|
188
170
|
def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
|
|
189
|
-
"""Return a new SubVarSet with this object's variables merged with other_subvars.
|
|
190
|
-
|
|
191
|
-
Copies dictionaries and pre-compiled patterns directly instead of
|
|
192
|
-
re-adding variables one at a time, avoiding O(V) regex recompilation.
|
|
193
|
-
"""
|
|
171
|
+
"""Return a new SubVarSet with this object's variables merged with other_subvars."""
|
|
194
172
|
if other_subvars is not None:
|
|
195
173
|
newsubs = SubVarSet()
|
|
196
174
|
newsubs._subs_dict = {**self._subs_dict, **other_subvars._subs_dict}
|
|
197
|
-
newsubs._compiled_patterns = {**self._compiled_patterns, **other_subvars._compiled_patterns}
|
|
198
175
|
newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
|
|
199
176
|
newsubs.compile_var_rx()
|
|
200
177
|
return newsubs
|
|
@@ -628,9 +628,9 @@ class TestSelectRowdictEncoding:
|
|
|
628
628
|
mock_curs = MagicMock()
|
|
629
629
|
mock_curs.description = [("name",)]
|
|
630
630
|
mock_curs.rowcount = 1
|
|
631
|
-
mock_curs.
|
|
632
|
-
(b"encoded value",),
|
|
633
|
-
|
|
631
|
+
mock_curs.fetchmany.side_effect = [
|
|
632
|
+
[(b"encoded value",)],
|
|
633
|
+
[],
|
|
634
634
|
]
|
|
635
635
|
|
|
636
636
|
with patch.object(db, "cursor", return_value=mock_curs):
|
|
@@ -648,7 +648,7 @@ class TestSelectRowdictEncoding:
|
|
|
648
648
|
mock_curs = MagicMock()
|
|
649
649
|
mock_curs.description = [("score",)]
|
|
650
650
|
mock_curs.rowcount = 1
|
|
651
|
-
mock_curs.
|
|
651
|
+
mock_curs.fetchmany.side_effect = [[(99,)], []]
|
|
652
652
|
|
|
653
653
|
with patch.object(db, "cursor", return_value=mock_curs):
|
|
654
654
|
_, it = db.select_rowdict("SELECT score FROM fake;")
|
|
@@ -664,7 +664,7 @@ class TestSelectRowdictEncoding:
|
|
|
664
664
|
mock_curs = MagicMock()
|
|
665
665
|
mock_curs.description = [("x",)]
|
|
666
666
|
mock_curs.rowcount = 1
|
|
667
|
-
mock_curs.
|
|
667
|
+
mock_curs.fetchmany.side_effect = [[("plain",)], []]
|
|
668
668
|
|
|
669
669
|
with patch.object(db, "cursor", return_value=mock_curs):
|
|
670
670
|
_, it = db.select_rowdict("SELECT x FROM fake;")
|
|
@@ -71,9 +71,22 @@ class TestXAssertRaisesOnFalseCondition:
|
|
|
71
71
|
with (
|
|
72
72
|
patch.object(_state, "xcmd_test", return_value=False),
|
|
73
73
|
patch.object(_state, "exec_log", mock_log),
|
|
74
|
-
pytest.raises(ErrInfo),
|
|
74
|
+
pytest.raises(ErrInfo) as exc_info,
|
|
75
75
|
):
|
|
76
76
|
x_assert(condtest="ROWCOUNT > 0", message=None, metacommandline="ASSERT ROWCOUNT > 0")
|
|
77
|
+
assert exc_info.value.type == "assert"
|
|
78
|
+
|
|
79
|
+
def test_false_condition_eval_err_says_assertion_failed(self) -> None:
|
|
80
|
+
mock_log = _make_exec_log()
|
|
81
|
+
with (
|
|
82
|
+
patch.object(_state, "xcmd_test", return_value=False),
|
|
83
|
+
patch.object(_state, "exec_log", mock_log),
|
|
84
|
+
pytest.raises(ErrInfo) as exc_info,
|
|
85
|
+
):
|
|
86
|
+
x_assert(condtest="ROWCOUNT > 0", message='"expected rows"', metacommandline="ASSERT ROWCOUNT > 0")
|
|
87
|
+
err_msg = exc_info.value.eval_err()
|
|
88
|
+
assert err_msg.startswith("**** Assertion failed.")
|
|
89
|
+
assert "expected rows" in err_msg
|
|
77
90
|
|
|
78
91
|
def test_false_condition_with_double_quoted_message(self) -> None:
|
|
79
92
|
mock_log = _make_exec_log()
|
|
@@ -706,7 +706,8 @@ class TestSetSystemVars:
|
|
|
706
706
|
set_system_vars()
|
|
707
707
|
assert _state.subvars.varvalue("$DB_USER") == "testuser"
|
|
708
708
|
|
|
709
|
-
def
|
|
709
|
+
def test_current_time_not_set_by_set_system_vars(self, engine_state):
|
|
710
|
+
"""$CURRENT_TIME is set per-statement in run_and_increment, not in set_system_vars."""
|
|
710
711
|
pool = MagicMock()
|
|
711
712
|
pool.current.return_value = self._make_db()
|
|
712
713
|
pool.current_alias.return_value = "main"
|
|
@@ -715,9 +716,7 @@ class TestSetSystemVars:
|
|
|
715
716
|
mock_timer.elapsed.return_value = 0
|
|
716
717
|
_state.timer = mock_timer
|
|
717
718
|
set_system_vars()
|
|
718
|
-
|
|
719
|
-
assert t is not None
|
|
720
|
-
assert len(t) == 16 # "YYYY-MM-DD HH:MM"
|
|
719
|
+
assert _state.subvars.varvalue("$CURRENT_TIME") is None
|
|
721
720
|
|
|
722
721
|
def test_populates_version_numbers(self, engine_state):
|
|
723
722
|
pool = MagicMock()
|