execsql2 2.14.1__tar.gz → 2.15.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.14.1 → execsql2-2.15.0}/CHANGELOG.md +20 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/PKG-INFO +3 -3
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/about/divergence.md +8 -8
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/reference/metacommands.md +11 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/pyproject.toml +3 -3
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/upsert.py +125 -17
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/gui.py +2 -2
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/gui/test_backends.py +28 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_pg_upsert.py +486 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/uv.lock +5 -5
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/dba.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/herald.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/inspector.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/liaison.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/oracle.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/patcher.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/qa.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/scribe.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/code-oracle.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/migrate.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/review-changes.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/test-module.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/update-changelog.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/where-is.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/project_context.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.claude/state/status.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.gitignore +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.pre-commit-config.yaml +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.python-version +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/.readthedocs.yaml +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/CLAUDE.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/CONTRIBUTING.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/LICENSE.txt +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/NOTICE +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/README.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/SECURITY.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/about/contributors.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/about/copyright.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/cli.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/db.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/exporters.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/importers.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/index.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/metacommands.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/architecture.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/getting-started/installation.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/debugging.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/documentation.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/encoding.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/examples.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/formatter.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/logging.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/usage.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/actions.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/actions2.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/checkboxes.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/connect.b64 +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/connect.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/create_conf.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/entry_form.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/execsql_console.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/fatals.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/logo_small.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/unmatched.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/index.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/reference/configuration.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/reference/security.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/justfile +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/__main__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/help.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/run.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/config.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/constants.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/access.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/base.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/factory.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exceptions.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/format.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/base.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/console.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/base.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/json.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/control.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/models.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/parser.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/py.typed +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/script/control.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/script/engine.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/script/variables.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/state.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/types.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/README.md +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/config_settings.sqlite +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/execsql.conf +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/make_config_db.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/md_compare.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/md_glossary.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/md_upsert.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/pg_compare.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/pg_glossary.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/pg_upsert.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/script_template.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/ss_compare.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/ss_glossary.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/templates/ss_upsert.sql +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_cli.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_lint.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_ping.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_profile.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/conftest.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_base.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_factory.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_postgres.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_base.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_db.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_html_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_json.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_json_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_latex_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_pretty_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_raw_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_templates_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_values_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/gui/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/gui/test_compare_stats.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_base_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_json_importer.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/conftest.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_config.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_config_data.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_config_extended.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_constants.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_engine.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_error_messages.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_exceptions.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_format.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_mail.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_models.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_package.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_parser.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_registry.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_script.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_state.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_types.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/__init__.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_auth.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_errors.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_regex.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_strings.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_timer.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.14.1 → execsql2-2.15.0}/zensical.toml +0 -0
|
@@ -13,8 +13,28 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.15.0] - 2026-04-09
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `PG_UPSERT` metacommand: new `EXPORT_FAILURES <dir>`, `EXPORT_FORMAT csv|json|xlsx`, and `EXPORT_MAX_ROWS <n>` keywords that write a "fix sheet" of failing QA rows — one row per unique violating staging row with a consolidated `_issues` column — to CSV, JSON, or XLSX. Works in all three modes (full pipeline, QA-only, schema check) and runs even when QA fails. New `$PG_UPSERT_EXPORT_PATH` substitution variable holds the directory written. A user-visible message reporting the export directory and format is emitted to both the console and the execsql log after every export.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `[upsert]` extra now requires `pg-upsert>=1.21.0` (up from `>=1.20.0`) for the fix-sheet export feature.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- `PROMPT MESSAGE ... CREDENTIALS <user_var> <pw_var>` no longer crashes in console-fallback mode with `TypeError: get_password() missing 2 required positional arguments: 'database_name' and 'user_name'`. The fallback now uses `getpass.getpass()` to read the password, matching the intent (keyring-aware `auth.get_password()` is for CONNECT, not for bare credential prompts).
|
|
29
|
+
|
|
30
|
+
______________________________________________________________________
|
|
31
|
+
|
|
16
32
|
## [2.14.1] - 2026-04-07
|
|
17
33
|
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Fix Windows CI: use `zf.namelist()[0]` instead of path for zip entry lookup.
|
|
37
|
+
|
|
18
38
|
______________________________________________________________________
|
|
19
39
|
|
|
20
40
|
## [2.14.0] - 2026-04-07
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.15.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
|
|
@@ -51,7 +51,7 @@ Requires-Dist: keyring; extra == 'all'
|
|
|
51
51
|
Requires-Dist: odfpy; extra == 'all'
|
|
52
52
|
Requires-Dist: openpyxl; extra == 'all'
|
|
53
53
|
Requires-Dist: oracledb; extra == 'all'
|
|
54
|
-
Requires-Dist: pg-upsert>=1.
|
|
54
|
+
Requires-Dist: pg-upsert>=1.21.0; extra == 'all'
|
|
55
55
|
Requires-Dist: polars; extra == 'all'
|
|
56
56
|
Requires-Dist: psycopg2-binary; extra == 'all'
|
|
57
57
|
Requires-Dist: pymysql; extra == 'all'
|
|
@@ -109,7 +109,7 @@ Requires-Dist: oracledb; extra == 'oracle'
|
|
|
109
109
|
Provides-Extra: postgres
|
|
110
110
|
Requires-Dist: psycopg2-binary; extra == 'postgres'
|
|
111
111
|
Provides-Extra: upsert
|
|
112
|
-
Requires-Dist: pg-upsert>=1.
|
|
112
|
+
Requires-Dist: pg-upsert>=1.21.0; extra == 'upsert'
|
|
113
113
|
Description-Content-Type: text/markdown
|
|
114
114
|
|
|
115
115
|
> [!NOTE]
|
|
@@ -41,14 +41,14 @@ ______________________________________________________________________
|
|
|
41
41
|
|
|
42
42
|
### Metacommands
|
|
43
43
|
|
|
44
|
-
| Metacommand | Description
|
|
45
|
-
| ---------------------- |
|
|
46
|
-
| `ASSERT` | Evaluate a condition and raise an error (halting the script) if it is false. Supports all IF conditions. Optional quoted failure message. Skipped in false IF blocks.
|
|
47
|
-
| `BREAKPOINT` | Pause script execution and drop into an interactive debug REPL. See [Debugging](#debugging) below for full details.
|
|
48
|
-
| `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime.
|
|
49
|
-
| `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file.
|
|
50
|
-
| `PG_UPSERT` | QA-checked, FK-dependency-ordered upserts from staging to base schema on PostgreSQL. Integrates [pg-upsert](https://pg-upsert.readthedocs.io/) as an optional dependency. Three modes: full pipeline, QA-only, and schema check. |
|
|
51
|
-
| `IMPORT … FROM JSON` | Import a JSON file (array of objects or NDJSON) into a database table. Nested objects are flattened with dot-separated column names; arrays are stored as JSON strings.
|
|
44
|
+
| Metacommand | Description |
|
|
45
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| `ASSERT` | Evaluate a condition and raise an error (halting the script) if it is false. Supports all IF conditions. Optional quoted failure message. Skipped in false IF blocks. |
|
|
47
|
+
| `BREAKPOINT` | Pause script execution and drop into an interactive debug REPL. See [Debugging](#debugging) below for full details. |
|
|
48
|
+
| `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
|
|
49
|
+
| `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file. |
|
|
50
|
+
| `PG_UPSERT` | QA-checked, FK-dependency-ordered upserts from staging to base schema on PostgreSQL. Integrates [pg-upsert](https://pg-upsert.readthedocs.io/) as an optional dependency. Three modes: full pipeline, QA-only, and schema check. Supports `EXPORT_FAILURES`, `EXPORT_FORMAT`, and `EXPORT_MAX_ROWS` keywords to write a "fix sheet" of failing QA rows to CSV/JSON/XLSX (requires `pg-upsert>=1.21.0`). |
|
|
51
|
+
| `IMPORT … FROM JSON` | Import a JSON file (array of objects or NDJSON) into a database table. Nested objects are flattened with dot-separated column names; arrays are stored as JSON strings. |
|
|
52
52
|
|
|
53
53
|
### Conditional Tests
|
|
54
54
|
|
|
@@ -2127,6 +2127,9 @@ Keywords can appear in any order after the table list.
|
|
|
2127
2127
|
| `COMPACT` | Use compact grid format for QA summary instead of detailed per-table panels. |
|
|
2128
2128
|
| `LOGFILE <path>` | Append pg-upsert's plain-text log output to the given file. Supports quoted paths for spaces: `LOGFILE "path/to/log.txt"`. |
|
|
2129
2129
|
| `CLEANUP` | Drop all `ups_*` temporary tables and views after execution. Without it, temp objects persist for inspection (default). |
|
|
2130
|
+
| `EXPORT_FAILURES <dir>` | Write a "fix sheet" of failing QA rows into `<dir>` (directory is created if missing). One row per unique violating staging row, with an `_issues` column summarizing every problem on that row. Supports quoted paths for spaces. Export runs even when QA fails — that is the whole point. A confirmation message (`PG_UPSERT: exported QA failures to <dir> (<format>)`) is written to the terminal, the execsql log, and the `LOGFILE` target if one is given. |
|
|
2131
|
+
| `EXPORT_FORMAT csv\|json\|xlsx` | Fix sheet format when `EXPORT_FAILURES` is given. `csv` (default) writes one file per table; `json` writes a single nested file; `xlsx` writes a single workbook with one sheet per table (requires `openpyxl`). |
|
|
2132
|
+
| `EXPORT_MAX_ROWS <n>` | Maximum rows to capture per check per table for the fix sheet. Default `1000`. Only meaningful with `EXPORT_FAILURES`. |
|
|
2130
2133
|
|
|
2131
2134
|
### Substitution variables
|
|
2132
2135
|
|
|
@@ -2150,6 +2153,7 @@ Set after every `PG_UPSERT` execution:
|
|
|
2150
2153
|
| `$PG_UPSERT_TABLE_QA_PASSED` | TRUE/FALSE | QA result for the current table (updated per table) |
|
|
2151
2154
|
| `$PG_UPSERT_TABLE_ROWS_UPDATED` | integer | Rows updated for the current table (updated per table) |
|
|
2152
2155
|
| `$PG_UPSERT_TABLE_ROWS_INSERTED` | integer | Rows inserted for the current table (updated per table) |
|
|
2156
|
+
| `$PG_UPSERT_EXPORT_PATH` | string | Directory path that the QA fix sheet was written to, or empty if `EXPORT_FAILURES` was not given or nothing was exported. |
|
|
2153
2157
|
|
|
2154
2158
|
!!! note "Using `$PG_UPSERT_RESULT_JSON` with WRITE"
|
|
2155
2159
|
The JSON value is stored as compact single-line JSON. Because it contains double quotes (`"`), square brackets (`[]`), and apostrophes may appear in data, use tilde or backtick delimiters with WRITE:
|
|
@@ -2211,6 +2215,13 @@ For the full list of temporary objects and their schemas, see the [pg-upsert Tem
|
|
|
2211
2215
|
-- Write the full JSON result using tilde delimiters (JSON contains " and [])
|
|
2212
2216
|
-- !x! PG_UPSERT FROM staging TO public TABLES books COMMIT
|
|
2213
2217
|
-- !x! WRITE ~!!$PG_UPSERT_RESULT_JSON!!~
|
|
2218
|
+
|
|
2219
|
+
-- Export failing QA rows to an Excel fix sheet (one sheet per table)
|
|
2220
|
+
-- !x! PG_UPSERT QA FROM staging TO public TABLES books, authors EXPORT_FAILURES "qa_failures/" EXPORT_FORMAT xlsx
|
|
2221
|
+
|
|
2222
|
+
-- Full pipeline with a CSV fix sheet cap of 50 rows per check per table
|
|
2223
|
+
-- !x! PG_UPSERT FROM staging TO public TABLES books, authors EXPORT_FAILURES /tmp/fix EXPORT_MAX_ROWS 50 COMMIT
|
|
2224
|
+
-- !x! WRITE "Fix sheet: !!$PG_UPSERT_EXPORT_PATH!!"
|
|
2214
2225
|
```
|
|
2215
2226
|
|
|
2216
2227
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.15.0"
|
|
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" }
|
|
@@ -58,7 +58,7 @@ odbc = ["pyodbc"]
|
|
|
58
58
|
# Feature bundles
|
|
59
59
|
formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables", "PyYAML"]
|
|
60
60
|
auth = ["keyring"]
|
|
61
|
-
upsert = ["pg-upsert>=1.
|
|
61
|
+
upsert = ["pg-upsert>=1.21.0"]
|
|
62
62
|
# Convenience groups
|
|
63
63
|
all-db = [
|
|
64
64
|
"psycopg2-binary",
|
|
@@ -161,7 +161,7 @@ skip-magic-trailing-comma = false
|
|
|
161
161
|
line-ending = "auto"
|
|
162
162
|
|
|
163
163
|
[tool.bumpversion]
|
|
164
|
-
current_version = "2.
|
|
164
|
+
current_version = "2.15.0"
|
|
165
165
|
commit = true
|
|
166
166
|
commit_args = "--no-verify"
|
|
167
167
|
tag = true
|
|
@@ -26,11 +26,11 @@ from execsql.utils.errors import exception_desc
|
|
|
26
26
|
|
|
27
27
|
_KW_METHOD = re.compile(r"\bMETHOD\s+(upsert|update|insert)\b", re.IGNORECASE)
|
|
28
28
|
_KW_EXCLUDE = re.compile(
|
|
29
|
-
r"\bEXCLUDE\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|LOGFILE|CLEANUP)\b|\s*$)",
|
|
29
|
+
r"\bEXCLUDE\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b|\s*$)",
|
|
30
30
|
re.IGNORECASE,
|
|
31
31
|
)
|
|
32
32
|
_KW_EXCLUDE_NULL = re.compile(
|
|
33
|
-
r"\bEXCLUDE_NULL\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE|LOGFILE|CLEANUP)\b|\s*$)",
|
|
33
|
+
r"\bEXCLUDE_NULL\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b|\s*$)",
|
|
34
34
|
re.IGNORECASE,
|
|
35
35
|
)
|
|
36
36
|
_KW_COMMIT = re.compile(r"\bCOMMIT\b", re.IGNORECASE)
|
|
@@ -38,13 +38,21 @@ _KW_INTERACTIVE = re.compile(r"\bINTERACTIVE\b", re.IGNORECASE)
|
|
|
38
38
|
_KW_COMPACT = re.compile(r"\bCOMPACT\b", re.IGNORECASE)
|
|
39
39
|
_KW_CLEANUP = re.compile(r"\bCLEANUP\b", re.IGNORECASE)
|
|
40
40
|
_KW_LOGFILE = re.compile(r"""\bLOGFILE\s+(?:"([^"]+)"|'([^']+)'|(\S+))""", re.IGNORECASE)
|
|
41
|
+
_KW_EXPORT_FAILURES = re.compile(
|
|
42
|
+
r"""\bEXPORT_FAILURES\s+(?:"([^"]+)"|'([^']+)'|(\S+))""",
|
|
43
|
+
re.IGNORECASE,
|
|
44
|
+
)
|
|
45
|
+
_KW_EXPORT_FORMAT = re.compile(r"\bEXPORT_FORMAT\s+(\S+)", re.IGNORECASE)
|
|
46
|
+
_KW_EXPORT_MAX_ROWS = re.compile(r"\bEXPORT_MAX_ROWS\s+(\S+)", re.IGNORECASE)
|
|
41
47
|
|
|
42
48
|
# All recognized keywords — used to split table names from options.
|
|
43
49
|
_ALL_KEYWORDS = re.compile(
|
|
44
|
-
r"\b(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|EXCLUDE|LOGFILE|CLEANUP)\b",
|
|
50
|
+
r"\b(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|EXCLUDE|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b",
|
|
45
51
|
re.IGNORECASE,
|
|
46
52
|
)
|
|
47
53
|
|
|
54
|
+
_VALID_EXPORT_FORMATS = ("csv", "json", "xlsx")
|
|
55
|
+
|
|
48
56
|
|
|
49
57
|
def _parse_tables_and_options(tail: str) -> dict[str, Any]:
|
|
50
58
|
"""Parse the trailing text after ``TABLES`` into table names and options.
|
|
@@ -90,6 +98,40 @@ def _parse_tables_and_options(tail: str) -> dict[str, Any]:
|
|
|
90
98
|
if m:
|
|
91
99
|
logfile = m.group(1) or m.group(2) or m.group(3)
|
|
92
100
|
|
|
101
|
+
export_failures: str | None = None
|
|
102
|
+
m = _KW_EXPORT_FAILURES.search(opts_part)
|
|
103
|
+
if m:
|
|
104
|
+
export_failures = m.group(1) or m.group(2) or m.group(3)
|
|
105
|
+
|
|
106
|
+
export_format = "csv"
|
|
107
|
+
m = _KW_EXPORT_FORMAT.search(opts_part)
|
|
108
|
+
if m:
|
|
109
|
+
fmt = m.group(1).lower()
|
|
110
|
+
if fmt not in _VALID_EXPORT_FORMATS:
|
|
111
|
+
raise ErrInfo(
|
|
112
|
+
"cmd",
|
|
113
|
+
other_msg=(
|
|
114
|
+
f"PG_UPSERT: unsupported EXPORT_FORMAT {m.group(1)!r}. "
|
|
115
|
+
f"Supported: {', '.join(_VALID_EXPORT_FORMATS)}"
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
export_format = fmt
|
|
119
|
+
|
|
120
|
+
export_max_rows = 1000
|
|
121
|
+
m = _KW_EXPORT_MAX_ROWS.search(opts_part)
|
|
122
|
+
if m:
|
|
123
|
+
raw = m.group(1)
|
|
124
|
+
try:
|
|
125
|
+
val = int(raw)
|
|
126
|
+
if val <= 0:
|
|
127
|
+
raise ValueError
|
|
128
|
+
except ValueError as exc:
|
|
129
|
+
raise ErrInfo(
|
|
130
|
+
"cmd",
|
|
131
|
+
other_msg=(f"PG_UPSERT: EXPORT_MAX_ROWS must be a positive integer, got {raw!r}"),
|
|
132
|
+
) from exc
|
|
133
|
+
export_max_rows = val
|
|
134
|
+
|
|
93
135
|
return {
|
|
94
136
|
"tables": tables,
|
|
95
137
|
"method": method,
|
|
@@ -100,6 +142,9 @@ def _parse_tables_and_options(tail: str) -> dict[str, Any]:
|
|
|
100
142
|
"exclude_null_check_cols": exclude_null,
|
|
101
143
|
"logfile": logfile,
|
|
102
144
|
"cleanup": bool(_KW_CLEANUP.search(opts_part)),
|
|
145
|
+
"export_failures": export_failures,
|
|
146
|
+
"export_format": export_format,
|
|
147
|
+
"export_max_rows": export_max_rows,
|
|
103
148
|
}
|
|
104
149
|
|
|
105
150
|
|
|
@@ -157,6 +202,9 @@ def _set_subvars(result: Any) -> None:
|
|
|
157
202
|
sv("$PG_UPSERT_STARTED_AT", result.started_at)
|
|
158
203
|
sv("$PG_UPSERT_FINISHED_AT", result.finished_at)
|
|
159
204
|
sv("$PG_UPSERT_RESULT_JSON", json.dumps(result.to_dict(), separators=(",", ":")))
|
|
205
|
+
# Default export path subvar to empty; _export_failures_if_requested
|
|
206
|
+
# will overwrite it with the actual path if an export was produced.
|
|
207
|
+
sv("$PG_UPSERT_EXPORT_PATH", "")
|
|
160
208
|
|
|
161
209
|
|
|
162
210
|
def _qa_failure_msg(result: Any) -> str:
|
|
@@ -241,20 +289,26 @@ def _create_pgupsert(
|
|
|
241
289
|
if _state.conf:
|
|
242
290
|
ui_mode = _state.conf.gui_framework
|
|
243
291
|
|
|
244
|
-
|
|
245
|
-
conn
|
|
246
|
-
staging_schema
|
|
247
|
-
base_schema
|
|
248
|
-
tables
|
|
249
|
-
do_commit
|
|
250
|
-
interactive
|
|
251
|
-
compact
|
|
252
|
-
upsert_method
|
|
253
|
-
exclude_cols
|
|
254
|
-
exclude_null_check_cols
|
|
255
|
-
ui_mode
|
|
256
|
-
callback
|
|
257
|
-
|
|
292
|
+
kwargs: dict[str, Any] = {
|
|
293
|
+
"conn": db.conn,
|
|
294
|
+
"staging_schema": staging_schema,
|
|
295
|
+
"base_schema": base_schema,
|
|
296
|
+
"tables": opts["tables"],
|
|
297
|
+
"do_commit": opts["commit"],
|
|
298
|
+
"interactive": opts["interactive"],
|
|
299
|
+
"compact": opts["compact"],
|
|
300
|
+
"upsert_method": opts["method"],
|
|
301
|
+
"exclude_cols": opts["exclude_cols"],
|
|
302
|
+
"exclude_null_check_cols": opts["exclude_null_check_cols"],
|
|
303
|
+
"ui_mode": ui_mode,
|
|
304
|
+
"callback": _make_callback(),
|
|
305
|
+
}
|
|
306
|
+
# Only pass fix-sheet capture args when an export was requested, so the
|
|
307
|
+
# metacommand stays compatible with any pg-upsert build that lacks them.
|
|
308
|
+
if opts.get("export_failures"):
|
|
309
|
+
kwargs["capture_detail_rows"] = True
|
|
310
|
+
kwargs["max_export_rows"] = opts.get("export_max_rows", 1000)
|
|
311
|
+
ups = PgUpsert(**kwargs)
|
|
258
312
|
return ups
|
|
259
313
|
|
|
260
314
|
|
|
@@ -328,6 +382,57 @@ def _run_with_autocommit_guard(db: Any, fn: Any) -> Any:
|
|
|
328
382
|
db.autocommit_on()
|
|
329
383
|
|
|
330
384
|
|
|
385
|
+
def _export_failures_if_requested(
|
|
386
|
+
result: Any,
|
|
387
|
+
opts: dict[str, Any],
|
|
388
|
+
metacommandline: str | None,
|
|
389
|
+
) -> None:
|
|
390
|
+
"""Export a QA fix sheet if EXPORT_FAILURES was given in the metacommand.
|
|
391
|
+
|
|
392
|
+
Always called after ``_set_subvars(result)`` so ``$PG_UPSERT_EXPORT_PATH``
|
|
393
|
+
is initialized to empty first, then overwritten here on a successful
|
|
394
|
+
export. Called even when QA failed — that's the whole point of the
|
|
395
|
+
fix sheet.
|
|
396
|
+
"""
|
|
397
|
+
path = opts.get("export_failures")
|
|
398
|
+
if not path:
|
|
399
|
+
return
|
|
400
|
+
fmt = opts["export_format"]
|
|
401
|
+
try:
|
|
402
|
+
exported = result.export_failures(path, fmt=fmt)
|
|
403
|
+
except Exception as exc:
|
|
404
|
+
raise ErrInfo(
|
|
405
|
+
"exception",
|
|
406
|
+
exception_msg=exception_desc(),
|
|
407
|
+
other_msg=(f"PG_UPSERT failed to export failure sheet to {path}"),
|
|
408
|
+
) from exc
|
|
409
|
+
_state.subvars.add_substitution(
|
|
410
|
+
"$PG_UPSERT_EXPORT_PATH",
|
|
411
|
+
str(exported) if exported else "",
|
|
412
|
+
)
|
|
413
|
+
if exported:
|
|
414
|
+
msg = f"PG_UPSERT: exported QA failures to {exported} ({fmt})"
|
|
415
|
+
else:
|
|
416
|
+
msg = f"PG_UPSERT: no QA failures to export (EXPORT_FAILURES {path} skipped)"
|
|
417
|
+
_state.exec_log.log_user_msg(msg)
|
|
418
|
+
try:
|
|
419
|
+
_state.output.write(msg + "\n")
|
|
420
|
+
except Exception:
|
|
421
|
+
# Output sink may be unavailable in some contexts (e.g. tests);
|
|
422
|
+
# the log message above is sufficient in that case.
|
|
423
|
+
pass
|
|
424
|
+
# Tee to the LOGFILE keyword target if one was given, matching how the
|
|
425
|
+
# pg-upsert display output is routed there via _FileWriterHandler.
|
|
426
|
+
logfile = opts.get("logfile")
|
|
427
|
+
if logfile:
|
|
428
|
+
try:
|
|
429
|
+
from execsql.utils.fileio import filewriter_write
|
|
430
|
+
|
|
431
|
+
filewriter_write(logfile, msg + "\n")
|
|
432
|
+
except Exception:
|
|
433
|
+
pass
|
|
434
|
+
|
|
435
|
+
|
|
331
436
|
def _handle_pg_upsert_errors(fn: Any, metacommandline: str | None) -> Any:
|
|
332
437
|
"""Run *fn*, translating pg-upsert exceptions to ErrInfo."""
|
|
333
438
|
from pg_upsert import UserCancelledError
|
|
@@ -381,6 +486,7 @@ def x_pg_upsert(**kwargs: Any) -> None:
|
|
|
381
486
|
_detach_log_handlers(loggers, handlers, prev_levels)
|
|
382
487
|
|
|
383
488
|
_set_subvars(result)
|
|
489
|
+
_export_failures_if_requested(result, opts, metacommandline)
|
|
384
490
|
if opts.get("cleanup"):
|
|
385
491
|
ups.cleanup()
|
|
386
492
|
|
|
@@ -420,6 +526,7 @@ def x_pg_upsert_qa(**kwargs: Any) -> None:
|
|
|
420
526
|
|
|
421
527
|
result = _build_result_from_qa_errors(ups)
|
|
422
528
|
_set_subvars(result)
|
|
529
|
+
_export_failures_if_requested(result, opts, metacommandline)
|
|
423
530
|
if opts.get("cleanup"):
|
|
424
531
|
ups.cleanup()
|
|
425
532
|
|
|
@@ -462,6 +569,7 @@ def x_pg_upsert_check(**kwargs: Any) -> None:
|
|
|
462
569
|
|
|
463
570
|
result = _build_result_from_qa_errors(ups)
|
|
464
571
|
_set_subvars(result)
|
|
572
|
+
_export_failures_if_requested(result, opts, metacommandline)
|
|
465
573
|
if opts.get("cleanup"):
|
|
466
574
|
ups.cleanup()
|
|
467
575
|
|
|
@@ -498,10 +498,10 @@ def gui_credentials(
|
|
|
498
498
|
cmd:
|
|
499
499
|
The originating metacommand line (for logging only).
|
|
500
500
|
"""
|
|
501
|
+
import getpass as _getpass
|
|
501
502
|
import queue as _queue
|
|
502
503
|
|
|
503
504
|
import execsql.state as _state
|
|
504
|
-
from execsql.utils.auth import get_password
|
|
505
505
|
|
|
506
506
|
gui_level = _state.conf.gui_level if _state.conf else 0
|
|
507
507
|
if gui_level > 0 and _state.gui_manager_thread is not None and _state.gui_manager_thread.is_alive():
|
|
@@ -515,7 +515,7 @@ def gui_credentials(
|
|
|
515
515
|
if message:
|
|
516
516
|
print(message, file=sys.stderr)
|
|
517
517
|
uname = input("Username: ")
|
|
518
|
-
passwd =
|
|
518
|
+
passwd = _getpass.getpass(f"Password for {uname}: ")
|
|
519
519
|
|
|
520
520
|
if username:
|
|
521
521
|
_state.subvars.add_substitution(username, uname)
|
|
@@ -808,6 +808,34 @@ class TestGuiPublicAPI:
|
|
|
808
808
|
monkeypatch.setattr("builtins.input", lambda *a: next(inputs))
|
|
809
809
|
assert get_yn("?") is False
|
|
810
810
|
|
|
811
|
+
def test_gui_credentials_console_fallback(self, monkeypatch):
|
|
812
|
+
"""gui_credentials must not call auth.get_password (which needs
|
|
813
|
+
dbms/database/user kwargs). Regression for the
|
|
814
|
+
'get_password() missing 2 required positional arguments' bug.
|
|
815
|
+
"""
|
|
816
|
+
from unittest.mock import MagicMock
|
|
817
|
+
|
|
818
|
+
import execsql.state as _state
|
|
819
|
+
from execsql.utils.gui import gui_credentials
|
|
820
|
+
|
|
821
|
+
# Force the fallback branch: no GUI manager thread.
|
|
822
|
+
monkeypatch.setattr(_state, "gui_manager_thread", None)
|
|
823
|
+
conf = MagicMock()
|
|
824
|
+
conf.gui_level = 0
|
|
825
|
+
monkeypatch.setattr(_state, "conf", conf)
|
|
826
|
+
|
|
827
|
+
subvars = MagicMock()
|
|
828
|
+
monkeypatch.setattr(_state, "subvars", subvars)
|
|
829
|
+
|
|
830
|
+
monkeypatch.setattr("builtins.input", lambda *a: "alice")
|
|
831
|
+
monkeypatch.setattr("getpass.getpass", lambda *a, **kw: "s3cret")
|
|
832
|
+
|
|
833
|
+
gui_credentials(message="Log in", username="$U", pwtext="$P")
|
|
834
|
+
|
|
835
|
+
calls = {c[0][0]: c[0][1] for c in subvars.add_substitution.call_args_list}
|
|
836
|
+
assert calls["$U"] == "alice"
|
|
837
|
+
assert calls["$P"] == "s3cret"
|
|
838
|
+
|
|
811
839
|
def test_pause_no_countdown(self, monkeypatch):
|
|
812
840
|
from execsql.utils.gui import pause
|
|
813
841
|
|