execsql2 2.31.1__tar.gz → 2.32.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.31.1 → execsql2-2.32.0}/CHANGELOG.md +21 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/PKG-INFO +2 -2
- {execsql2-2.31.1 → execsql2-2.32.0}/README.md +1 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/about/divergence.md +3 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/api/index.md +2 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/dev/adding_db_adapters.md +1 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/dev/architecture.md +1 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/formatter.md +1 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/reference/lint.md +1 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/vscode-execsql/package-lock.json +2 -2
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/vscode-execsql/package.json +1 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/pyproject.toml +2 -2
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/access.py +3 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/base.py +19 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/dsn.py +3 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/duckdb.py +60 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/odbc.py +18 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/sqlserver.py +3 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/duckdb.py +2 -10
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/models.py +73 -48
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/types.py +14 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/datetime.py +16 -2
- execsql2-2.32.0/tests/db/test_bulk_inserts.py +169 -0
- execsql2-2.32.0/tests/db/test_odbc_fresh_sessions.py +54 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_postgres_inprocess.py +58 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_sqlserver_inprocess.py +29 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_models.py +22 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_datetime.py +31 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/uv.lock +1 -1
- {execsql2-2.31.1 → execsql2-2.32.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/.github/dependabot.yml +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/.gitignore +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/.pre-commit-config.yaml +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/.python-version +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/.readthedocs.yaml +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/CONTRIBUTING.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/LICENSE.txt +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/NOTICE +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/SECURITY.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/about/contributors.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/about/copyright.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/api/cli.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/api/db.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/api/exporters.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/api/importers.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/api/metacommands.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/dev/releasing.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/getting-started/installation.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/debugging.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/documentation.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/editors.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/encoding.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/examples.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/logging.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/usage.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/actions.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/actions2.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/checkboxes.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/connect.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/fatals.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/unmatched.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/index.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/reference/configuration.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/reference/metacommands.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/reference/security.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/templates/compare.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/templates/glossary.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/templates/index.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/docs/templates/upsert.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/plugin-template/README.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/plugin-template/pyproject.toml +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/plugin-template/tests/test_plugin.py.example +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/vscode-execsql/.vscodeignore +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/vscode-execsql/LICENSE.txt +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/vscode-execsql/extension.js +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/vscode-execsql/icon.png +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/justfile +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/scripts/benchmarks/README.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/scripts/benchmarks/results/.gitignore +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/scripts/benchmarks/run_benchmarks.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/scripts/release_notes.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/__main__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/api.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/application.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/commands/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/commands/config.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/commands/format.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/commands/init.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/commands/lint.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/commands/listing.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/commands/lsp.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/commands/run.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/dispatch.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/help.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/options.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/cli/run.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/config.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/config_validation.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/data/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/data/execsql.conf.template +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/data/script_header.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/factory.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/db/tiers.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exceptions.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/format.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/gui/base.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/gui/console.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/importers/base.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/importers/json.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/interactive.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/_util.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/cache.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/code_actions.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/completion.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/diagnostics.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/document.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/folding.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/formatting.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/hover.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/navigation.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/rename.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/lsp/server.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/manifest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/control.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/effects.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/reference.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/metacommands/upsert.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/parser.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/plugins.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/py.typed +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/script/ast.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/script/control.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/script/engine.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/script/executor.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/script/parser.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/script/variables.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/state.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/state.pyi +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/color.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/paths.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/README.md +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/config_settings.sqlite +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/make_config_db.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/md_compare.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/md_glossary.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/md_upsert.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/pg_compare.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/pg_glossary.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/script_template.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/ss_compare.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/ss_glossary.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/templates/ss_upsert.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_cli.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_cli_surface.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_commands.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_config_validate.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_dispatch.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_error_location.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_init.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_lint.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_lint_fixtures.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_log_exit_record.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_manifest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_ping.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_profile.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_sigterm.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/cli/test_undeclared_imports.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/conftest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/formatter_shapes/comment_anchoring.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/formatter_shapes/escapes_and_literals.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/formatter_shapes/substitution_and_metacommands.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/formatter_shapes/unparsable_sql.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/lint/conditions_and_blocks.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/lint/does_not_parse.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/lint/every_metacommand.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/lint/invalid.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/ods/libreoffice.fods +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/data/ods/libreoffice.ods +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/conftest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_access_windows.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_base.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_copy_new.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_db_adapters_mocked.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_db_adapters_mocked_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_dsn.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_factory.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_factory_postgres_encoding.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_import_atomic.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_import_replacement_quoted.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_import_short_rows.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_import_timezones.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_mysql_case_folding.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_mysql_inprocess.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_mysql_trailing_blank_lines.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_new_table_checks.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_oracle_driver.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_postgres.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_quoted_substitution_roundtrip.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_replacement_missing_table.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_roundtrip_adapters.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_sqlserver_driver18.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_tiers.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/db/test_transactions.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/conftest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_base.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_db.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_html_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_json.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_json_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_latex_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_ods_export.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_pretty_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_raw_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_columnar.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_dbfile.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_delimited.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_json.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_latex_xml.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_markup.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_ods.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_pretty_raw.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_template.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_values.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_roundtrip_xlsx.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_templates_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_values_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_values_roundtrip.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_backends.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_backends_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_compare_stats.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_compute_row_diffs.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_desktop_dialogs.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_format_table.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_tui_pilot.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_tui_pilot_complex.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/gui/test_utils_gui_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/conftest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_base_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_csv_blank_lines.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_csv_edge_cases.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_json_importer.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_ods_values.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_roundtrip_csv.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/integration/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/integration/conftest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/live_db.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_cache.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_code_actions.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_completion.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_diagnostics.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_document.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_folding.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_formatting.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_hover.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_lsp_command.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_navigation.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_rename.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/lsp/test_server.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/conftest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_conditions_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_pg_upsert.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_prompt.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_serve_output.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/metacommands/test_show_scripts.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/audit_smoke.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/config_runtime.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/control_flow.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/error_handling.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/import_options.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/includes/helper.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/includes.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/io_formats.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/multi_db.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/predicates.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/scripts.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/smoke.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/template_export.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/timer.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/transactions.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/write_create_table.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/scripts/test_sql_scripts.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_env_detection.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_expansion_bomb.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_import_regex_hardening.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_log_redaction.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_log_secrets.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_network_paths.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_odbc_connection_strings.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_odbc_injection.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_path_containment.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_substitution_injection.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/security/test_zip_bomb.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/templates/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/templates/conftest.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/templates/test_compare.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/templates/test_glossary.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/templates/test_static.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/templates/test_upsert.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_api.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_api_errors.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_api_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_api_filewriter.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_api_isolation.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_api_subprocess.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_ast.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_ast_parser.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_bundled_templates.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_config.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_config_data.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_config_extended.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_date_inference.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_debug_repl.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_engine.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_error_messages.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_exceptions.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_executor.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_executor_inprocess.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_format.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_interactive.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_mail.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_package.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_parser.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_parser_params.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_plugins.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_reference.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_registry.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_release_notes.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_script.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_state.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_tooling_versions.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/test_types.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/__init__.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_auth.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_encodedfile_context.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_errors.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_keyring_rules.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_mail_tls.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_regex.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_strings.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_timer.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.31.1 → execsql2-2.32.0}/zensical.toml +0 -0
|
@@ -13,6 +13,27 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.32.0] - 2026-10-16
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Working out column types is about twice as fast** for `IMPORT`, `COPY` and the exports that create a table (`EXPORT … AS SQLITE`, `DUCKDB`, `JSON`, `FEATHER`); a CSV whose dates repeat scans close to three times as fast. The types chosen are the same.
|
|
21
|
+
- **`IMPORT`, `COPY` and `EXPORT … AS DUCKDB` into DuckDB are about five times as fast**: they insert many rows per statement. Copying 200,000 rows took 124 s and now takes 22 s.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A column of large integers**, such as epoch milliseconds, no longer makes `IMPORT`, `COPY` or those exports hang or run out of memory while execsql works out the column types.
|
|
26
|
+
|
|
27
|
+
______________________________________________________________________
|
|
28
|
+
|
|
29
|
+
## [2.31.2] - 2026-10-16
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **SQL Server, Access and ODBC DSN connections** start a new database session on every `CONNECT` and `execsql.run()`: temp tables and `SET` options no longer carry over from an earlier connection in the same process on macOS and Linux. execsql turns off pyodbc's connection pooling, which also applies to other pyodbc connections your program makes.
|
|
34
|
+
|
|
35
|
+
______________________________________________________________________
|
|
36
|
+
|
|
16
37
|
## [2.31.1] - 2026-10-16
|
|
17
38
|
|
|
18
39
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.32.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/en/stable/
|
|
6
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
@@ -441,7 +441,7 @@ execsql format --no-sql --in-place scripts/
|
|
|
441
441
|
```yaml
|
|
442
442
|
repos:
|
|
443
443
|
- repo: https://github.com/geocoug/execsql
|
|
444
|
-
rev: v2.
|
|
444
|
+
rev: v2.32.0
|
|
445
445
|
hooks:
|
|
446
446
|
- id: execsql-format
|
|
447
447
|
- id: execsql-lint
|
|
@@ -264,6 +264,8 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
|
|
|
264
264
|
- **PostgreSQL driver is psycopg3** — the PostgreSQL adapter uses `psycopg` (psycopg3, installed via the `psycopg[binary]` extra) instead of upstream's `psycopg2`. The connection now passes the libpq `dbname` keyword (psycopg3 rejects psycopg2's `database`), `VACUUM` toggles `conn.autocommit` instead of the removed `set_session()`, server-side `COPY` import uses psycopg3's `cursor.copy()` context manager instead of `copy_expert()`, and binary `IMPORT` sends raw `bytes` instead of `psycopg2.Binary()`. This also makes `db.conn` directly usable by the `PG_UPSERT` metacommand's pg-upsert ≥1.26.0 dependency, which itself requires a psycopg3 connection. The connection sets `prepare_threshold=None` to disable psycopg3's automatic server-side prepared statements, which psycopg2 never used; without this, re-running a query (e.g. via `EXPORT`) against an object the script drops/recreates or alters raises `FeatureNotSupported: cached plan must not change result type`.
|
|
265
265
|
- **Oracle driver is python-oracledb** — the Oracle adapter imports `oracledb`, the driver the `oracle` extra installs, instead of upstream's `cx_Oracle` (which ships no wheels for Python 3.12+). The `makedsn` / `connect` calls are the same. `cx_Oracle` is still used when it is the only Oracle driver installed.
|
|
266
266
|
- **SQL Server tries ODBC Driver 18 first** — upstream's driver list stopped at Driver 17, so a machine with only Microsoft's current driver could not connect. Driver 18 encrypts the connection and verifies the server's certificate; the `trust_server_certificate` setting accepts a self-signed one.
|
|
267
|
+
- **DuckDB bulk inserts** — `IMPORT`, `COPY` and `EXPORT ... AS DUCKDB` insert many rows per `INSERT ... VALUES` statement instead of `executemany()`, which DuckDB runs one row at a time; the values stored are the same. While binding them, and only when pandas is not installed, `sys.modules["pandas"]` is `None`: the duckdb package looks pandas up twice per row, and without that entry each lookup searched `sys.path`.
|
|
268
|
+
- **Each pyodbc connection is a new session** — the SQL Server, Access and ODBC DSN adapters set `pyodbc.pooling = False` before they connect. With pyodbc's default pooling, unixODBC (macOS, Linux) handed a closed connection's session unchanged to the next connection to the same server, so temp tables and `SET` options from one `CONNECT`, or one `execsql.run()`, were still there in the next. pyodbc applies the setting at the process's first pyodbc connection, so it also covers other pyodbc connections the process makes, and has no effect if the program connected with pyodbc before execsql did.
|
|
267
269
|
- **`Database` is an ABC** — `open_db()` and `exec_cmd()` are abstract methods. Subclasses that omit them raise `TypeError` at instantiation instead of at call time.
|
|
268
270
|
- **Connection timeouts** — PostgreSQL and SQLite adapters accept a connection timeout parameter (default 30 seconds).
|
|
269
271
|
- **DuckDB temporal types** — `TIMESTAMPTZ`, `TIMESTAMP`, `DATE`, `TIME` now map to native DuckDB types instead of `TEXT`.
|
|
@@ -433,6 +435,7 @@ These are behavioral changes driven by security or correctness issues in the ups
|
|
|
433
435
|
| ODS export replaced newlines inside a value with spaces | String cells were written after `item.replace("\n", " ")`, so an exported value silently differed from the one in the database. An XML parser does normalise a newline inside an attribute, so `office:string-value` cannot carry one — but ODF represents a multi-line cell as one `<text:p>` per line, which is what LibreOffice writes and what execsql2 now writes. Single-line values still use the attribute. |
|
|
434
436
|
| XLSX export wrote `numeric` columns as text | `_cell_value()` passed `int` and `float` straight to openpyxl but let every other type fall through to `str()`. PostgreSQL, MySQL, and DuckDB all return `Decimal` for `numeric`/`decimal` columns, so measurement values arrived in Excel as text cells — invisible in a screenshot, useless in a formula, a sort, or a chart. openpyxl lists `Decimal` in its own `NUMERIC_TYPES` and stores it as a real numeric cell, so the conversion was never needed. Found by exporting from a live PostgreSQL container and checking `cell.data_type` rather than `cell.value`. |
|
|
435
437
|
| `EMAIL` SMTP login without a password | Upstream calls `smtplib.login()` with only a username when `[email] password` and `enc_password` are both unset, which raises `TypeError` from the stdlib — `login()` has no single-argument form. execsql2 reports a configuration error naming the missing password instead. |
|
|
438
|
+
| Type scan of large integers | Working out a column's type tested each integer against the binary type by building `bytearray(n)`, n zero bytes, so a column of large integers (epoch milliseconds: about a terabyte per value) hung or ran out of memory. The binary test now answers for an integer without building it; the inferred types are unchanged. |
|
|
436
439
|
|
|
437
440
|
______________________________________________________________________
|
|
438
441
|
|
|
@@ -78,11 +78,12 @@ for t in threads:
|
|
|
78
78
|
|
|
79
79
|
Each run has its own [file writer](#file-output), so two runs writing files at the same time both get complete files, and a run that halts on an error stops only its own output. Plugin metacommands are registered once per process, not once per call, and a `--manifest` recorder belongs to the run that started it.
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Four things are still shared by every thread in the process, so keep them apart yourself:
|
|
82
82
|
|
|
83
83
|
- **The working directory.** `CD` calls `os.chdir()`, which moves every thread while the script runs. Use absolute paths in scripts that run concurrently, or don't use `CD` in them. When a script that ran `CD` finishes, `run()` changes back to the directory it was in before that `CD`, so the caller ends where it started; a run without `CD` leaves the directory as it finds it.
|
|
84
84
|
- **The same output file.** Two runs writing one file at the same time can interleave their lines. Give each run its own output paths.
|
|
85
85
|
- **Interactive prompts.** `PROMPT` and the other GUI metacommands are not meant to be driven from several threads at once.
|
|
86
|
+
- **pyodbc's connection pooling.** Before its first SQL Server, Access or ODBC DSN connection, execsql turns pooling off (`pyodbc.pooling = False`), so every `CONNECT` and every `run()` gets a new database session: a temp table or `SET` option from one run is gone in the next. pyodbc reads the setting at the process's first pyodbc connection, so your own pyodbc connections are not pooled either, and if your program connects with pyodbc before execsql does, pooling stays on and runs against the same server can share a session.
|
|
86
87
|
|
|
87
88
|
### File Output { #file-output }
|
|
88
89
|
|
|
@@ -148,6 +148,7 @@ These are the instance attributes and methods you must configure correctly:
|
|
|
148
148
|
| `schema_exists()` | method | **Must override.** Return `False` if schemas are not supported. |
|
|
149
149
|
| `drop_table()` | method | **Must override.** Drop a table (used by IMPORT when creating fresh). |
|
|
150
150
|
| `populate_table()` | method | **Must override.** Bulk-load rows from a generator (used by IMPORT). |
|
|
151
|
+
| `row_inserter()` | method | Override for a faster bulk path that stores exactly what `executemany()` stores. |
|
|
151
152
|
| `exec_cmd()` | method | Override if the DBMS can execute stored procedures or views as commands. |
|
|
152
153
|
|
|
153
154
|
Methods inherited from `Database` that you get for free include `execute()`, `cursor()`, `close()`, `commit()`, `rollback()`, `select_rowsource()`, `select_data()`, `schema_qualified_table_name()`, `quote_identifier()`, and `paramsubs()`.
|
|
@@ -297,7 +297,7 @@ ______________________________________________________________________
|
|
|
297
297
|
1. An `IMPORT` metacommand is dispatched to the appropriate handler.
|
|
298
298
|
1. The handler instantiates the correct importer (e.g., `importers/csv.py`, `importers/ods.py`) based on the file type.
|
|
299
299
|
1. The importer reads the file and yields rows.
|
|
300
|
-
1. `Database.populate_table()`
|
|
300
|
+
1. `Database.populate_table()` converts the rows and inserts them in batches of `import_row_buffer` rows, in one transaction, with optional progress bars. Each batch goes through the adapter's `row_inserter()`: `executemany()` by default, and multi-row `INSERT ... VALUES` on DuckDB, whose `executemany()` binds one row at a time.
|
|
301
301
|
|
|
302
302
|
### Export metadata
|
|
303
303
|
|
|
@@ -272,7 +272,7 @@ select id,name,created_at from users where active = true order by name;
|
|
|
272
272
|
```yaml
|
|
273
273
|
repos:
|
|
274
274
|
- repo: https://github.com/geocoug/execsql
|
|
275
|
-
rev: v2.
|
|
275
|
+
rev: v2.32.0
|
|
276
276
|
hooks:
|
|
277
277
|
- id: execsql-format
|
|
278
278
|
- 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.32.0
|
|
162
162
|
hooks:
|
|
163
163
|
- id: execsql-lint
|
|
164
164
|
args: [--ignore, V002] # optional: any execsql lint option
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "execsql-syntax",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "execsql-syntax",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.32.0",
|
|
10
10
|
"license": "GPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"vscode-languageclient": "10.1.2"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "execsql-syntax",
|
|
3
3
|
"displayName": "execsql",
|
|
4
4
|
"description": "Syntax highlighting, lint findings, completion, hover, navigation, quick fixes and formatting for execsql scripts.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.32.0",
|
|
6
6
|
"publisher": "geocoug",
|
|
7
7
|
"license": "GPL-3.0-or-later",
|
|
8
8
|
"icon": "icon.png",
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.32.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" }
|
|
@@ -188,7 +188,7 @@ line-ending = "auto"
|
|
|
188
188
|
exclude = ["*.md"]
|
|
189
189
|
|
|
190
190
|
[tool.bumpversion]
|
|
191
|
-
current_version = "2.
|
|
191
|
+
current_version = "2.32.0"
|
|
192
192
|
commit = true
|
|
193
193
|
tag = true
|
|
194
194
|
tag_name = "v{new_version}"
|
|
@@ -15,7 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any, cast
|
|
16
16
|
|
|
17
17
|
from execsql.db.base import Database, _fetched
|
|
18
|
-
from execsql.db.odbc import odbc_value
|
|
18
|
+
from execsql.db.odbc import fresh_sessions, odbc_value
|
|
19
19
|
from execsql.db.tiers import SupportTier
|
|
20
20
|
from execsql.exceptions import ErrInfo
|
|
21
21
|
from execsql.utils.errors import exception_desc, fatal_error, install_hint
|
|
@@ -108,6 +108,8 @@ class AccessDatabase(Database):
|
|
|
108
108
|
# Open an ODBC connection.
|
|
109
109
|
import pyodbc
|
|
110
110
|
|
|
111
|
+
fresh_sessions(pyodbc)
|
|
112
|
+
|
|
111
113
|
if self.conn is not None:
|
|
112
114
|
self.conn.close()
|
|
113
115
|
self.conn = None
|
|
@@ -665,6 +665,23 @@ class Database(ABC):
|
|
|
665
665
|
self.execute(f"drop table if exists {tablename} cascade;")
|
|
666
666
|
self.commit()
|
|
667
667
|
|
|
668
|
+
def row_inserter(self, curs: Any, table: str, columns: list[str]) -> Callable[[list[list[Any]]], None]:
|
|
669
|
+
"""Return a function that inserts one batch of rows into *table* through *curs*.
|
|
670
|
+
|
|
671
|
+
*table* is schema-qualified and quoted; *columns* are the quoted
|
|
672
|
+
column names, in the order of each row's values. IMPORT and COPY call
|
|
673
|
+
the function once per batch, all inside one transaction. This one
|
|
674
|
+
runs ``executemany``. An adapter may override it with a faster bulk
|
|
675
|
+
path only if that path stores exactly what ``executemany`` stores, for
|
|
676
|
+
every value type and column type: a faster load must not change data.
|
|
677
|
+
"""
|
|
678
|
+
sql = f"insert into {table} ({','.join(columns)}) values ({self.paramsubs(len(columns))});"
|
|
679
|
+
|
|
680
|
+
def insert(rows: list[list[Any]]) -> None:
|
|
681
|
+
curs.executemany(sql, rows)
|
|
682
|
+
|
|
683
|
+
return insert
|
|
684
|
+
|
|
668
685
|
def populate_table(
|
|
669
686
|
self,
|
|
670
687
|
schema_name: str | None,
|
|
@@ -740,6 +757,7 @@ class Database(ABC):
|
|
|
740
757
|
|
|
741
758
|
def _import_loop(curs) -> int:
|
|
742
759
|
nonlocal eof, total_rows, task_id
|
|
760
|
+
insert = self.row_inserter(curs, sq_name, columns)
|
|
743
761
|
while True:
|
|
744
762
|
b = []
|
|
745
763
|
for _j in range(_state.conf.import_row_buffer):
|
|
@@ -809,7 +827,7 @@ class Database(ABC):
|
|
|
809
827
|
b.append(row)
|
|
810
828
|
if len(b) > 0:
|
|
811
829
|
try:
|
|
812
|
-
|
|
830
|
+
insert(b)
|
|
813
831
|
except ErrInfo:
|
|
814
832
|
raise
|
|
815
833
|
except Exception as e:
|
|
@@ -10,6 +10,7 @@ the CLI.
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
from execsql.db.base import Database
|
|
13
|
+
from execsql.db.odbc import fresh_sessions
|
|
13
14
|
from execsql.db.tiers import SupportTier
|
|
14
15
|
from execsql.exceptions import ErrInfo
|
|
15
16
|
from execsql.utils.errors import exception_desc, fatal_error, install_hint
|
|
@@ -75,6 +76,8 @@ class DsnDatabase(Database):
|
|
|
75
76
|
# Open an ODBC connection using a DSN.
|
|
76
77
|
import pyodbc
|
|
77
78
|
|
|
79
|
+
fresh_sessions(pyodbc)
|
|
80
|
+
|
|
78
81
|
if self.conn is not None:
|
|
79
82
|
self.conn.close()
|
|
80
83
|
self.conn = None
|
|
@@ -8,8 +8,12 @@ analytics databases via the ``duckdb`` package. Corresponds to ``-t k``
|
|
|
8
8
|
on the CLI.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import importlib.util
|
|
11
12
|
import re
|
|
13
|
+
import sys
|
|
12
14
|
from collections import deque
|
|
15
|
+
from collections.abc import Callable, Iterator
|
|
16
|
+
from contextlib import contextmanager
|
|
13
17
|
from pathlib import Path
|
|
14
18
|
from typing import Any
|
|
15
19
|
|
|
@@ -18,7 +22,7 @@ from execsql.exceptions import ErrInfo
|
|
|
18
22
|
from execsql.utils.errors import exception_desc, fatal_error, install_hint
|
|
19
23
|
import execsql.state as _state
|
|
20
24
|
|
|
21
|
-
__all__ = ["DuckDBDatabase"]
|
|
25
|
+
__all__ = ["DuckDBDatabase", "insert_rows"]
|
|
22
26
|
|
|
23
27
|
# Statements that return rows; any other statement DuckDB answers with a
|
|
24
28
|
# one-column "Count" result (rows inserted, updated or deleted).
|
|
@@ -28,6 +32,53 @@ _QUERY_RX = re.compile(r"(?:SELECT|WITH|VALUES|FROM|TABLE|SHOW|DESCRIBE|SUMMARIZ
|
|
|
28
32
|
_TRANSACTION_RX = re.compile(r"(?P<begin>BEGIN|START\s+TRANSACTION)\b|(?P<end>COMMIT|END|ROLLBACK|ABORT)\b", re.I)
|
|
29
33
|
|
|
30
34
|
|
|
35
|
+
# Parameters bound by one INSERT ... VALUES statement in insert_rows(); a
|
|
36
|
+
# wide table gets fewer rows per statement.
|
|
37
|
+
_PARAMS_PER_STATEMENT = 10_000
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@contextmanager
|
|
41
|
+
def _pandas_lookup_skipped() -> Iterator[None]:
|
|
42
|
+
"""Make DuckDB's check for pandas fail at once while it binds rows.
|
|
43
|
+
|
|
44
|
+
The duckdb package asks Python's import system for ``pandas`` twice for
|
|
45
|
+
every row it binds, and when pandas is not installed nothing remembers
|
|
46
|
+
the answer, so each row searches ``sys.path`` again: about three
|
|
47
|
+
quarters of an INSERT's time. A ``None`` in ``sys.modules`` makes that
|
|
48
|
+
import fail without the search. It is set only when pandas is not
|
|
49
|
+
installed, so no import that could have succeeded fails, and it is
|
|
50
|
+
removed afterwards.
|
|
51
|
+
"""
|
|
52
|
+
if "pandas" in sys.modules or importlib.util.find_spec("pandas") is not None:
|
|
53
|
+
yield
|
|
54
|
+
return
|
|
55
|
+
sys.modules["pandas"] = None # type: ignore[assignment]
|
|
56
|
+
try:
|
|
57
|
+
yield
|
|
58
|
+
finally:
|
|
59
|
+
if "pandas" in sys.modules and sys.modules["pandas"] is None:
|
|
60
|
+
del sys.modules["pandas"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def insert_rows(curs: Any, table: str, columns: list[str], rows: list[Any]) -> None:
|
|
64
|
+
"""Insert *rows* into *table* with multi-row ``INSERT ... VALUES`` statements.
|
|
65
|
+
|
|
66
|
+
*table* is quoted (and schema-qualified as needed); *columns* are the
|
|
67
|
+
quoted column names, in the order of each row's values. DuckDB's
|
|
68
|
+
``executemany`` runs one row at a time; binding many rows in one
|
|
69
|
+
statement is several times faster and stores the same values.
|
|
70
|
+
"""
|
|
71
|
+
if not rows:
|
|
72
|
+
return
|
|
73
|
+
per_statement = max(1, _PARAMS_PER_STATEMENT // max(1, len(columns)))
|
|
74
|
+
head = f"insert into {table} ({','.join(columns)}) values "
|
|
75
|
+
row_marks = "(" + ",".join("?" * len(columns)) + ")"
|
|
76
|
+
with _pandas_lookup_skipped():
|
|
77
|
+
for start in range(0, len(rows), per_statement):
|
|
78
|
+
chunk = rows[start : start + per_statement]
|
|
79
|
+
curs.execute(head + ",".join([row_marks] * len(chunk)), [value for row in chunk for value in row])
|
|
80
|
+
|
|
81
|
+
|
|
31
82
|
class _SharedCursor:
|
|
32
83
|
"""A cursor that runs on the database's one connection.
|
|
33
84
|
|
|
@@ -261,6 +312,14 @@ class DuckDBDatabase(Database):
|
|
|
261
312
|
curs.execute(sql, params)
|
|
262
313
|
return len(curs.fetchall()) > 0
|
|
263
314
|
|
|
315
|
+
def row_inserter(self, curs: Any, table: str, columns: list[str]) -> Callable[[list[list[Any]]], None]:
|
|
316
|
+
"""Insert each batch with multi-row ``INSERT ... VALUES`` (:func:`insert_rows`)."""
|
|
317
|
+
|
|
318
|
+
def insert(rows: list[list[Any]]) -> None:
|
|
319
|
+
insert_rows(curs, table, columns, rows)
|
|
320
|
+
|
|
321
|
+
return insert
|
|
322
|
+
|
|
264
323
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
265
324
|
"""Return True if the named view exists in the DuckDB database."""
|
|
266
325
|
# DuckDB information_schema has no 'views' table; views are listed in 'tables'
|
|
@@ -7,15 +7,32 @@ taken from a substitution variable) would end its attribute early and
|
|
|
7
7
|
start another. :func:`odbc_value` brace-quotes such values;
|
|
8
8
|
:func:`connection_string` joins the attributes, and :func:`loggable`
|
|
9
9
|
gives the same string with the password masked for the log.
|
|
10
|
+
:func:`fresh_sessions` makes each pyodbc connection a new database session.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
__all__ = ["connection_string", "fresh_sessions", "loggable", "odbc_value"]
|
|
15
18
|
|
|
16
19
|
_SECRET_KEYS = frozenset({"pwd", "password"})
|
|
17
20
|
|
|
18
21
|
|
|
22
|
+
def fresh_sessions(pyodbc: Any) -> None:
|
|
23
|
+
"""Turn off pyodbc's connection pooling, so each connection is a new session.
|
|
24
|
+
|
|
25
|
+
With pooling on, closing a connection hands its session back to the ODBC
|
|
26
|
+
driver manager, and unixODBC gives it unchanged to the next connection to
|
|
27
|
+
the same server: temp tables and ``SET`` options from one ``CONNECT`` or
|
|
28
|
+
``execsql.run()`` were still there in the next. pyodbc applies the
|
|
29
|
+
setting when the process makes its first pyodbc connection, so it covers
|
|
30
|
+
every pyodbc connection in the process, and has no effect if one was
|
|
31
|
+
made before execsql's first.
|
|
32
|
+
"""
|
|
33
|
+
pyodbc.pooling = False
|
|
34
|
+
|
|
35
|
+
|
|
19
36
|
def odbc_value(value: object) -> str:
|
|
20
37
|
"""Return *value* as an ODBC attribute value, brace-quoted when it needs it.
|
|
21
38
|
|
|
@@ -8,7 +8,7 @@ Server via ``pyodbc``. Corresponds to ``-t s`` on the CLI.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from execsql.db.base import Database
|
|
11
|
-
from execsql.db.odbc import connection_string, loggable
|
|
11
|
+
from execsql.db.odbc import connection_string, fresh_sessions, loggable
|
|
12
12
|
from execsql.exceptions import ErrInfo
|
|
13
13
|
from execsql.utils.errors import fatal_error, install_hint
|
|
14
14
|
from execsql.utils.auth import (
|
|
@@ -98,6 +98,8 @@ class SqlServerDatabase(Database):
|
|
|
98
98
|
"""Open a connection to the SQL Server database via pyodbc."""
|
|
99
99
|
import pyodbc
|
|
100
100
|
|
|
101
|
+
fresh_sessions(pyodbc)
|
|
102
|
+
|
|
101
103
|
if self.conn is None:
|
|
102
104
|
begin_login()
|
|
103
105
|
if self.user and self.need_passwd and not self.password:
|
|
@@ -10,7 +10,6 @@ Requires the ``execsql2[duckdb]`` extra.
|
|
|
10
10
|
|
|
11
11
|
import datetime as _datetime
|
|
12
12
|
import decimal as _decimal
|
|
13
|
-
import math
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
from typing import Any
|
|
16
15
|
|
|
@@ -38,9 +37,9 @@ def export_duckdb(
|
|
|
38
37
|
fatal_error(f"The duckdb module is required to export data in that format. {install_hint('duckdb')}")
|
|
39
38
|
return
|
|
40
39
|
|
|
40
|
+
from execsql.db.duckdb import insert_rows
|
|
41
41
|
from execsql.models import DataTable
|
|
42
42
|
|
|
43
|
-
chunksize = 10000
|
|
44
43
|
# B07a/F026: tablename and catalog are substituted from user input.
|
|
45
44
|
# Identifier-quote the table-name interpolation site and use
|
|
46
45
|
# placeholders for the information_schema literal-value query.
|
|
@@ -70,15 +69,8 @@ def export_duckdb(
|
|
|
70
69
|
curs.execute(sql)
|
|
71
70
|
# Export all rows of data
|
|
72
71
|
columns = [dbt_duckdb.quoted(col) for col in hdrs]
|
|
73
|
-
colspec = ",".join(columns)
|
|
74
|
-
paramspec = ",".join(("?",) * len(columns))
|
|
75
|
-
sql = f"insert into {qtable} ({colspec}) values ({paramspec});"
|
|
76
|
-
n_chunks = math.ceil(len(rowdata) / chunksize)
|
|
77
72
|
curs.execute("BEGIN TRANSACTION;")
|
|
78
|
-
|
|
79
|
-
start = i * chunksize
|
|
80
|
-
end = start + chunksize
|
|
81
|
-
curs.executemany(sql, rowdata[start:end])
|
|
73
|
+
insert_rows(curs, qtable, columns, rowdata)
|
|
82
74
|
curs.execute("COMMIT;")
|
|
83
75
|
curs.close()
|
|
84
76
|
ddb.close()
|
|
@@ -50,6 +50,16 @@ __all__ = [
|
|
|
50
50
|
ColumnType = tuple[str, type[DataType], int | None, bool, int | None, int | None]
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def _text_length(value: Any) -> int:
|
|
54
|
+
"""Return the length of *value* written as text (a column may turn out to be text)."""
|
|
55
|
+
if isinstance(value, str):
|
|
56
|
+
return len(value)
|
|
57
|
+
try:
|
|
58
|
+
return len(str(value))
|
|
59
|
+
except Exception:
|
|
60
|
+
return len(bytes(value))
|
|
61
|
+
|
|
62
|
+
|
|
53
63
|
class Column:
|
|
54
64
|
"""Compile data-type match statistics for a single column of imported data."""
|
|
55
65
|
|
|
@@ -84,40 +94,39 @@ class Column:
|
|
|
84
94
|
"""Test whether a non-null value matches this data type and update statistics."""
|
|
85
95
|
# datavalue must be non-null
|
|
86
96
|
if not self.failed:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
self.count += 1
|
|
90
|
-
if isinstance(datavalue, str):
|
|
91
|
-
vlen = len(datavalue)
|
|
92
|
-
else:
|
|
93
|
-
# This column may turn out to have to be text, so we need the maximum length
|
|
94
|
-
# of any data value when represented as text.
|
|
95
|
-
try:
|
|
96
|
-
vlen = len(str(datavalue))
|
|
97
|
-
except Exception:
|
|
98
|
-
vlen = len(bytes(datavalue))
|
|
99
|
-
if self.maxlen > 0 and vlen != self.maxlen:
|
|
100
|
-
self.varlen = True
|
|
101
|
-
if vlen > self.maxlen:
|
|
102
|
-
self.maxlen = vlen
|
|
103
|
-
if self.dt.precision is not None and self.dt.scale is not None:
|
|
104
|
-
int_digits = self.dt.precision - self.dt.scale
|
|
105
|
-
if self.maxprecision is None:
|
|
106
|
-
self.maxprecision = self.dt.precision
|
|
107
|
-
self.scale = self.dt.scale
|
|
108
|
-
self.maxscale = self.dt.scale
|
|
109
|
-
self.max_int_digits = int_digits
|
|
110
|
-
else:
|
|
111
|
-
assert self.maxscale is not None
|
|
112
|
-
assert self.max_int_digits is not None
|
|
113
|
-
self.maxprecision = max(self.dt.precision, self.maxprecision)
|
|
114
|
-
if self.dt.scale != self.scale:
|
|
115
|
-
self.varscale = True
|
|
116
|
-
self.maxscale = max(self.dt.scale, self.maxscale)
|
|
117
|
-
self.max_int_digits = max(int_digits, self.max_int_digits)
|
|
97
|
+
if self.dt.matches(datavalue):
|
|
98
|
+
self.record(_text_length(datavalue))
|
|
118
99
|
else:
|
|
119
100
|
self.failed = True
|
|
120
101
|
|
|
102
|
+
def record(self, vlen: int) -> None:
|
|
103
|
+
"""Count a value this data type matched, *vlen* characters long as text.
|
|
104
|
+
|
|
105
|
+
Call it right after :meth:`DataType.matches` accepted the value:
|
|
106
|
+
a decimal type's precision and scale are those of the value it
|
|
107
|
+
last matched.
|
|
108
|
+
"""
|
|
109
|
+
self.count += 1
|
|
110
|
+
if self.maxlen > 0 and vlen != self.maxlen:
|
|
111
|
+
self.varlen = True
|
|
112
|
+
if vlen > self.maxlen:
|
|
113
|
+
self.maxlen = vlen
|
|
114
|
+
if self.dt.precision is not None and self.dt.scale is not None:
|
|
115
|
+
int_digits = self.dt.precision - self.dt.scale
|
|
116
|
+
if self.maxprecision is None:
|
|
117
|
+
self.maxprecision = self.dt.precision
|
|
118
|
+
self.scale = self.dt.scale
|
|
119
|
+
self.maxscale = self.dt.scale
|
|
120
|
+
self.max_int_digits = int_digits
|
|
121
|
+
else:
|
|
122
|
+
assert self.maxscale is not None
|
|
123
|
+
assert self.max_int_digits is not None
|
|
124
|
+
self.maxprecision = max(self.dt.precision, self.maxprecision)
|
|
125
|
+
if self.dt.scale != self.scale:
|
|
126
|
+
self.varscale = True
|
|
127
|
+
self.maxscale = max(self.dt.scale, self.maxscale)
|
|
128
|
+
self.max_int_digits = max(int_digits, self.max_int_digits)
|
|
129
|
+
|
|
121
130
|
def __init__(self, colname: str, infer_strings: bool = True) -> None:
|
|
122
131
|
"""Create a column characteriser for the named column.
|
|
123
132
|
|
|
@@ -177,6 +186,15 @@ class Column:
|
|
|
177
186
|
self.str_accums = tuple(ac for ac in self.accums if isinstance(ac.dt, (DT_Character, DT_Varchar, DT_Text)))
|
|
178
187
|
# A bytes value is checked as binary or text only: int(b"12") is 12.
|
|
179
188
|
self.bytes_accums = tuple(ac for ac in self.accums if isinstance(ac.dt, DT_Binary)) + self.str_accums
|
|
189
|
+
# A value is tried only against the types that matched every value
|
|
190
|
+
# before it: a type that failed once cannot be the column's type.
|
|
191
|
+
self._live = list(self.accums)
|
|
192
|
+
self._live_str = list(self.str_accums)
|
|
193
|
+
self._live_bytes = list(self.bytes_accums)
|
|
194
|
+
# The settings the scan reads for every value, read once.
|
|
195
|
+
self._trim_strings = conf.trim_strings
|
|
196
|
+
self._replace_newlines = conf.replace_newlines
|
|
197
|
+
self._empty_strings = conf.empty_strings
|
|
180
198
|
self.dt_eval = False
|
|
181
199
|
# self.dt is a tuple of: 0: column name; 1: data type class; 2: max length or None;
|
|
182
200
|
# 3: bool indicating any null values; 4: precision or None; 5: scale or None.
|
|
@@ -187,31 +205,38 @@ class Column:
|
|
|
187
205
|
|
|
188
206
|
def eval_types(self, column_value: Any) -> None:
|
|
189
207
|
"""Evaluate which data types the value matches and update counters."""
|
|
190
|
-
# Evaluate which data type(s) the value matches, and increment the appropriate counter(s).
|
|
191
|
-
import execsql.state as _state
|
|
192
|
-
|
|
193
208
|
self.dt_eval = False
|
|
194
209
|
self.rowcount += 1
|
|
195
|
-
|
|
196
|
-
if
|
|
197
|
-
if
|
|
210
|
+
is_str = isinstance(column_value, str)
|
|
211
|
+
if is_str:
|
|
212
|
+
if self._trim_strings:
|
|
198
213
|
column_value = column_value.strip()
|
|
199
|
-
if
|
|
214
|
+
if self._replace_newlines:
|
|
200
215
|
column_value = re.sub(r"[\s\t]*[\r\n]+[\s\t]*", " ", column_value)
|
|
201
|
-
if column_value is None or (
|
|
202
|
-
not conf.empty_strings and isinstance(column_value, str) and len(column_value.strip()) == 0
|
|
203
|
-
):
|
|
216
|
+
if column_value is None or (not self._empty_strings and is_str and len(column_value.strip()) == 0):
|
|
204
217
|
self.nullrows += 1
|
|
205
218
|
return
|
|
206
|
-
if not self.infer_strings and
|
|
207
|
-
|
|
208
|
-
dt.check(column_value)
|
|
219
|
+
if not self.infer_strings and is_str:
|
|
220
|
+
live = self._live_str
|
|
209
221
|
elif not self.infer_strings and isinstance(column_value, (bytes, bytearray, memoryview)):
|
|
210
|
-
|
|
211
|
-
dt.check(column_value)
|
|
222
|
+
live = self._live_bytes
|
|
212
223
|
else:
|
|
213
|
-
|
|
214
|
-
|
|
224
|
+
live = self._live
|
|
225
|
+
vlen = None
|
|
226
|
+
any_failed = False
|
|
227
|
+
# The value is not NULL, the one case DataType.matches() adds to _is_match().
|
|
228
|
+
for ac in live:
|
|
229
|
+
if ac.dt._is_match(column_value):
|
|
230
|
+
if vlen is None:
|
|
231
|
+
vlen = _text_length(column_value)
|
|
232
|
+
ac.record(vlen)
|
|
233
|
+
else:
|
|
234
|
+
ac.failed = True
|
|
235
|
+
any_failed = True
|
|
236
|
+
if any_failed:
|
|
237
|
+
self._live = [ac for ac in self._live if not ac.failed]
|
|
238
|
+
self._live_str = [ac for ac in self._live_str if not ac.failed]
|
|
239
|
+
self._live_bytes = [ac for ac in self._live_bytes if not ac.failed]
|
|
215
240
|
|
|
216
241
|
def column_type(self) -> ColumnType:
|
|
217
242
|
"""Return the inferred type of this column as a 6-tuple."""
|
|
@@ -550,6 +550,8 @@ class DT_Character(DataType):
|
|
|
550
550
|
return "DT_Character()"
|
|
551
551
|
|
|
552
552
|
def _is_match(self, data: object) -> bool:
|
|
553
|
+
if isinstance(data, str): # what _from_data() returns a str as, unchanged
|
|
554
|
+
return True
|
|
553
555
|
if isinstance(data, bytearray):
|
|
554
556
|
return False
|
|
555
557
|
return super()._is_match(data)
|
|
@@ -582,6 +584,8 @@ class DT_Varchar(DataType):
|
|
|
582
584
|
return "DT_Varchar()"
|
|
583
585
|
|
|
584
586
|
def _is_match(self, data: object) -> bool:
|
|
587
|
+
if isinstance(data, str): # the length check _from_data() makes
|
|
588
|
+
return len(data) <= 255
|
|
585
589
|
if isinstance(data, bytearray):
|
|
586
590
|
return False
|
|
587
591
|
return super()._is_match(data)
|
|
@@ -609,6 +613,8 @@ class DT_Text(DataType):
|
|
|
609
613
|
return "DT_Text()"
|
|
610
614
|
|
|
611
615
|
def _is_match(self, data: object) -> bool:
|
|
616
|
+
if isinstance(data, str): # what _from_data() returns a str as, unchanged
|
|
617
|
+
return True
|
|
612
618
|
if isinstance(data, bytearray):
|
|
613
619
|
return False
|
|
614
620
|
return super()._is_match(data)
|
|
@@ -634,6 +640,14 @@ class DT_Binary(DataType):
|
|
|
634
640
|
def __repr__(self) -> str:
|
|
635
641
|
return "DT_Binary()"
|
|
636
642
|
|
|
643
|
+
def _is_match(self, data: object) -> bool:
|
|
644
|
+
# bytearray(n) is n zero bytes, so an int converts when it is not
|
|
645
|
+
# negative. Building it to find out allocated n bytes per value: a
|
|
646
|
+
# column of epoch milliseconds asked for a terabyte each.
|
|
647
|
+
if isinstance(data, int):
|
|
648
|
+
return data >= 0
|
|
649
|
+
return super()._is_match(data)
|
|
650
|
+
|
|
637
651
|
|
|
638
652
|
class DbType:
|
|
639
653
|
"""Map Python DataType subclasses to DBMS-specific SQL type names."""
|