execsql2 2.28.0__tar.gz → 2.29.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.28.0 → execsql2-2.29.0}/CHANGELOG.md +15 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/PKG-INFO +2 -2
- {execsql2-2.28.0 → execsql2-2.29.0}/README.md +1 -1
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/about/divergence.md +5 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/formatter.md +1 -1
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/lint.md +1 -1
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/metacommands.md +2 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/security.md +1 -1
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/substitution_vars.md +1 -1
- {execsql2-2.28.0 → execsql2-2.29.0}/pyproject.toml +2 -2
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/base.py +6 -9
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/mysql.py +19 -3
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/postgres.py +1 -1
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/xls.py +3 -2
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/variables.py +42 -10
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/types.py +8 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_postgres_inprocess.py +3 -0
- execsql2-2.29.0/tests/db/test_quoted_substitution_roundtrip.py +40 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_xls_importer.py +32 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_mysql.py +56 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_postgres.py +56 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke.sql +3 -3
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_substitution_injection.py +64 -20
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_types.py +17 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/uv.lock +1 -1
- {execsql2-2.28.0 → execsql2-2.29.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/.github/dependabot.yml +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/.gitignore +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/.pre-commit-config.yaml +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/.python-version +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/.readthedocs.yaml +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/CONTRIBUTING.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/LICENSE.txt +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/NOTICE +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/SECURITY.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/about/contributors.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/about/copyright.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/cli.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/db.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/exporters.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/importers.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/index.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/metacommands.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/architecture.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/releasing.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/getting-started/installation.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/debugging.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/documentation.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/editors.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/encoding.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/examples.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/logging.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/usage.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/actions.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/actions2.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/checkboxes.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/connect.b64 +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/connect.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/create_conf.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/entry_form.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/execsql_console.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/fatals.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/logo_small.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/unmatched.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/index.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/configuration.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/plugin-template/README.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/plugin-template/pyproject.toml +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/plugin-template/tests/test_plugin.py.example +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/.vscodeignore +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/LICENSE.txt +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/extension.js +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/icon.png +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/package-lock.json +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/justfile +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/scripts/benchmarks/README.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/scripts/benchmarks/results/.gitignore +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/scripts/benchmarks/run_benchmarks.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/__main__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/api.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/application.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/config.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/format.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/init.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/lint.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/listing.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/lsp.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/run.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/dispatch.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/help.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/options.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/run.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/config.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/config_validation.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/data/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/data/execsql.conf.template +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/data/script_header.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/access.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/factory.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/tiers.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exceptions.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/format.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/base.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/console.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/base.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/json.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/interactive.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/code_actions.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/completion.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/diagnostics.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/document.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/folding.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/formatting.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/hover.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/navigation.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/rename.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/server.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/manifest.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/control.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/effects.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/reference.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/upsert.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/models.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/parser.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/plugins.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/py.typed +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/ast.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/control.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/engine.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/executor.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/parser.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/state.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/state.pyi +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/color.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/README.md +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/config_settings.sqlite +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/make_config_db.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/md_compare.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/md_glossary.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/md_upsert.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/pg_compare.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/pg_glossary.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/pg_upsert.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/script_template.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/ss_compare.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/ss_glossary.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/templates/ss_upsert.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_cli.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_cli_surface.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_commands.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_config_validate.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_dispatch.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_error_location.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_init.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_lint.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_lint_fixtures.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_log_exit_record.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_manifest.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_ping.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_profile.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_sigterm.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_undeclared_imports.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/conftest.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/formatter_shapes/comment_anchoring.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/formatter_shapes/escapes_and_literals.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/formatter_shapes/substitution_and_metacommands.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/formatter_shapes/unparsable_sql.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/lint/conditions_and_blocks.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/lint/does_not_parse.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/lint/every_metacommand.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/lint/invalid.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_access_windows.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_base.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_copy_new.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_db_adapters_mocked.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_db_adapters_mocked_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_dsn.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_factory.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_factory_postgres_encoding.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_import_atomic.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_import_replacement_quoted.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_import_short_rows.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_import_timezones.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_mysql_case_folding.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_mysql_inprocess.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_new_table_checks.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_oracle_driver.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_postgres.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_roundtrip_adapters.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_sqlserver_inprocess.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_tiers.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_transactions.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/conftest.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_base.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_db.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_html_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_json.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_json_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_latex_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_ods_export.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_pretty_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_raw_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_columnar.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_dbfile.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_delimited.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_json.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_latex_xml.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_markup.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_ods.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_pretty_raw.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_template.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_values.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_xlsx.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_templates_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_values_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_backends.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_backends_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_compare_stats.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_compute_row_diffs.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_desktop_dialogs.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_format_table.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_tui_pilot.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_tui_pilot_complex.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_utils_gui_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/conftest.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_base_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_csv_blank_lines.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_csv_edge_cases.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_json_importer.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_roundtrip_csv.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/conftest.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/live_db.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_code_actions.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_completion.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_diagnostics.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_document.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_folding.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_formatting.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_hover.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_lsp_command.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_navigation.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_rename.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_server.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_conditions_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_pg_upsert.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_prompt.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_show_scripts.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/config_runtime.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/control_flow.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/error_handling.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/import_options.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/includes/helper.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/includes.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/io_formats.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/multi_db.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/predicates.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/scripts.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/smoke.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/template_export.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/timer.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/transactions.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/write_create_table.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/test_sql_scripts.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_env_detection.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_expansion_bomb.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_import_regex_hardening.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_log_redaction.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_log_secrets.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_odbc_injection.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_path_containment.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_zip_bomb.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_errors.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_filewriter.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_isolation.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_subprocess.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_ast.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_ast_parser.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_bundled_templates.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_config.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_config_data.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_config_extended.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_date_inference.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_debug_repl.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_engine.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_error_messages.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_exceptions.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_executor.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_executor_inprocess.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_format.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_interactive.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_mail.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_models.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_package.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_parser.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_parser_params.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_plugins.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_reference.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_registry.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_script.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_state.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_tooling_versions.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/__init__.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_auth.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_encodedfile_context.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_errors.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_keyring_rules.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_mail_tls.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_regex.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_strings.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_timer.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.28.0 → execsql2-2.29.0}/zensical.toml +0 -0
|
@@ -13,6 +13,21 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.29.0] - 2026-10-13
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`!'!var!'!` stores backslashes as written** on every DBMS: a value such as `C:\data\new` was stored as `C:\\data\\new` on SQLite, PostgreSQL, DuckDB, SQL Server, Oracle, Firebird and Access. MySQL/MariaDB (which read `\` as an escape) and PostgreSQL escape strings written as `E!'!var!'!` still get backslashes doubled, so both forms store the same value and the `E` workaround is no longer needed. Scripts that removed the doubled backslashes some other way should stop.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Oracle IMPORT and COPY with a time column** create it as text (`VARCHAR2(15)`, holding `HH:MM:SS`); `TO NEW` stopped with "Oracle DBMS type has no specification for data type time".
|
|
25
|
+
- **MySQL IMPORT accepts a header with capital letters** (`Id,Name`): it stopped with "columns that are not in table" (leaving the new table behind), and with `IMPORT_COMMON_COLUMNS_ONLY` it loaded those columns as NULL.
|
|
26
|
+
- **PostgreSQL IMPORT honors `SKIP n`** for quoted CSV files, which load through PostgreSQL's COPY: it skipped only the header line, loading the lines before it as data or failing on them.
|
|
27
|
+
- **Spreadsheet IMPORT reads every data row:** an XLSX row whose values are all `0` or FALSE no longer ends the data (it and every row after it were dropped), and an XLS sheet imported with `SKIP n` no longer loses its last *n* rows.
|
|
28
|
+
|
|
29
|
+
______________________________________________________________________
|
|
30
|
+
|
|
16
31
|
## [2.28.0] - 2026-10-13
|
|
17
32
|
|
|
18
33
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.29.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
|
|
@@ -466,7 +466,7 @@ execsql format --no-sql --in-place scripts/
|
|
|
466
466
|
```yaml
|
|
467
467
|
repos:
|
|
468
468
|
- repo: https://github.com/geocoug/execsql
|
|
469
|
-
rev: v2.
|
|
469
|
+
rev: v2.29.0
|
|
470
470
|
hooks:
|
|
471
471
|
- id: execsql-format
|
|
472
472
|
- id: execsql-lint
|
|
@@ -362,6 +362,11 @@ These are behavioral changes driven by security or correctness issues in the ups
|
|
|
362
362
|
| Partial dates inferred as timestamps | `dateutil` fills fields missing from a value with today's date, so `1-2`, `5/6`, `March 5`, `Jan` and `Monday` were inferred as timestamps and, on DuckDB, SQL Server, MySQL, Oracle, Firebird, Access and every ODS/XLSX/JSON/Parquet/Feather import, stored as dates in the current year. Inference now requires a four-digit year (or a month-first `m/d/yy` or `m-d-yy`) and a month and day, which restores upstream's behaviour for these values. `m-d-yy` is accepted in addition to upstream's `m/d/yy`. |
|
|
363
363
|
| Offset values stored as plain timestamps | With `dateutil`, a value with a UTC offset (`2024-01-01 10:00+05:00`) also counted as a plain timestamp, so a column mixing offset and plain values was created as `timestamp` and the driver converted the offset values with the client's time zone: the stored value depended on the machine that ran the import. A value with an offset is again not a plain timestamp, as with upstream's offset-free format list, so a mixed column is text and keeps each value as written. A column where every value has an offset is still `timestamptz`. |
|
|
364
364
|
| Binary columns in `COPY` | Every value a source cursor returned as bytes was decoded to text (undecodable bytes as `\xNN` escapes) before `COPY` wrote it, and `TO NEW` typed the column as text. Upstream did the same for SQLite BLOBs; with psycopg2 a PostgreSQL `bytea` arrived as a `memoryview` and escaped the decode, so for PostgreSQL this was an execsql2 regression. `COPY` now moves bytes unchanged and creates binary columns. Exports still write the decoded text. |
|
|
365
|
+
| Backslashes in `!'!var!'!` | Upstream doubled backslashes on Windows only, to protect `re.sub`'s replacement template; execsql2 2.x doubled them on every platform as SQL escaping, which corrupted values on every DBMS but MySQL. A backslash is now doubled only where it is an escape: on a MySQL/MariaDB connection, and in a PostgreSQL escape string written as `E!'!var!'!`. Apostrophes are doubled everywhere. |
|
|
366
|
+
| Time columns on Oracle | `IMPORT` / `COPY ... TO NEW` of a column of times stopped with "Oracle DBMS type has no specification for data type time" (a `DT_Time_Oracle` type was registered but inference never produced it). A time column is now created as `VARCHAR2(15)` holding the ISO time text. |
|
|
367
|
+
| Capitalized headers on MySQL | MySQL compared the file's headers with the table's lower-cased column names, so `Id` did not match `id`: IMPORT stopped (after `TO NEW` had created the table), and with `IMPORT_COMMON_COLUMNS_ONLY` such columns were skipped. The comparison now ignores case, as on PostgreSQL and the other DBMSs. |
|
|
368
|
+
| `SKIP` on PostgreSQL's COPY path | The PostgreSQL fast path (a quoted CSV file) skipped one line, the header, whatever `SKIP` said, so the skipped lines were loaded as data. It now starts after the `SKIP` lines like every other reader. |
|
|
369
|
+
| Spreadsheet rows on import | XLSX reading stopped at the first row with no truthy value, so a row of `0` / FALSE values ended the data; XLS reading with `SKIP n` stopped *n* rows before the end of the sheet. Both now read to the first row of empty cells. |
|
|
365
370
|
| Short data lines on import | A line with fewer values than the header was padded with NULLs only on PostgreSQL and MySQL. SQLite stopped with "Too few values", DuckDB and the other DBMSs with a driver parameter-count error. With `TO NEW` / `REPLACEMENT`, type inference skipped the missing values, so the column could be created `NOT NULL` and then reject the padded line, on PostgreSQL too. Missing trailing values are now NULL on every DBMS, and inference counts them as NULL. |
|
|
366
371
|
| `COPY` re-typed text values | Upstream described the destination table by re-reading the source's text values as if they came from a file, so a text column of `1`/`0` became `boolean` and one of dates became `timestamp` with `NEW` / `REPLACEMENT`, and values copied into an existing text column were converted on the way in (`1` stored as `true`). Text from the source database now stays text. Upstream also ran the source query twice, once to describe the table and once to copy it; execsql2 runs it once and holds the rows in a temporary file in between. |
|
|
367
372
|
| `REPLACEMENT` of a quoted or schema-qualified table | The SQLite, SQL Server, Oracle, Firebird and Access adapters quoted the table name a second time before dropping it, so `IMPORT` / `COPY ... TO REPLACEMENT "my tbl"` dropped a table named `"my tbl"` (quotes included) that did not exist and then failed to create the table, and on SQL Server, Oracle and Firebird `REPLACEMENT staging.t` tried to drop one table named `staging.t`. The name is now quoted once. |
|
|
@@ -270,7 +270,7 @@ select id,name,created_at from users where active = true order by name;
|
|
|
270
270
|
```yaml
|
|
271
271
|
repos:
|
|
272
272
|
- repo: https://github.com/geocoug/execsql
|
|
273
|
-
rev: v2.
|
|
273
|
+
rev: v2.29.0
|
|
274
274
|
hooks:
|
|
275
275
|
- id: execsql-format
|
|
276
276
|
- id: execsql-lint # optional: fail the commit on lint errors
|
|
@@ -158,7 +158,7 @@ The `execsql-lint` [pre-commit](https://pre-commit.com/) hook runs `execsql lint
|
|
|
158
158
|
```yaml
|
|
159
159
|
repos:
|
|
160
160
|
- repo: https://github.com/geocoug/execsql
|
|
161
|
-
rev: v2.
|
|
161
|
+
rev: v2.29.0
|
|
162
162
|
hooks:
|
|
163
163
|
- id: execsql-lint
|
|
164
164
|
args: [--ignore, V002] # optional: any execsql lint option
|
|
@@ -1950,6 +1950,8 @@ The delimiter characters that will be recognized in a text file, and that can be
|
|
|
1950
1950
|
|
|
1951
1951
|
The SKIP key phrase specifies the number of lines (or rows) at the beginning of the file (or worksheet) to discard before evaluating the remainder of the input as a data table.
|
|
1952
1952
|
|
|
1953
|
+
A worksheet's data ends at the first row whose cells are all empty; a row of zeros or FALSE values is data.
|
|
1954
|
+
|
|
1953
1955
|
If the NEW keyword is used, the input will be scanned to determine the data type of each column, and a CREATE TABLE statement run to create a new table for the data. A column is typed as a date or timestamp only if every value gives a full date with the year written out (`2024-01-05`, `1/5/2024`, `Jan 5, 2024`) or as a month-first two-digit year (`1/5/24`, `01-05-24`); values such as `1-2`, `5/6`, `March 5`, `Jan`, `Monday` or a day-first `13-05-24` leave the column as text. Scanning of the file to determine data formats is separate from the scanning that may be done to determine the quote and delimiter characters. If a table of that name already exists, the IMPORT stops before creating or loading anything, with the error `Table <name> already exists`. A table name without a schema is looked for only in the schema the table would be created in (on MySQL, the current database), so a table of the same name in another schema does not stop the IMPORT. If the table already exists when the NEW keyword is used, a fatal error will result and *execsql* will halt. If the REPLACEMENT keyword is used, the result is the same as if the NEW keyword were used, except that an existing table of the given name will be deleted first. This replacement is not atomic: if the drop succeeds but a later create or load step fails, the old table has already been removed. If the drop fails, *execsql* stops the IMPORT command before creating or loading the replacement table. *execsql* uses a "drop table" statement to drop an existing table, and the "drop table" statement may not succeed if there are dependencies on that table (see the discussion of [implicit drop table
|
|
1954
1956
|
statements](../guides/sql_syntax.md#implicit_drop)). If the table to be dropped does not exist, an informational message will be written to the log.
|
|
1955
1957
|
|
|
@@ -184,7 +184,7 @@ SELECT * FROM !!target_schema!!.mytable;
|
|
|
184
184
|
|
|
185
185
|
If a variable value is derived from external input (command-line arguments, environment variables, `PROMPT` responses, or data read from a file), the script author is responsible for quoting or validating that value before it is substituted into SQL.
|
|
186
186
|
|
|
187
|
-
The `!'!varname!'!` dereferencing form wraps the value in single quotes and doubles any embedded apostrophes, which helps when inserting text values into SQL literals:
|
|
187
|
+
The `!'!varname!'!` dereferencing form wraps the value in single quotes and doubles any embedded apostrophes (and, on MySQL/MariaDB, backslashes, so `\'` cannot end the literal there), which helps when inserting text values into SQL literals:
|
|
188
188
|
|
|
189
189
|
```sql
|
|
190
190
|
-- !x! sub author_name !!&AUTHOR!!
|
|
@@ -11,7 +11,7 @@ SUB <match_string> <replacement_string>
|
|
|
11
11
|
The \<match_string> identifies the substitution variable; the \<replacement_string> is the text inserted in its place. A substitution is recognized only when the match string is bracketed by one of three tokens:
|
|
12
12
|
|
|
13
13
|
- `!!name!!` — insert the value verbatim.
|
|
14
|
-
- `!'!name!'!` — wrap the value in single quotes, doubling any embedded apostrophe. Suitable for SQL string literals containing user data.
|
|
14
|
+
- `!'!name!'!` — wrap the value in single quotes, doubling any embedded apostrophe; on a MySQL or MariaDB connection, which reads `\` in a string literal as an escape, backslashes are doubled too. The value is stored exactly as written (`C:\data\new` stays `C:\data\new`). Suitable for SQL string literals containing user data. On PostgreSQL, `E!'!name!'!` (an escape string) doubles backslashes too, so it stores the same value as `!'!name!'!`; the `E` is not needed.
|
|
15
15
|
- `!"!name!"!` — wrap the value in double quotes. Suitable for SQL quoted identifiers, or for metacommand arguments that must be quoted.
|
|
16
16
|
|
|
17
17
|
Variables can also be set before the script starts: with `--var NAME=VALUE` on the [command line](../getting-started/syntax.md#options), or in the `[variables]` section of a [config file](configuration.md#configuration). `--var` wins over the config file, and a `SUB` in the script can reassign either.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.29.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" }
|
|
@@ -189,7 +189,7 @@ line-ending = "auto"
|
|
|
189
189
|
exclude = ["*.md"]
|
|
190
190
|
|
|
191
191
|
[tool.bumpversion]
|
|
192
|
-
current_version = "2.
|
|
192
|
+
current_version = "2.29.0"
|
|
193
193
|
commit = true
|
|
194
194
|
tag = true
|
|
195
195
|
tag_name = "v{new_version}"
|
|
@@ -215,21 +215,18 @@ class Database(ABC):
|
|
|
215
215
|
def quote_literal(self, value: Any) -> str:
|
|
216
216
|
"""Return *value* as a SQL string literal, safely escaped.
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
Override per-DBMS only when the wire protocol requires a different
|
|
225
|
-
literal form.
|
|
218
|
+
Standard SQL: wrap in single quotes and double embedded apostrophes;
|
|
219
|
+
a backslash is an ordinary character. Embedded NUL bytes are
|
|
220
|
+
rejected (most wire protocols truncate or reject them). A DBMS
|
|
221
|
+
that reads a backslash in a literal as an escape (MySQL) overrides
|
|
222
|
+
this.
|
|
226
223
|
"""
|
|
227
224
|
if value is None:
|
|
228
225
|
return "NULL"
|
|
229
226
|
s = str(value)
|
|
230
227
|
if "\x00" in s:
|
|
231
228
|
raise ValueError("SQL literal contains a NUL byte; refusing to quote.")
|
|
232
|
-
return "'" + s.replace("
|
|
229
|
+
return "'" + s.replace("'", "''") + "'"
|
|
233
230
|
|
|
234
231
|
def paramsubs(self, paramcount: int) -> str:
|
|
235
232
|
"""Return a comma-separated string of *paramcount* parameter placeholders."""
|
|
@@ -67,6 +67,20 @@ class MySQLDatabase(Database):
|
|
|
67
67
|
re.I,
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
+
def quote_literal(self, value: Any) -> str:
|
|
71
|
+
"""Return *value* as a string literal for MySQL/MariaDB.
|
|
72
|
+
|
|
73
|
+
Unless ``NO_BACKSLASH_ESCAPES`` is set, MySQL reads a backslash in a
|
|
74
|
+
literal as an escape, so ``\\'`` would end it: backslashes are doubled
|
|
75
|
+
as well as apostrophes.
|
|
76
|
+
"""
|
|
77
|
+
if value is None:
|
|
78
|
+
return "NULL"
|
|
79
|
+
s = str(value)
|
|
80
|
+
if "\x00" in s:
|
|
81
|
+
raise ValueError("SQL literal contains a NUL byte; refusing to quote.")
|
|
82
|
+
return "'" + s.replace("\\", "\\\\").replace("'", "''") + "'"
|
|
83
|
+
|
|
70
84
|
def __init__(
|
|
71
85
|
self,
|
|
72
86
|
server_name: str,
|
|
@@ -307,10 +321,12 @@ class MySQLDatabase(Database):
|
|
|
307
321
|
table_cols = self.table_columns(table_name, schema_name)
|
|
308
322
|
data_table_cols = [d.lower() for d in table_cols]
|
|
309
323
|
csv_file_cols = csv_file_obj.column_headers()
|
|
324
|
+
# MySQL column names are case-insensitive: compare without case, and
|
|
325
|
+
# send the file's own spelling.
|
|
310
326
|
if _state.conf.import_common_cols_only:
|
|
311
|
-
import_cols = [col for col in csv_file_cols if col in data_table_cols]
|
|
327
|
+
import_cols = [col for col in csv_file_cols if col.lower() in data_table_cols]
|
|
312
328
|
else:
|
|
313
|
-
unmatched_cols =
|
|
329
|
+
unmatched_cols = [col for col in csv_file_cols if col.lower() not in data_table_cols]
|
|
314
330
|
if len(unmatched_cols) > 0:
|
|
315
331
|
raise ErrInfo(
|
|
316
332
|
type="error",
|
|
@@ -326,7 +342,7 @@ class MySQLDatabase(Database):
|
|
|
326
342
|
# reads the format that was detected.
|
|
327
343
|
if (
|
|
328
344
|
csv_file_obj.escapechar is None
|
|
329
|
-
and data_table_cols == csv_file_cols
|
|
345
|
+
and data_table_cols == [col.lower() for col in csv_file_cols]
|
|
330
346
|
and _state.conf.empty_strings
|
|
331
347
|
and _state.conf.empty_rows
|
|
332
348
|
and not _state.conf.del_empty_cols
|
|
@@ -353,7 +353,7 @@ class PostgresDatabase(Database):
|
|
|
353
353
|
from execsql.exporters.delimited import blank_lines_as_empty_rows
|
|
354
354
|
|
|
355
355
|
# Use Postgres' COPY FROM method via psycopg3's cursor.copy() context manager.
|
|
356
|
-
rf = csv_file_obj.
|
|
356
|
+
rf = csv_file_obj.openclean("rt") # past the SKIP lines, as every other reader starts
|
|
357
357
|
if skipheader:
|
|
358
358
|
next(rf)
|
|
359
359
|
# Copy_from() requires a delimiter, but if there is none, feed it an
|
|
@@ -158,7 +158,7 @@ class XlsFile:
|
|
|
158
158
|
break
|
|
159
159
|
ncols += 1
|
|
160
160
|
sheet_data = []
|
|
161
|
-
for r in range(junk_header_rows, sheet.nrows
|
|
161
|
+
for r in range(junk_header_rows, sheet.nrows):
|
|
162
162
|
datarow = row_data(r, ncols)
|
|
163
163
|
if datarow.count(None) == len(datarow):
|
|
164
164
|
break
|
|
@@ -270,7 +270,8 @@ class XlsxFile:
|
|
|
270
270
|
sheet_data = []
|
|
271
271
|
rowsrc = sheet.iter_rows(min_row=junk_header_rows + 1, values_only=True)
|
|
272
272
|
for r in rowsrc:
|
|
273
|
-
|
|
273
|
+
# A row of zeros or FALSE values is data; only a row of empty cells ends it.
|
|
274
|
+
if all(c is None or c == "" for c in r):
|
|
274
275
|
break
|
|
275
276
|
sheet_data.append(list(r))
|
|
276
277
|
for r in range(len(sheet_data)):
|
|
@@ -17,6 +17,42 @@ from execsql.exceptions import ErrInfo
|
|
|
17
17
|
__all__ = ["CounterVars", "SubVarSet", "LocalSubVarSet", "ScriptArgSubVarSet"]
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _after_escape_string_prefix(command: str, start: int) -> bool:
|
|
21
|
+
"""Whether the ``!'!`` token at *start* follows an ``E`` that makes it a PostgreSQL escape string (``E!'!v!'!``).
|
|
22
|
+
|
|
23
|
+
The ``E`` must stand alone: in ``TYPE!'!v!'!`` it ends a word.
|
|
24
|
+
"""
|
|
25
|
+
if start < 1 or command[start - 1] not in "Ee":
|
|
26
|
+
return False
|
|
27
|
+
return start < 2 or not (command[start - 2].isalnum() or command[start - 2] == "_")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _sql_string_literal(value: str, *, escape_string: bool = False) -> str:
|
|
31
|
+
"""*value* as the string literal ``!'!var!'!`` substitutes, in the current connection's form.
|
|
32
|
+
|
|
33
|
+
An apostrophe is doubled everywhere; a backslash only where the DBMS
|
|
34
|
+
reads it as an escape (MySQL/MariaDB), or inside an escape string the
|
|
35
|
+
script wrote as ``E!'!var!'!`` (*escape_string*), so ``C:\\data`` is
|
|
36
|
+
stored as written in every case. Without a connection, the standard SQL
|
|
37
|
+
form. (Upstream doubled backslashes only on Windows, to protect
|
|
38
|
+
``re.sub``'s replacement template, not the SQL.)
|
|
39
|
+
"""
|
|
40
|
+
if escape_string:
|
|
41
|
+
return "'" + value.replace("\\", "\\\\").replace("'", "''") + "'"
|
|
42
|
+
db = None
|
|
43
|
+
try:
|
|
44
|
+
import execsql.state as _state
|
|
45
|
+
|
|
46
|
+
dbs = _state.dbs
|
|
47
|
+
if dbs is not None and dbs.current_db is not None:
|
|
48
|
+
db = dbs.current()
|
|
49
|
+
except Exception:
|
|
50
|
+
db = None
|
|
51
|
+
if db is not None:
|
|
52
|
+
return db.quote_literal(value)
|
|
53
|
+
return "'" + value.replace("'", "''") + "'"
|
|
54
|
+
|
|
55
|
+
|
|
20
56
|
# ---------------------------------------------------------------------------
|
|
21
57
|
# CounterVars
|
|
22
58
|
# ---------------------------------------------------------------------------
|
|
@@ -253,13 +289,10 @@ class SubVarSet:
|
|
|
253
289
|
)
|
|
254
290
|
quote = m.group("q")
|
|
255
291
|
if quote == "'":
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
# branch (``os.name != 'posix'``) applied the escape on
|
|
261
|
-
# the wrong axis — host OS vs target DBMS.
|
|
262
|
-
sub = "'" + sub.replace("\\", "\\\\").replace("'", "''") + "'"
|
|
292
|
+
sub = _sql_string_literal(
|
|
293
|
+
sub,
|
|
294
|
+
escape_string=_after_escape_string_prefix(command_str, m.start()),
|
|
295
|
+
)
|
|
263
296
|
elif quote == '"':
|
|
264
297
|
# B07a/F001: wrap in double quotes, doubling embedded
|
|
265
298
|
# ``"`` so a value containing ``"; DROP TABLE x; --``
|
|
@@ -306,13 +339,12 @@ class SubVarSet:
|
|
|
306
339
|
if idx != -1:
|
|
307
340
|
_check_nul(varname, sub)
|
|
308
341
|
return command_str[:idx] + sub + command_str[idx + len(token) :], True
|
|
309
|
-
# Single-quote-wrapped token: !'!varname!'! —
|
|
310
|
-
# double embedded ``'`` (see substitute_one for rationale).
|
|
342
|
+
# Single-quote-wrapped token: !'!varname!'! — a string literal.
|
|
311
343
|
tokenq = f"!'!{varname}!'!"
|
|
312
344
|
idxq = cmd_lower.find(tokenq)
|
|
313
345
|
if idxq != -1:
|
|
314
346
|
_check_nul(varname, sub)
|
|
315
|
-
wrapped =
|
|
347
|
+
wrapped = _sql_string_literal(sub, escape_string=_after_escape_string_prefix(command_str, idxq))
|
|
316
348
|
return (
|
|
317
349
|
command_str[:idxq] + wrapped + command_str[idxq + len(tokenq) :],
|
|
318
350
|
True,
|
|
@@ -884,6 +884,14 @@ dbt_oracle.name_datatype(DT_TimestampTZ, "TIMESTAMP WITH TIME ZONE")
|
|
|
884
884
|
dbt_oracle.name_datatype(DT_Timestamp, "TIMESTAMP", casting_name="TIMESTAMP")
|
|
885
885
|
dbt_oracle.name_datatype(DT_Date, "DATE", casting_name="DATE")
|
|
886
886
|
dbt_oracle.name_datatype(DT_Time_Oracle, "VARCHAR2", True, casting_name="VARCHAR(20)")
|
|
887
|
+
# Oracle has no time-of-day type, and oracledb cannot bind a datetime.time: a
|
|
888
|
+
# time column is stored as its ISO text (at most 15 characters, HH:MM:SS.ffffff).
|
|
889
|
+
dbt_oracle.name_datatype(
|
|
890
|
+
DT_Time,
|
|
891
|
+
"VARCHAR2(15)",
|
|
892
|
+
casting_name="VARCHAR(20)",
|
|
893
|
+
conv_mod_fn=lambda x: x.isoformat() if x is not None else None,
|
|
894
|
+
)
|
|
887
895
|
dbt_oracle.name_datatype(DT_Integer, "NUMBER")
|
|
888
896
|
dbt_oracle.name_datatype(DT_Long, "NUMBER")
|
|
889
897
|
dbt_oracle.name_datatype(DT_Float, "FLOAT")
|
|
@@ -430,6 +430,9 @@ def _make_csv_file_obj(tmp_path, headers, rows, *, delimiter=",", quotechar='"',
|
|
|
430
430
|
def open(self, _mode):
|
|
431
431
|
return open(self.csvfname, encoding=encoding)
|
|
432
432
|
|
|
433
|
+
def openclean(self, mode):
|
|
434
|
+
return self.open(mode) # no junk lines before the header
|
|
435
|
+
|
|
433
436
|
def reader(self):
|
|
434
437
|
import csv as _csv
|
|
435
438
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""``!'!var!'!`` stores a value exactly as written, backslashes and apostrophes included."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from execsql import run
|
|
8
|
+
|
|
9
|
+
VALUES = [r"C:\data\new", r"x\'; drop table t; --", "O'Brien", "\\", r"two\\backslashes"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _script(value: str) -> str:
|
|
13
|
+
return f"-- !x! SUB v {value}\ncreate table t (v text);\ninsert into t values (!'!v!'!);\n"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize("value", VALUES)
|
|
17
|
+
def test_sqlite_round_trip(tmp_path, value):
|
|
18
|
+
import sqlite3
|
|
19
|
+
|
|
20
|
+
db = tmp_path / "t.db"
|
|
21
|
+
result = run(sql=_script(value), dsn=f"sqlite:///{db}", new_db=True)
|
|
22
|
+
assert result.success, result.errors
|
|
23
|
+
con = sqlite3.connect(db)
|
|
24
|
+
try:
|
|
25
|
+
assert con.execute("select v from t").fetchall() == [(value,)]
|
|
26
|
+
finally:
|
|
27
|
+
con.close()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.parametrize("value", VALUES)
|
|
31
|
+
def test_duckdb_round_trip(tmp_path, value):
|
|
32
|
+
duckdb = pytest.importorskip("duckdb")
|
|
33
|
+
db = tmp_path / "t.duckdb"
|
|
34
|
+
result = run(sql=_script(value), dsn=f"duckdb:///{db}", new_db=True)
|
|
35
|
+
assert result.success, result.errors
|
|
36
|
+
con = duckdb.connect(str(db), read_only=True)
|
|
37
|
+
try:
|
|
38
|
+
assert con.execute("select v from t").fetchall() == [(value,)]
|
|
39
|
+
finally:
|
|
40
|
+
con.close()
|
|
@@ -182,3 +182,35 @@ class TestImportXls:
|
|
|
182
182
|
names = [r[0] for r in rows]
|
|
183
183
|
assert "existing" in names
|
|
184
184
|
assert "new" in names
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestSpreadsheetRowsAfterTheHeader:
|
|
188
|
+
"""Every data row is read: a row of zeros is data, and SKIP does not shorten the sheet."""
|
|
189
|
+
|
|
190
|
+
def test_an_xlsx_row_of_zeros_and_false_does_not_end_the_data(self, tmp_path):
|
|
191
|
+
rows = [["id", "flag", "note"], [1, True, "a"], [0, False, None], [2, True, "c"]]
|
|
192
|
+
path = _make_xlsx(tmp_path, "Sheet1", rows)
|
|
193
|
+
_hdrs, data = xls_data(path, "Sheet1", 0)
|
|
194
|
+
assert len(data) == 3
|
|
195
|
+
|
|
196
|
+
def test_an_xlsx_row_of_empty_cells_still_ends_the_data(self, tmp_path):
|
|
197
|
+
path = _make_xlsx(tmp_path, "Sheet1", [["id"], [1], [None], [2]])
|
|
198
|
+
_hdrs, data = xls_data(path, "Sheet1", 0)
|
|
199
|
+
assert len(data) == 1
|
|
200
|
+
|
|
201
|
+
def test_xls_skip_keeps_the_last_rows(self):
|
|
202
|
+
from types import SimpleNamespace
|
|
203
|
+
|
|
204
|
+
from execsql.exporters.xls import XlsFile
|
|
205
|
+
|
|
206
|
+
def cell(value):
|
|
207
|
+
return SimpleNamespace(ctype=1 if isinstance(value, str) else 2, value=value)
|
|
208
|
+
|
|
209
|
+
sheet_rows = [["junk"], ["junk"], ["id", "v"], [1, 1], [2, 2], [3, 3]]
|
|
210
|
+
sheet = SimpleNamespace(nrows=len(sheet_rows), row_slice=lambda r: [cell(v) for v in sheet_rows[r]])
|
|
211
|
+
xls = object.__new__(XlsFile)
|
|
212
|
+
xls.filename = "fake.xls"
|
|
213
|
+
xls.sheet_named = lambda name: sheet
|
|
214
|
+
data = xls.sheet_data("Sheet1", junk_header_rows=2)
|
|
215
|
+
assert data[0] == ["id", "v"]
|
|
216
|
+
assert [r[0] for r in data[1:]] == [1, 2, 3]
|
|
@@ -485,3 +485,59 @@ class TestImportBackslashes:
|
|
|
485
485
|
|
|
486
486
|
def test_a_backslash_in_the_file_path_is_kept(self, tmp_path):
|
|
487
487
|
assert self._import(tmp_path, tmp_path / "dir\\with\\backslashes") == self.VALUES
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class TestHeadersWithCapitals:
|
|
491
|
+
"""MySQL column names are case-insensitive; a header with capitals must match its column."""
|
|
492
|
+
|
|
493
|
+
@pytest.mark.parametrize("quoted", [False, True], ids=["load-data", "row-by-row"])
|
|
494
|
+
def test_import_to_new_loads_every_column(self, tmp_path, quoted):
|
|
495
|
+
_exec_mysql("DROP TABLE IF EXISTS t_mixed")
|
|
496
|
+
_write_conf(tmp_path)
|
|
497
|
+
csv_path = tmp_path / "mixed.csv"
|
|
498
|
+
# An escape character sends the file down the row-by-row path.
|
|
499
|
+
body = 'Id,Name\n1,"Ann \\"A\\""\n2,Bo\n' if quoted else "Id,Name\n1,Ann\n2,Bo\n"
|
|
500
|
+
csv_path.write_text(body)
|
|
501
|
+
script = write_script(tmp_path, f"-- !x! IMPORT TO NEW t_mixed FROM {csv_path}\n")
|
|
502
|
+
try:
|
|
503
|
+
result = _run_execsql_mysql(tmp_path, script)
|
|
504
|
+
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
505
|
+
rows = _query_mysql("SELECT Id, Name FROM t_mixed ORDER BY Id")
|
|
506
|
+
assert [r[0] for r in rows] == [1, 2]
|
|
507
|
+
assert all(r[1] for r in rows)
|
|
508
|
+
finally:
|
|
509
|
+
_exec_mysql("DROP TABLE IF EXISTS t_mixed")
|
|
510
|
+
|
|
511
|
+
def test_common_columns_only_keeps_a_capitalized_column(self, tmp_path):
|
|
512
|
+
_exec_mysql("DROP TABLE IF EXISTS t_common")
|
|
513
|
+
_exec_mysql("CREATE TABLE t_common (id INTEGER, name VARCHAR(10))")
|
|
514
|
+
_write_conf(tmp_path)
|
|
515
|
+
csv_path = tmp_path / "common.csv"
|
|
516
|
+
csv_path.write_text("Id,Name,Extra\n1,Ann,x\n")
|
|
517
|
+
script = write_script(
|
|
518
|
+
tmp_path,
|
|
519
|
+
f"-- !x! IMPORT_COMMON_COLUMNS_ONLY YES\n-- !x! IMPORT TO t_common FROM {csv_path}\n",
|
|
520
|
+
)
|
|
521
|
+
try:
|
|
522
|
+
result = _run_execsql_mysql(tmp_path, script)
|
|
523
|
+
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
524
|
+
assert [tuple(r) for r in _query_mysql("SELECT id, name FROM t_common")] == [(1, "Ann")]
|
|
525
|
+
finally:
|
|
526
|
+
_exec_mysql("DROP TABLE IF EXISTS t_common")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@pytest.mark.parametrize("value", [r"C:\data\new", r"x\'; drop table t; --", "O'Brien", r"two\\backslashes"])
|
|
530
|
+
def test_quoted_substitution_stores_the_value_as_written(tmp_path, value):
|
|
531
|
+
"""MySQL reads \\ as an escape, so !'!v!'! doubles it there, and \\' cannot end the literal."""
|
|
532
|
+
_exec_mysql("DROP TABLE IF EXISTS quoted_sub")
|
|
533
|
+
_write_conf(tmp_path)
|
|
534
|
+
script = write_script(
|
|
535
|
+
tmp_path,
|
|
536
|
+
f"-- !x! SUB v {value}\ncreate table quoted_sub (v text);\ninsert into quoted_sub values (!'!v!'!);\n",
|
|
537
|
+
)
|
|
538
|
+
try:
|
|
539
|
+
result = _run_execsql_mysql(tmp_path, script)
|
|
540
|
+
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
541
|
+
assert [tuple(r) for r in _query_mysql("SELECT v FROM quoted_sub")] == [(value,)]
|
|
542
|
+
finally:
|
|
543
|
+
_exec_mysql("DROP TABLE IF EXISTS quoted_sub")
|
|
@@ -686,3 +686,59 @@ def test_copy_moves_a_bytea_column_byte_for_byte(tmp_path):
|
|
|
686
686
|
assert row == ("bytea", payload)
|
|
687
687
|
finally:
|
|
688
688
|
run(sql="drop table if exists bin_src;\ndrop table if exists bin_dst;\n", dsn=_PG_DSN)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def test_import_skip_applies_on_the_copy_path(tmp_path):
|
|
692
|
+
"""A quoted CSV takes PostgreSQL's COPY path; SKIP n must drop n lines before the header there too."""
|
|
693
|
+
from execsql import run
|
|
694
|
+
|
|
695
|
+
csv = tmp_path / "skip.csv"
|
|
696
|
+
csv.write_text('exported by a tool\nsecond preamble line\nid,name\n"1","one"\n"2","two"\n', encoding="utf-8")
|
|
697
|
+
run(sql="drop table if exists skip_target;\n", dsn=_PG_DSN)
|
|
698
|
+
try:
|
|
699
|
+
result = run(sql=f"-- !x! IMPORT TO NEW skip_target FROM {csv} SKIP 2\n", dsn=_PG_DSN)
|
|
700
|
+
assert result.success, result.errors
|
|
701
|
+
with psycopg.connect(**_PG_CONNECT_KWARGS) as conn:
|
|
702
|
+
rows = conn.execute("select id, name from skip_target order by id").fetchall()
|
|
703
|
+
assert [tuple(map(str, r)) for r in rows] == [("1", "one"), ("2", "two")]
|
|
704
|
+
finally:
|
|
705
|
+
run(sql="drop table if exists skip_target;\n", dsn=_PG_DSN)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@pytest.mark.parametrize("value", [r"C:\data\new", r"x\'; drop table t; --", "O'Brien", r"two\\backslashes"])
|
|
709
|
+
def test_quoted_substitution_stores_the_value_as_written(value):
|
|
710
|
+
"""standard_conforming_strings is on: a backslash in '...' is text, so !'!v!'! must not double it."""
|
|
711
|
+
from execsql import run
|
|
712
|
+
|
|
713
|
+
run(sql="drop table if exists quoted_sub;\n", dsn=_PG_DSN)
|
|
714
|
+
try:
|
|
715
|
+
result = run(
|
|
716
|
+
sql=f"-- !x! SUB v {value}\ncreate table quoted_sub (v text);\ninsert into quoted_sub values (!'!v!'!);\n",
|
|
717
|
+
dsn=_PG_DSN,
|
|
718
|
+
)
|
|
719
|
+
assert result.success, result.errors
|
|
720
|
+
with psycopg.connect(**_PG_CONNECT_KWARGS) as conn:
|
|
721
|
+
assert conn.execute("select v from quoted_sub").fetchall() == [(value,)]
|
|
722
|
+
finally:
|
|
723
|
+
run(sql="drop table if exists quoted_sub;\n", dsn=_PG_DSN)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@pytest.mark.parametrize("value", [r"C:\data\new", r"x\'; drop table t; --", r"two\\backslashes"])
|
|
727
|
+
def test_escape_string_form_stores_the_same_value(value):
|
|
728
|
+
"""E!'!v!'! (a PostgreSQL escape string) stores exactly what !'!v!'! stores."""
|
|
729
|
+
from execsql import run
|
|
730
|
+
|
|
731
|
+
run(sql="drop table if exists quoted_sub_e;\n", dsn=_PG_DSN)
|
|
732
|
+
try:
|
|
733
|
+
result = run(
|
|
734
|
+
sql=(
|
|
735
|
+
f"-- !x! SUB v {value}\ncreate table quoted_sub_e (plain text, escaped text);\n"
|
|
736
|
+
"insert into quoted_sub_e values (!'!v!'!, E!'!v!'!);\n"
|
|
737
|
+
),
|
|
738
|
+
dsn=_PG_DSN,
|
|
739
|
+
)
|
|
740
|
+
assert result.success, result.errors
|
|
741
|
+
with psycopg.connect(**_PG_CONNECT_KWARGS) as conn:
|
|
742
|
+
assert conn.execute("select plain, escaped from quoted_sub_e").fetchall() == [(value, value)]
|
|
743
|
+
finally:
|
|
744
|
+
run(sql="drop table if exists quoted_sub_e;\n", dsn=_PG_DSN)
|
|
@@ -143,11 +143,11 @@ drop table if exists q_bs_chk;
|
|
|
143
143
|
CREATE TABLE q_bs_chk (val TEXT);
|
|
144
144
|
INSERT INTO q_bs_chk SELECT val FROM q_bs;
|
|
145
145
|
|
|
146
|
-
-- SQLite
|
|
146
|
+
-- SQLite reads no escapes in a literal, so the value round-trips byte for byte.
|
|
147
147
|
-- !x! select_sub q_bs_chk
|
|
148
|
-
-- !x! ASSERT EQUALS("!!@val!!", "c
|
|
148
|
+
-- !x! ASSERT EQUALS("!!@val!!", "c:\path\file") "a backslash is stored as written outside MySQL"
|
|
149
149
|
|
|
150
|
-
INSERT INTO _runlog VALUES ('phase5', 'backslash
|
|
150
|
+
INSERT INTO _runlog VALUES ('phase5', 'backslash stored as written');
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
-- =========================================================================
|