execsql2 2.4.0__tar.gz → 2.4.4__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.4.0 → execsql2-2.4.4}/.github/workflows/ci-cd.yml +55 -3
- execsql2-2.4.4/.pre-commit-hooks.yaml +7 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/CHANGELOG.md +44 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/CLAUDE.md +2 -2
- {execsql2-2.4.0 → execsql2-2.4.4}/PKG-INFO +1 -1
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/formatter.md +29 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/pyproject.toml +3 -3
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/cli/run.py +11 -5
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/factory.py +10 -5
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/mysql.py +14 -1
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/importers/base.py +3 -3
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/importers/csv.py +5 -5
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/importers/feather.py +4 -4
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/db/test_duckdb.py +129 -4
- execsql2-2.4.4/tests/db/test_sqlite_extra.py +726 -0
- execsql2-2.4.4/tests/exporters/test_base.py +503 -0
- execsql2-2.4.4/tests/exporters/test_feather.py +352 -0
- execsql2-2.4.4/tests/integration/conftest.py +36 -0
- execsql2-2.4.4/tests/integration/test_dsn.py +169 -0
- execsql2-2.4.0/tests/test_integration_duckdb.py → execsql2-2.4.4/tests/integration/test_duckdb.py +33 -58
- execsql2-2.4.4/tests/integration/test_mysql.py +415 -0
- execsql2-2.4.4/tests/integration/test_postgres.py +414 -0
- execsql2-2.4.0/tests/test_integration.py → execsql2-2.4.4/tests/integration/test_sqlite.py +52 -76
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_cli.py +278 -0
- execsql2-2.4.4/tests/test_cli_run.py +1447 -0
- execsql2-2.4.4/tests/utils/__init__.py +0 -0
- execsql2-2.4.4/tests/utils/test_auth_extra.py +370 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/uv.lock +1 -1
- execsql2-2.4.0/tests/exporters/test_base.py +0 -201
- execsql2-2.4.0/tests/exporters/test_feather.py +0 -62
- execsql2-2.4.0/tests/utils/test_auth_extra.py +0 -111
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/agents/dba.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/agents/herald.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/agents/inspector.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/agents/oracle.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/agents/patcher.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/agents/qa.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/agents/scribe.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/commands/code-oracle.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/commands/migrate.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/commands/review-changes.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/commands/test-module.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/commands/update-changelog.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/commands/where-is.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/project_context.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.claude/state/status.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.gitignore +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.pre-commit-config.yaml +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.python-version +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/.readthedocs.yaml +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/CONTRIBUTING.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/LICENSE.txt +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/NOTICE +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/README.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/api/cli.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/api/db.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/api/exporters.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/api/importers.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/api/index.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/api/metacommands.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/change_log.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/configuration.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/contributors.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/copyright.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/debugging.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/documentation.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/encoding.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/examples.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/actions.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/actions2.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/checkboxes.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/connect.b64 +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/connect.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/create_conf.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/entry_form.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/execsql_console.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/fatals.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/logo_small.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/unmatched.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/index.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/installation.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/logging.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/metacommands.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/requirements.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/security.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/sql_syntax.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/substitution_vars.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/syntax.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/usage.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/docs/using_scripts.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/justfile +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/__main__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/cli/help.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/config.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/constants.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/access.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/base.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exceptions.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/format.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/gui/base.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/gui/console.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/control.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/models.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/parser.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/py.typed +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/script/control.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/script/engine.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/script/variables.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/state.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/types.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/README.md +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/config_settings.sqlite +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/execsql.conf +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/make_config_db.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/md_compare.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/md_glossary.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/md_upsert.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/pg_compare.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/pg_glossary.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/pg_upsert.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/script_template.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/ss_compare.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/ss_glossary.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/templates/ss_upsert.sql +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/conftest.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/db/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/db/test_base.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/db/test_factory.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/db/test_postgres.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_db.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_json.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/gui/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/gui/test_backends.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/importers/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.4.0/tests/metacommands → execsql2-2.4.4/tests/integration}/__init__.py +0 -0
- {execsql2-2.4.0/tests/utils → execsql2-2.4.4/tests/metacommands}/__init__.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_config.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_config_data.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_constants.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_exceptions.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_format.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_mail.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_models.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_package.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_parser.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_registry.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_script.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_state.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/test_types.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_auth.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_errors.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_regex.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_strings.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_timer.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.4.0 → execsql2-2.4.4}/zensical.toml +0 -0
|
@@ -51,7 +51,6 @@ jobs:
|
|
|
51
51
|
python -m pip install --upgrade pip
|
|
52
52
|
python -m pip install tox
|
|
53
53
|
python -m pip install ".[dev]"
|
|
54
|
-
python -m pip install ".[formats]"
|
|
55
54
|
- name: Install tkinter (Ubuntu)
|
|
56
55
|
if: runner.os == 'Linux'
|
|
57
56
|
run: sudo apt-get install -y python3-tk
|
|
@@ -67,10 +66,63 @@ jobs:
|
|
|
67
66
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
68
67
|
files: coverage.xml
|
|
69
68
|
|
|
69
|
+
integration-tests:
|
|
70
|
+
name: integration-tests-py${{ matrix.python-version }}
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
strategy:
|
|
73
|
+
fail-fast: false
|
|
74
|
+
matrix:
|
|
75
|
+
python-version:
|
|
76
|
+
- "3.13"
|
|
77
|
+
permissions:
|
|
78
|
+
contents: read
|
|
79
|
+
services:
|
|
80
|
+
postgres:
|
|
81
|
+
image: postgres:16
|
|
82
|
+
env:
|
|
83
|
+
POSTGRES_USER: execsql
|
|
84
|
+
POSTGRES_PASSWORD: execsql
|
|
85
|
+
POSTGRES_DB: execsql_test
|
|
86
|
+
ports:
|
|
87
|
+
- 5432:5432
|
|
88
|
+
options: >-
|
|
89
|
+
--health-cmd pg_isready
|
|
90
|
+
--health-interval 10s
|
|
91
|
+
--health-timeout 5s
|
|
92
|
+
--health-retries 5
|
|
93
|
+
mysql:
|
|
94
|
+
image: mysql:8
|
|
95
|
+
env:
|
|
96
|
+
MYSQL_ROOT_PASSWORD: root
|
|
97
|
+
MYSQL_USER: execsql
|
|
98
|
+
MYSQL_PASSWORD: execsql
|
|
99
|
+
MYSQL_DATABASE: execsql_test
|
|
100
|
+
ports:
|
|
101
|
+
- 3306:3306
|
|
102
|
+
options: >-
|
|
103
|
+
--health-cmd "mysqladmin ping"
|
|
104
|
+
--health-interval 10s
|
|
105
|
+
--health-timeout 5s
|
|
106
|
+
--health-retries 5
|
|
107
|
+
steps:
|
|
108
|
+
- name: Check out repository code
|
|
109
|
+
uses: actions/checkout@v4
|
|
110
|
+
- name: Setup Python ${{ matrix.python-version }}
|
|
111
|
+
uses: actions/setup-python@v5
|
|
112
|
+
with:
|
|
113
|
+
python-version: ${{ matrix.python-version }}
|
|
114
|
+
- name: Install dependencies
|
|
115
|
+
run: |
|
|
116
|
+
python -m pip install --upgrade pip
|
|
117
|
+
python -m pip install ".[dev,postgres,mysql]"
|
|
118
|
+
- name: Run integration tests
|
|
119
|
+
run: |
|
|
120
|
+
python -m pytest tests/integration/test_postgres.py tests/integration/test_mysql.py -v --override-ini="addopts="
|
|
121
|
+
|
|
70
122
|
build:
|
|
71
123
|
name: Build distribution 📦
|
|
72
124
|
runs-on: ubuntu-latest
|
|
73
|
-
needs: [tests]
|
|
125
|
+
needs: [tests, integration-tests]
|
|
74
126
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
75
127
|
permissions:
|
|
76
128
|
contents: read
|
|
@@ -100,7 +152,7 @@ jobs:
|
|
|
100
152
|
runs-on: ubuntu-latest
|
|
101
153
|
environment:
|
|
102
154
|
name: pypi
|
|
103
|
-
url: https://pypi.org/p
|
|
155
|
+
url: https://pypi.org/p/execsql2
|
|
104
156
|
permissions:
|
|
105
157
|
id-token: write
|
|
106
158
|
contents: read
|
|
@@ -13,6 +13,42 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.4.4] - 2026-03-30
|
|
17
|
+
|
|
18
|
+
______________________________________________________________________
|
|
19
|
+
|
|
20
|
+
## [2.4.3] - 2026-03-30
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Pre-commit hook for `execsql-format` — users can add the repo to their `.pre-commit-config.yaml` and pass `--check` or `--in-place` via `args`.
|
|
25
|
+
|
|
26
|
+
______________________________________________________________________
|
|
27
|
+
|
|
28
|
+
## [2.4.2] - 2026-03-30
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Raised test coverage floor from 75% to 80% in `pyproject.toml`.
|
|
33
|
+
|
|
34
|
+
______________________________________________________________________
|
|
35
|
+
|
|
36
|
+
## [2.4.1] - 2026-03-30
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- `--dsn` now correctly overrides connection settings from configuration files.
|
|
41
|
+
- MySQL `LOAD DATA INFILE` encoding — map Python encoding names (e.g. `utf-8`) to MySQL charset names (e.g. `utf8mb4`).
|
|
42
|
+
- Importer error reporting — replaced removed `exception_info()` with `exception_desc()`.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- Integration tests moved to `tests/integration/` with a shared conftest and parallel CI execution.
|
|
47
|
+
- CI no longer enforces the coverage threshold for integration tests.
|
|
48
|
+
- Removed `docker-compose.yml` — CI uses GitHub Actions services directly.
|
|
49
|
+
|
|
50
|
+
______________________________________________________________________
|
|
51
|
+
|
|
16
52
|
## [2.4.0] - 2026-03-30
|
|
17
53
|
|
|
18
54
|
### Changed
|
|
@@ -29,11 +65,19 @@ ______________________________________________________________________
|
|
|
29
65
|
|
|
30
66
|
- Python 3.14 support — added to CI matrix, tox environments, and PyPI classifiers.
|
|
31
67
|
- `formats` extra included in `dev` dependencies so ODS/Excel/Jinja2 tests run without manual installation.
|
|
68
|
+
- PostgreSQL integration tests (9 tests) — full lifecycle via `--dsn` connection strings.
|
|
69
|
+
- MySQL/MariaDB integration tests (9 tests, 1 xfail for pre-existing import adapter bug).
|
|
70
|
+
- `docker-compose.yml` for local PostgreSQL and MySQL test databases.
|
|
71
|
+
- CI integration test job with GitHub Actions services (PostgreSQL 16, MySQL 8).
|
|
32
72
|
- Roadmap items in `templates/README.md` for integrating execsql-compare and execsql-upsert documentation into the main docs site.
|
|
33
73
|
|
|
34
74
|
### Fixed
|
|
35
75
|
|
|
36
76
|
- Fix odfpy import — `import of` corrected to `import odf as of` in `exporters/ods.py` and test skip guards. ODS export was broken since the modular refactor.
|
|
77
|
+
- Pass `--dsn` password through to all database backends (MySQL, SQL Server, Oracle, Firebird, DSN). Previously only PostgreSQL received the password from connection strings.
|
|
78
|
+
- Fix importer error reporting — `exception_info()` (returns tuple) replaced with `exception_desc()` (returns string) in 6 call sites across `importers/base.py`, `importers/csv.py`, and `importers/feather.py`. This caused `AttributeError: 'tuple' has no attribute 'replace'` on any import failure.
|
|
79
|
+
- Map Python encoding names to MySQL charset names in `LOAD DATA LOCAL INFILE` (e.g., `utf-8` → `utf8mb4`). Previously caused `Unknown character set` errors on MySQL imports.
|
|
80
|
+
- `--dsn` now overrides conf-file connection settings (server, database, user, port). Previously conf-file values took precedence, silently ignoring the DSN.
|
|
37
81
|
|
|
38
82
|
______________________________________________________________________
|
|
39
83
|
|
|
@@ -37,13 +37,13 @@ A multi-agent system where specialized agents collaborate to improve, extend, de
|
|
|
37
37
|
1. **Research** — Oracle investigates codebase, finds relevant code paths and impact
|
|
38
38
|
1. **Plan** — DBA synthesizes research into implementation approach, aligns with human
|
|
39
39
|
1. **Implement** — Patcher writes code, Oracle advises on architecture
|
|
40
|
-
1. **Test** — QA writes/runs tests, verifies coverage stays above
|
|
40
|
+
1. **Test** — QA writes/runs tests, verifies coverage stays above 80%
|
|
41
41
|
1. **Document** — Scribe updates docs, Herald updates changelog
|
|
42
42
|
1. **Review** — Inspector does final code review before human merge
|
|
43
43
|
|
|
44
44
|
## Constraints
|
|
45
45
|
|
|
46
|
-
- Coverage floor (
|
|
46
|
+
- Coverage floor (80%) must be maintained — QA blocks any change that drops it
|
|
47
47
|
- Backwards compatibility with upstream execsql v1.130.1 unless explicitly approved
|
|
48
48
|
- No destructive git operations without human approval
|
|
49
49
|
- Agents should always read `.claude/project_context.md` before starting work
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.4
|
|
4
4
|
Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
|
|
5
5
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
6
6
|
Project-URL: Issues, https://github.com/geocoug/execsql/issues
|
|
@@ -161,6 +161,35 @@ select id,name,created_at from users where active = true order by name;
|
|
|
161
161
|
-- !x! END LOOP
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
## Pre-commit Hook { #pre-commit }
|
|
165
|
+
|
|
166
|
+
`execsql-format` can be used as a [pre-commit](https://pre-commit.com/) hook. Add the following to your `.pre-commit-config.yaml`:
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
repos:
|
|
170
|
+
- repo: https://github.com/geocoug/execsql
|
|
171
|
+
rev: v2.4.2
|
|
172
|
+
hooks:
|
|
173
|
+
- id: execsql-format
|
|
174
|
+
args: [--in-place]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The hook runs on `*.sql` files. Pass any CLI options via `args`:
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
# Check-only (CI — fail if files need formatting)
|
|
181
|
+
- id: execsql-format
|
|
182
|
+
args: [--check]
|
|
183
|
+
|
|
184
|
+
# Auto-fix in place, skip SQL reformatting
|
|
185
|
+
- id: execsql-format
|
|
186
|
+
args: [--in-place, --no-sql]
|
|
187
|
+
|
|
188
|
+
# Custom indent width
|
|
189
|
+
- id: execsql-format
|
|
190
|
+
args: [--in-place, --indent, "2"]
|
|
191
|
+
```
|
|
192
|
+
|
|
164
193
|
## Exit Codes { #exit-codes }
|
|
165
194
|
|
|
166
195
|
| Code | Meaning |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.4"
|
|
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" }
|
|
@@ -155,7 +155,7 @@ skip-magic-trailing-comma = false
|
|
|
155
155
|
line-ending = "auto"
|
|
156
156
|
|
|
157
157
|
[tool.bumpversion]
|
|
158
|
-
current_version = "2.4.
|
|
158
|
+
current_version = "2.4.4"
|
|
159
159
|
commit = true
|
|
160
160
|
commit_args = "--no-verify"
|
|
161
161
|
tag = true
|
|
@@ -179,7 +179,7 @@ addopts = [
|
|
|
179
179
|
"--cov=execsql",
|
|
180
180
|
"--cov-branch",
|
|
181
181
|
"--cov-report=xml",
|
|
182
|
-
"--cov-fail-under=
|
|
182
|
+
"--cov-fail-under=80",
|
|
183
183
|
"--strict-markers",
|
|
184
184
|
"--strict-config",
|
|
185
185
|
"--color=yes",
|
|
@@ -141,18 +141,19 @@ def _run(
|
|
|
141
141
|
raise SystemExit(1)
|
|
142
142
|
db_type = db_type or parsed_dsn["db_type"]
|
|
143
143
|
conf.db_type = db_type
|
|
144
|
-
|
|
144
|
+
# DSN values override conf-file values — the CLI flag is explicit.
|
|
145
|
+
if parsed_dsn["server"]:
|
|
145
146
|
conf.server = parsed_dsn["server"]
|
|
146
|
-
if parsed_dsn["db"]
|
|
147
|
+
if parsed_dsn["db"]:
|
|
147
148
|
conf.db = parsed_dsn["db"]
|
|
148
|
-
if parsed_dsn["db_file"]
|
|
149
|
+
if parsed_dsn["db_file"]:
|
|
149
150
|
conf.db_file = parsed_dsn["db_file"]
|
|
150
|
-
if parsed_dsn["user"]
|
|
151
|
+
if parsed_dsn["user"]:
|
|
151
152
|
user = parsed_dsn["user"]
|
|
152
153
|
if parsed_dsn["password"]:
|
|
153
154
|
conf.db_password = parsed_dsn["password"]
|
|
154
155
|
conf.passwd_prompt = False
|
|
155
|
-
if parsed_dsn["port"]
|
|
156
|
+
if parsed_dsn["port"]:
|
|
156
157
|
port = parsed_dsn["port"]
|
|
157
158
|
|
|
158
159
|
# Apply CLI options over config-file values
|
|
@@ -538,6 +539,7 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
538
539
|
pw_needed=conf.passwd_prompt,
|
|
539
540
|
port=conf.port,
|
|
540
541
|
encoding=conf.db_encoding,
|
|
542
|
+
password=getattr(conf, "db_password", None),
|
|
541
543
|
)
|
|
542
544
|
elif conf.db_type == "l":
|
|
543
545
|
if conf.db_file is None:
|
|
@@ -553,6 +555,7 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
553
555
|
pw_needed=conf.passwd_prompt,
|
|
554
556
|
port=conf.port,
|
|
555
557
|
encoding=conf.db_encoding,
|
|
558
|
+
password=getattr(conf, "db_password", None),
|
|
556
559
|
)
|
|
557
560
|
elif conf.db_type == "k":
|
|
558
561
|
if conf.db_file is None:
|
|
@@ -568,6 +571,7 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
568
571
|
pw_needed=conf.passwd_prompt,
|
|
569
572
|
port=conf.port,
|
|
570
573
|
encoding=conf.db_encoding,
|
|
574
|
+
password=getattr(conf, "db_password", None),
|
|
571
575
|
)
|
|
572
576
|
elif conf.db_type == "f":
|
|
573
577
|
return db_Firebird(
|
|
@@ -577,6 +581,7 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
577
581
|
pw_needed=conf.passwd_prompt,
|
|
578
582
|
port=conf.port,
|
|
579
583
|
encoding=conf.db_encoding,
|
|
584
|
+
password=getattr(conf, "db_password", None),
|
|
580
585
|
)
|
|
581
586
|
elif conf.db_type == "d":
|
|
582
587
|
return db_Dsn(
|
|
@@ -584,6 +589,7 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
584
589
|
user=conf.username,
|
|
585
590
|
pw_needed=conf.passwd_prompt,
|
|
586
591
|
encoding=conf.db_encoding,
|
|
592
|
+
password=getattr(conf, "db_password", None),
|
|
587
593
|
)
|
|
588
594
|
else:
|
|
589
595
|
from execsql.utils.errors import fatal_error
|
|
@@ -93,9 +93,10 @@ def db_SqlServer(
|
|
|
93
93
|
pw_needed: bool = True,
|
|
94
94
|
port: int | None = None,
|
|
95
95
|
encoding: str | None = None,
|
|
96
|
+
password: str | None = None,
|
|
96
97
|
) -> SqlServerDatabase:
|
|
97
98
|
"""Open a Microsoft SQL Server connection via pyodbc."""
|
|
98
|
-
return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
99
|
+
return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
def db_MySQL(
|
|
@@ -105,9 +106,10 @@ def db_MySQL(
|
|
|
105
106
|
pw_needed: bool = True,
|
|
106
107
|
port: int | None = None,
|
|
107
108
|
encoding: str | None = None,
|
|
109
|
+
password: str | None = None,
|
|
108
110
|
) -> MySQLDatabase:
|
|
109
111
|
"""Open a MySQL or MariaDB connection via pymysql."""
|
|
110
|
-
return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
112
|
+
return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
|
|
111
113
|
|
|
112
114
|
|
|
113
115
|
def db_DuckDB(
|
|
@@ -136,9 +138,10 @@ def db_Oracle(
|
|
|
136
138
|
pw_needed: bool = True,
|
|
137
139
|
port: int | None = None,
|
|
138
140
|
encoding: str | None = None,
|
|
141
|
+
password: str | None = None,
|
|
139
142
|
) -> OracleDatabase:
|
|
140
143
|
"""Open an Oracle database connection via cx_Oracle (python-oracledb)."""
|
|
141
|
-
return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
144
|
+
return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
|
|
142
145
|
|
|
143
146
|
|
|
144
147
|
def db_Firebird(
|
|
@@ -148,9 +151,10 @@ def db_Firebird(
|
|
|
148
151
|
pw_needed: bool = True,
|
|
149
152
|
port: int | None = None,
|
|
150
153
|
encoding: str | None = None,
|
|
154
|
+
password: str | None = None,
|
|
151
155
|
) -> FirebirdDatabase:
|
|
152
156
|
"""Open a Firebird database connection via the firebird-driver package."""
|
|
153
|
-
return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
157
|
+
return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
|
|
154
158
|
|
|
155
159
|
|
|
156
160
|
def db_Dsn(
|
|
@@ -158,6 +162,7 @@ def db_Dsn(
|
|
|
158
162
|
user: str | None = None,
|
|
159
163
|
pw_needed: bool = True,
|
|
160
164
|
encoding: str | None = None,
|
|
165
|
+
password: str | None = None,
|
|
161
166
|
) -> DsnDatabase:
|
|
162
167
|
"""Open a connection to any ODBC data source registered under *dsn_name*."""
|
|
163
|
-
return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding)
|
|
168
|
+
return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding, password=password)
|
|
@@ -18,6 +18,18 @@ import execsql.state as _state
|
|
|
18
18
|
|
|
19
19
|
__all__ = ["MySQLDatabase"]
|
|
20
20
|
|
|
21
|
+
# Map Python encoding names to MySQL CHARACTER SET names for LOAD DATA INFILE.
|
|
22
|
+
_PYTHON_TO_MYSQL_CHARSET: dict[str, str] = {
|
|
23
|
+
"utf-8": "utf8mb4",
|
|
24
|
+
"utf8": "utf8mb4",
|
|
25
|
+
"latin-1": "latin1",
|
|
26
|
+
"iso-8859-1": "latin1",
|
|
27
|
+
"iso8859-1": "latin1",
|
|
28
|
+
"ascii": "ascii",
|
|
29
|
+
"cp1252": "cp1252",
|
|
30
|
+
"windows-1252": "cp1252",
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
|
|
22
34
|
class MySQLDatabase(Database):
|
|
23
35
|
"""MySQL and MariaDB adapter using the pymysql package."""
|
|
@@ -190,7 +202,8 @@ class MySQLDatabase(Database):
|
|
|
190
202
|
):
|
|
191
203
|
import_sql = f"load data local infile '{csv_file_obj.csvfname}' into table {sq_name}"
|
|
192
204
|
if csv_file_obj.encoding:
|
|
193
|
-
|
|
205
|
+
charset = _PYTHON_TO_MYSQL_CHARSET.get(csv_file_obj.encoding.lower(), csv_file_obj.encoding)
|
|
206
|
+
import_sql = f"{import_sql} character set {charset}"
|
|
194
207
|
if csv_file_obj.delimiter or csv_file_obj.quotechar:
|
|
195
208
|
import_sql = import_sql + " columns"
|
|
196
209
|
if csv_file_obj.delimiter:
|
|
@@ -28,7 +28,7 @@ def import_data_table(
|
|
|
28
28
|
hdrs: list[str],
|
|
29
29
|
data: list[Any],
|
|
30
30
|
) -> None:
|
|
31
|
-
from execsql.utils.errors import
|
|
31
|
+
from execsql.utils.errors import exception_desc
|
|
32
32
|
|
|
33
33
|
conf = _state.conf
|
|
34
34
|
if any(x is None or len(x.strip()) == 0 for x in hdrs):
|
|
@@ -91,7 +91,7 @@ def import_data_table(
|
|
|
91
91
|
raise ErrInfo(
|
|
92
92
|
type="db",
|
|
93
93
|
command_text=sql,
|
|
94
|
-
exception_msg=
|
|
94
|
+
exception_msg=exception_desc(),
|
|
95
95
|
other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
|
|
96
96
|
) from e
|
|
97
97
|
table_cols = db.table_columns(tablename, schemaname)
|
|
@@ -111,4 +111,4 @@ def import_data_table(
|
|
|
111
111
|
except ErrInfo:
|
|
112
112
|
raise
|
|
113
113
|
except Exception as e:
|
|
114
|
-
raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=
|
|
114
|
+
raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=exception_desc()) from e
|
|
@@ -32,7 +32,7 @@ def importtable(
|
|
|
32
32
|
encoding: str | None = None,
|
|
33
33
|
junk_header_lines: int = 0,
|
|
34
34
|
) -> None:
|
|
35
|
-
from execsql.utils.errors import
|
|
35
|
+
from execsql.utils.errors import exception_desc
|
|
36
36
|
|
|
37
37
|
conf = _state.conf
|
|
38
38
|
if not Path(filename).is_file():
|
|
@@ -66,7 +66,7 @@ def importtable(
|
|
|
66
66
|
raise ErrInfo(
|
|
67
67
|
type="db",
|
|
68
68
|
command_text=sql,
|
|
69
|
-
exception_msg=
|
|
69
|
+
exception_msg=exception_desc(),
|
|
70
70
|
other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
|
|
71
71
|
) from e
|
|
72
72
|
else:
|
|
@@ -91,7 +91,7 @@ def importtable(
|
|
|
91
91
|
fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
|
|
92
92
|
raise ErrInfo(
|
|
93
93
|
"exception",
|
|
94
|
-
exception_msg=
|
|
94
|
+
exception_msg=exception_desc(),
|
|
95
95
|
other_msg=f"Can't import tabular file ({filename}) to table ({fq_tablename})",
|
|
96
96
|
) from e
|
|
97
97
|
inf.close()
|
|
@@ -104,7 +104,7 @@ def importfile(
|
|
|
104
104
|
columname: str,
|
|
105
105
|
filename: str,
|
|
106
106
|
) -> None:
|
|
107
|
-
from execsql.utils.errors import
|
|
107
|
+
from execsql.utils.errors import exception_desc
|
|
108
108
|
|
|
109
109
|
if schemaname is not None:
|
|
110
110
|
if not db.table_exists(tablename, schemaname):
|
|
@@ -127,6 +127,6 @@ def importfile(
|
|
|
127
127
|
fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
|
|
128
128
|
raise ErrInfo(
|
|
129
129
|
"exception",
|
|
130
|
-
exception_msg=
|
|
130
|
+
exception_msg=exception_desc(),
|
|
131
131
|
other_msg=f"Can't import file ({filename}) to table ({fq_tablename})",
|
|
132
132
|
) from e
|
|
@@ -24,14 +24,14 @@ def import_feather(
|
|
|
24
24
|
filename: str,
|
|
25
25
|
is_new: Any,
|
|
26
26
|
) -> None:
|
|
27
|
-
from execsql.utils.errors import
|
|
27
|
+
from execsql.utils.errors import exception_desc
|
|
28
28
|
|
|
29
29
|
try:
|
|
30
30
|
import polars as pl
|
|
31
31
|
except Exception as e:
|
|
32
32
|
raise ErrInfo(
|
|
33
33
|
"exception",
|
|
34
|
-
exception_msg=
|
|
34
|
+
exception_msg=exception_desc(),
|
|
35
35
|
other_msg="The polars Python library must be installed to import data from the Feather format.",
|
|
36
36
|
) from e
|
|
37
37
|
df = pl.read_ipc(filename)
|
|
@@ -47,14 +47,14 @@ def import_parquet(
|
|
|
47
47
|
filename: str,
|
|
48
48
|
is_new: Any,
|
|
49
49
|
) -> None:
|
|
50
|
-
from execsql.utils.errors import
|
|
50
|
+
from execsql.utils.errors import exception_desc
|
|
51
51
|
|
|
52
52
|
try:
|
|
53
53
|
import polars as pl
|
|
54
54
|
except Exception as e:
|
|
55
55
|
raise ErrInfo(
|
|
56
56
|
"exception",
|
|
57
|
-
exception_msg=
|
|
57
|
+
exception_msg=exception_desc(),
|
|
58
58
|
other_msg="The polars Python library must be installed to import data from the Parquet format.",
|
|
59
59
|
) from e
|
|
60
60
|
df = pl.read_parquet(filename)
|
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
Tests for execsql.db.duckdb — DuckDBDatabase adapter.
|
|
3
3
|
|
|
4
4
|
Uses an in-memory DuckDB database so no files or external services are
|
|
5
|
-
needed. Tests exercise construction, DML, metadata queries,
|
|
6
|
-
DuckDB-specific overrides
|
|
5
|
+
needed. Tests exercise construction, DML, metadata queries, the
|
|
6
|
+
DuckDB-specific overrides, and error paths (ImportError, open_db failure,
|
|
7
|
+
exec_cmd exception propagation).
|
|
7
8
|
|
|
8
9
|
Note: DuckDBDatabase.__init__ calls open_db(), which connects to DuckDB
|
|
9
|
-
via the installed ``duckdb`` package.
|
|
10
|
-
installed
|
|
10
|
+
via the installed ``duckdb`` package. Most tests are skipped if duckdb is
|
|
11
|
+
not installed, but ImportError handling is tested independently of the
|
|
12
|
+
installed package.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
14
16
|
|
|
17
|
+
import sys
|
|
18
|
+
from unittest.mock import MagicMock, patch
|
|
19
|
+
|
|
15
20
|
import pytest
|
|
16
21
|
|
|
17
22
|
try:
|
|
@@ -185,3 +190,123 @@ class TestDuckDBTransactions:
|
|
|
185
190
|
|
|
186
191
|
def test_rollback_does_not_raise(self, db):
|
|
187
192
|
db.rollback()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# ImportError handling (lines 27-28) — tested without the pytestmark skip
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@pytest.mark.usefixtures("minimal_conf")
|
|
201
|
+
class TestDuckDBImportError:
|
|
202
|
+
"""Verify that missing duckdb package triggers fatal_error immediately.
|
|
203
|
+
|
|
204
|
+
This class is NOT decorated with the module-level pytestmark skip, so it
|
|
205
|
+
runs regardless of whether duckdb is installed. Setting sys.modules["duckdb"]
|
|
206
|
+
to None causes 'import duckdb' inside __init__ to raise ImportError, which
|
|
207
|
+
the constructor catches and translates to fatal_error().
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
# Explicitly clear the module-level skip marker for this class.
|
|
211
|
+
pytestmark = [] # type: ignore[assignment]
|
|
212
|
+
|
|
213
|
+
def test_missing_duckdb_calls_fatal_error(self):
|
|
214
|
+
"""When duckdb is absent, __init__ must call fatal_error before doing anything else."""
|
|
215
|
+
mock_fatal_error = MagicMock(side_effect=SystemExit(1))
|
|
216
|
+
|
|
217
|
+
# Setting sys.modules["duckdb"] = None makes 'import duckdb' raise ImportError.
|
|
218
|
+
with (
|
|
219
|
+
patch.dict(sys.modules, {"duckdb": None}),
|
|
220
|
+
patch("execsql.db.duckdb.fatal_error", mock_fatal_error),
|
|
221
|
+
pytest.raises(SystemExit),
|
|
222
|
+
):
|
|
223
|
+
DuckDBDatabase(":memory:")
|
|
224
|
+
mock_fatal_error.assert_called_once_with("The duckdb module is required.")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# open_db() error path (lines 50-60)
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TestDuckDBOpenDbError:
|
|
233
|
+
"""Verify that a connection failure in open_db() raises ErrInfo."""
|
|
234
|
+
|
|
235
|
+
def test_open_db_connection_failure_raises_errinfo(self, tmp_path):
|
|
236
|
+
"""A failing duckdb.connect wraps the exception in ErrInfo."""
|
|
237
|
+
from execsql.exceptions import ErrInfo
|
|
238
|
+
|
|
239
|
+
bad_path = str(tmp_path / "nonexistent" / "sub" / "db.duckdb")
|
|
240
|
+
with pytest.raises((ErrInfo, Exception)):
|
|
241
|
+
DuckDBDatabase(bad_path)
|
|
242
|
+
|
|
243
|
+
def test_open_db_not_called_when_conn_already_set(self, db):
|
|
244
|
+
"""open_db() is a no-op when self.conn is already populated."""
|
|
245
|
+
original_conn = db.conn
|
|
246
|
+
db.open_db() # Second call — should be a no-op, not replace the connection.
|
|
247
|
+
assert db.conn is original_conn
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# exec_cmd() (lines 62-72)
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestDuckDBExecCmd:
|
|
256
|
+
"""Tests for exec_cmd(), which queries a named view.
|
|
257
|
+
|
|
258
|
+
NOTE: duckdb.cursor.execute() does not accept bytes; exec_cmd encodes the
|
|
259
|
+
SQL string with cmd.encode(self.encoding) before passing it to the cursor.
|
|
260
|
+
This is a known limitation — the success path is tested by mocking the
|
|
261
|
+
cursor's execute() so the bytes call succeeds. The exception path is tested
|
|
262
|
+
with a non-existent view name, which raises before the encode matters.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def test_exec_cmd_success_path(self, db):
|
|
266
|
+
"""exec_cmd() reaches the add_substitution call when cursor.execute succeeds."""
|
|
267
|
+
import execsql.state as _state
|
|
268
|
+
from execsql.script.variables import SubVarSet
|
|
269
|
+
|
|
270
|
+
_state.subvars = SubVarSet()
|
|
271
|
+
|
|
272
|
+
mock_cursor = MagicMock()
|
|
273
|
+
mock_cursor.execute = MagicMock()
|
|
274
|
+
mock_cursor.rowcount = 3
|
|
275
|
+
|
|
276
|
+
with patch.object(db, "cursor", return_value=mock_cursor):
|
|
277
|
+
db.exec_cmd("myview")
|
|
278
|
+
|
|
279
|
+
mock_cursor.execute.assert_called_once()
|
|
280
|
+
call_args = mock_cursor.execute.call_args[0][0]
|
|
281
|
+
assert b"myview" in call_args # encoded bytes contain the view name
|
|
282
|
+
|
|
283
|
+
def test_exec_cmd_on_nonexistent_view_raises(self, db):
|
|
284
|
+
"""exec_cmd() propagates the exception for a non-existent view."""
|
|
285
|
+
import execsql.state as _state
|
|
286
|
+
from execsql.script.variables import SubVarSet
|
|
287
|
+
|
|
288
|
+
_state.subvars = SubVarSet()
|
|
289
|
+
|
|
290
|
+
with pytest.raises((RuntimeError, Exception)): # noqa: B017
|
|
291
|
+
db.exec_cmd("no_such_view_xyz")
|
|
292
|
+
|
|
293
|
+
def test_exec_cmd_rollback_called_on_error(self, db):
|
|
294
|
+
"""exec_cmd() calls rollback() before re-raising on execute failure."""
|
|
295
|
+
import execsql.state as _state
|
|
296
|
+
from execsql.script.variables import SubVarSet
|
|
297
|
+
|
|
298
|
+
_state.subvars = SubVarSet()
|
|
299
|
+
|
|
300
|
+
original_rollback = db.rollback
|
|
301
|
+
rollback_called = []
|
|
302
|
+
|
|
303
|
+
def tracking_rollback():
|
|
304
|
+
rollback_called.append(True)
|
|
305
|
+
original_rollback()
|
|
306
|
+
|
|
307
|
+
db.rollback = tracking_rollback
|
|
308
|
+
|
|
309
|
+
with pytest.raises((RuntimeError, Exception)): # noqa: B017
|
|
310
|
+
db.exec_cmd("no_such_view_xyz")
|
|
311
|
+
|
|
312
|
+
assert rollback_called, "rollback() was not called after exec_cmd failure"
|