execsql2 2.15.2__tar.gz → 2.15.5__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.15.2 → execsql2-2.15.5}/CHANGELOG.md +24 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/PKG-INFO +11 -2
- {execsql2-2.15.2 → execsql2-2.15.5}/README.md +3 -1
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/about/divergence.md +1 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/reference/security.md +2 -2
- {execsql2-2.15.2 → execsql2-2.15.5}/pyproject.toml +6 -2
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/datetime.py +12 -0
- execsql2-2.15.5/tests/importers/test_csv_edge_cases.py +209 -0
- execsql2-2.15.5/tests/test_debug_repl.py +555 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_format.py +119 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_parser.py +79 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_datetime.py +15 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/uv.lock +64 -2
- execsql2-2.15.2/CLAUDE.md +0 -56
- {execsql2-2.15.2 → execsql2-2.15.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/.gitignore +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/.pre-commit-config.yaml +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/.python-version +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/.readthedocs.yaml +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/CONTRIBUTING.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/LICENSE.txt +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/NOTICE +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/SECURITY.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/about/contributors.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/about/copyright.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/cli.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/db.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/exporters.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/importers.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/index.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/metacommands.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/architecture.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/getting-started/installation.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/debugging.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/documentation.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/encoding.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/examples.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/formatter.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/logging.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/usage.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/actions.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/actions2.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/checkboxes.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/connect.b64 +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/connect.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/create_conf.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/entry_form.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/execsql_console.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/fatals.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/logo_small.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/unmatched.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/index.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/reference/configuration.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/reference/metacommands.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/justfile +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/__main__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/help.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/run.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/config.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/access.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/base.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/factory.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exceptions.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/format.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/base.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/console.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/base.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/json.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/control.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/upsert.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/models.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/parser.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/py.typed +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/script/control.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/script/engine.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/script/variables.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/state.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/types.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/README.md +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/config_settings.sqlite +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/execsql.conf +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/make_config_db.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/md_compare.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/md_glossary.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/md_upsert.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/pg_compare.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/pg_glossary.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/pg_upsert.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/script_template.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/ss_compare.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/ss_glossary.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/templates/ss_upsert.sql +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_cli.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_lint.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_ping.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_profile.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/conftest.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_base.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_factory.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_postgres.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_base.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_db.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_html_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_json.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_json_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_latex_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_pretty_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_raw_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_templates_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_values_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/gui/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/gui/test_backends.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/gui/test_compare_stats.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/gui/test_compute_row_diffs.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_base_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_json_importer.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/conftest.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_pg_upsert.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_config.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_config_data.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_config_extended.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_engine.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_error_messages.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_exceptions.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_mail.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_models.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_package.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_registry.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_script.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_state.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_types.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/__init__.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_auth.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_errors.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_regex.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_strings.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_timer.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.15.2 → execsql2-2.15.5}/zensical.toml +0 -0
|
@@ -13,6 +13,30 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.15.5] - 2026-04-15
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `DT_Timestamp` type inference no longer claims time-only values (e.g. `13:15:45`). `dateutil.parser.parse()` silently fills in today's date for bare time strings, causing `DT_Timestamp` to match before `DT_Time` and generating PostgreSQL `InvalidDatetimeFormat` errors on CSV import. Time-only strings are now rejected by `parse_datetime()`.
|
|
21
|
+
|
|
22
|
+
______________________________________________________________________
|
|
23
|
+
|
|
24
|
+
## [2.15.4] - 2026-04-15
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed typo in `test_latin1_encoding` test data (`calf\xe9` → `calf\xe9`) that caused assertion failure on Windows CI.
|
|
29
|
+
|
|
30
|
+
______________________________________________________________________
|
|
31
|
+
|
|
32
|
+
## [2.15.3] - 2026-04-15
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- New optional dependency extras `auth-plaintext` and `auth-encrypted` for headless Linux keyring backends. `pip install execsql2[auth-plaintext]` installs `keyring` + `keyrings.alt`; `pip install execsql2[auth-encrypted]` adds `pycryptodome` for the encrypted file backend.
|
|
37
|
+
|
|
38
|
+
______________________________________________________________________
|
|
39
|
+
|
|
16
40
|
## [2.15.2] - 2026-04-14
|
|
17
41
|
|
|
18
42
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.15.
|
|
3
|
+
Version: 2.15.5
|
|
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
|
|
@@ -69,6 +69,13 @@ Requires-Dist: pymysql; extra == 'all-db'
|
|
|
69
69
|
Requires-Dist: pyodbc; extra == 'all-db'
|
|
70
70
|
Provides-Extra: auth
|
|
71
71
|
Requires-Dist: keyring; extra == 'auth'
|
|
72
|
+
Provides-Extra: auth-encrypted
|
|
73
|
+
Requires-Dist: keyring; extra == 'auth-encrypted'
|
|
74
|
+
Requires-Dist: keyrings-alt; extra == 'auth-encrypted'
|
|
75
|
+
Requires-Dist: pycryptodome; extra == 'auth-encrypted'
|
|
76
|
+
Provides-Extra: auth-plaintext
|
|
77
|
+
Requires-Dist: keyring; extra == 'auth-plaintext'
|
|
78
|
+
Requires-Dist: keyrings-alt; extra == 'auth-plaintext'
|
|
72
79
|
Provides-Extra: dev
|
|
73
80
|
Requires-Dist: build>=1.2.2.post1; extra == 'dev'
|
|
74
81
|
Requires-Dist: bump-my-version>=1.2.7; extra == 'dev'
|
|
@@ -167,7 +174,9 @@ pip install execsql2[odbc] # ODBC DSN (pyodbc)
|
|
|
167
174
|
|
|
168
175
|
# Feature bundles
|
|
169
176
|
pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
|
|
170
|
-
pip install execsql2[auth]
|
|
177
|
+
pip install execsql2[auth] # OS keyring integration
|
|
178
|
+
pip install execsql2[auth-plaintext] # Keyring + plaintext file backend (headless Linux)
|
|
179
|
+
pip install execsql2[auth-encrypted] # Keyring + encrypted file backend (headless Linux)
|
|
171
180
|
|
|
172
181
|
# Convenience
|
|
173
182
|
pip install execsql2[all-db] # All database drivers
|
|
@@ -52,7 +52,9 @@ pip install execsql2[odbc] # ODBC DSN (pyodbc)
|
|
|
52
52
|
|
|
53
53
|
# Feature bundles
|
|
54
54
|
pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
|
|
55
|
-
pip install execsql2[auth]
|
|
55
|
+
pip install execsql2[auth] # OS keyring integration
|
|
56
|
+
pip install execsql2[auth-plaintext] # Keyring + plaintext file backend (headless Linux)
|
|
57
|
+
pip install execsql2[auth-encrypted] # Keyring + encrypted file backend (headless Linux)
|
|
56
58
|
|
|
57
59
|
# Convenience
|
|
58
60
|
pip install execsql2[all-db] # All database drivers
|
|
@@ -253,6 +253,7 @@ These are behavioral changes driven by security or correctness issues in the ups
|
|
|
253
253
|
| `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. Inherited from upstream. |
|
|
254
254
|
| `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. Inherited from upstream. |
|
|
255
255
|
| `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. Inherited from upstream. |
|
|
256
|
+
| `DT_Timestamp` claims time-only values | `dateutil.parser.parse()` silently fills in today's date for bare time strings like `"13:15:45"`, so `DT_Timestamp` matched before `DT_Time` in the inference order. `parse_datetime()` now rejects time-only strings. Inherited from upstream. |
|
|
256
257
|
|
|
257
258
|
______________________________________________________________________
|
|
258
259
|
|
|
@@ -52,7 +52,7 @@ To disable keyring integration, set `use_keyring = No` in the `[connect]` sectio
|
|
|
52
52
|
Passwords are stored encrypted on disk. Requires a master password the first time keyring is used per session.
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
pip install
|
|
55
|
+
pip install execsql2[auth-encrypted]
|
|
56
56
|
mkdir -p ~/.config/python_keyring
|
|
57
57
|
cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
|
|
58
58
|
[backend]
|
|
@@ -67,7 +67,7 @@ The encrypted keyring file is stored at `~/.local/share/python_keyring/crypted_p
|
|
|
67
67
|
Passwords are stored in plain text on disk. No master password is needed — execsql will never prompt for a password after the first successful entry.
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
pip install
|
|
70
|
+
pip install execsql2[auth-plaintext]
|
|
71
71
|
mkdir -p ~/.config/python_keyring
|
|
72
72
|
cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
|
|
73
73
|
[backend]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.15.
|
|
7
|
+
version = "2.15.5"
|
|
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" }
|
|
@@ -59,6 +59,8 @@ odbc = ["pyodbc"]
|
|
|
59
59
|
# Feature bundles
|
|
60
60
|
formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables", "PyYAML"]
|
|
61
61
|
auth = ["keyring"]
|
|
62
|
+
auth-plaintext = ["keyring", "keyrings.alt"]
|
|
63
|
+
auth-encrypted = ["keyring", "keyrings.alt", "pycryptodome"]
|
|
62
64
|
upsert = ["pg-upsert>=1.21.0"]
|
|
63
65
|
# Convenience groups
|
|
64
66
|
all-db = [
|
|
@@ -162,7 +164,7 @@ skip-magic-trailing-comma = false
|
|
|
162
164
|
line-ending = "auto"
|
|
163
165
|
|
|
164
166
|
[tool.bumpversion]
|
|
165
|
-
current_version = "2.15.
|
|
167
|
+
current_version = "2.15.5"
|
|
166
168
|
commit = true
|
|
167
169
|
commit_args = "--no-verify"
|
|
168
170
|
tag = true
|
|
@@ -231,6 +233,8 @@ hel = "hel"
|
|
|
231
233
|
fo = "fo"
|
|
232
234
|
# odfpy package imports as 'odf', not 'of'
|
|
233
235
|
odf = "odf"
|
|
236
|
+
# Latin-1 test data: b"caf\xe9" decodes to "café"
|
|
237
|
+
caf = "caf"
|
|
234
238
|
|
|
235
239
|
[tool.tox]
|
|
236
240
|
required = ["tox-uv"]
|
|
@@ -25,12 +25,22 @@ __all__ = ["parse_datetime", "parse_datetimetz"]
|
|
|
25
25
|
# misidentified as timestamps.
|
|
26
26
|
_NUMERIC_ONLY = re.compile(r"^[+-]?\d+\.?\d*$")
|
|
27
27
|
|
|
28
|
+
# Match time-only strings like "13:15:45", "9:30", "1:15:45.123", "09:30 AM".
|
|
29
|
+
# dateutil parses these by filling in today's date, which causes DT_Timestamp
|
|
30
|
+
# to claim the column before DT_Time gets a chance.
|
|
31
|
+
_TIME_ONLY = re.compile(r"^\d{1,2}:\d{2}(?::\d{2}(?:\.\d+)?)?\s*(?:[AaPp][Mm])?$")
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
def _looks_numeric(s: str) -> bool:
|
|
30
35
|
"""Return True if *s* is a bare number that should not be parsed as a date."""
|
|
31
36
|
return bool(_NUMERIC_ONLY.match(s.strip()))
|
|
32
37
|
|
|
33
38
|
|
|
39
|
+
def _looks_time_only(s: str) -> bool:
|
|
40
|
+
"""Return True if *s* is a time-only string (no date component)."""
|
|
41
|
+
return bool(_TIME_ONLY.match(s.strip()))
|
|
42
|
+
|
|
43
|
+
|
|
34
44
|
def parse_datetime(datestr: Any) -> datetime.datetime | None:
|
|
35
45
|
"""Parse a date/time string into a :class:`datetime.datetime`.
|
|
36
46
|
|
|
@@ -53,6 +63,8 @@ def parse_datetime(datestr: Any) -> datetime.datetime | None:
|
|
|
53
63
|
return None
|
|
54
64
|
if _looks_numeric(datestr):
|
|
55
65
|
return None
|
|
66
|
+
if _looks_time_only(datestr):
|
|
67
|
+
return None
|
|
56
68
|
try:
|
|
57
69
|
return _dateutil_parser.parse(datestr)
|
|
58
70
|
except (ValueError, OverflowError, TypeError):
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Edge-case tests for CSV/delimited import.
|
|
3
|
+
|
|
4
|
+
Covers scenarios not exercised by test_csv_importer.py:
|
|
5
|
+
- Empty files
|
|
6
|
+
- Header-only files (no data rows)
|
|
7
|
+
- Files with BOM markers (UTF-8 BOM)
|
|
8
|
+
- Inconsistent column counts across rows
|
|
9
|
+
- Unicode/special characters in data
|
|
10
|
+
- Very long field values
|
|
11
|
+
- Files with only whitespace rows
|
|
12
|
+
- Encoding parameter override
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from execsql.db.sqlite import SQLiteDatabase
|
|
20
|
+
from execsql.exceptions import ErrInfo
|
|
21
|
+
from execsql.importers.csv import importtable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# Extra conf attributes required by the importers
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture(autouse=True)
|
|
30
|
+
def importer_conf(minimal_conf):
|
|
31
|
+
minimal_conf.del_empty_cols = False
|
|
32
|
+
minimal_conf.create_col_hdrs = False
|
|
33
|
+
minimal_conf.clean_col_hdrs = False
|
|
34
|
+
minimal_conf.trim_col_hdrs = "none"
|
|
35
|
+
minimal_conf.fold_col_hdrs = "no"
|
|
36
|
+
minimal_conf.dedup_col_hdrs = False
|
|
37
|
+
minimal_conf.import_encoding = "utf-8"
|
|
38
|
+
minimal_conf.import_common_cols_only = False
|
|
39
|
+
minimal_conf.quote_all_text = False
|
|
40
|
+
minimal_conf.scan_lines = 50
|
|
41
|
+
minimal_conf.empty_rows = True
|
|
42
|
+
minimal_conf.import_row_buffer = 1000
|
|
43
|
+
minimal_conf.import_progress_interval = 0
|
|
44
|
+
minimal_conf.show_progress = False
|
|
45
|
+
yield minimal_conf
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def db(tmp_path):
|
|
50
|
+
path = str(tmp_path / "test.db")
|
|
51
|
+
d = SQLiteDatabase(path)
|
|
52
|
+
yield d
|
|
53
|
+
d.close()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ===========================================================================
|
|
57
|
+
# Empty and minimal files
|
|
58
|
+
# ===========================================================================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestEmptyAndMinimalFiles:
|
|
62
|
+
def test_header_only_no_data_rows(self, db, tmp_path):
|
|
63
|
+
"""A CSV with headers but zero data rows should create a table with no rows."""
|
|
64
|
+
csv = tmp_path / "empty_data.csv"
|
|
65
|
+
csv.write_text("id,name,value\n", encoding="utf-8")
|
|
66
|
+
importtable(db, None, "empty_tbl", str(csv), is_new=1)
|
|
67
|
+
_, rows = db.select_data("SELECT * FROM empty_tbl;")
|
|
68
|
+
assert len(rows) == 0
|
|
69
|
+
|
|
70
|
+
def test_single_column_single_row(self, db, tmp_path):
|
|
71
|
+
csv = tmp_path / "single.csv"
|
|
72
|
+
csv.write_text("x\n42\n", encoding="utf-8")
|
|
73
|
+
importtable(db, None, "single_tbl", str(csv), is_new=1)
|
|
74
|
+
_, rows = db.select_data("SELECT x FROM single_tbl;")
|
|
75
|
+
assert len(rows) == 1
|
|
76
|
+
|
|
77
|
+
def test_nonexistent_file_raises(self, db, tmp_path):
|
|
78
|
+
with pytest.raises(ErrInfo, match="Non-existent file"):
|
|
79
|
+
importtable(db, None, "t", str(tmp_path / "ghost.csv"), is_new=1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ===========================================================================
|
|
83
|
+
# BOM handling
|
|
84
|
+
# ===========================================================================
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestBOMHandling:
|
|
88
|
+
def test_utf8_bom_import(self, db, tmp_path):
|
|
89
|
+
"""UTF-8 BOM should not corrupt the first column header."""
|
|
90
|
+
csv = tmp_path / "bom.csv"
|
|
91
|
+
csv.write_bytes(b"\xef\xbb\xbfid,name\n1,Alice\n2,Bob\n")
|
|
92
|
+
importtable(db, None, "bom_tbl", str(csv), is_new=1, encoding="utf-8-sig")
|
|
93
|
+
cols = db.table_columns("bom_tbl")
|
|
94
|
+
# First column should be 'id', not '\ufeffid'
|
|
95
|
+
assert cols[0] == "id"
|
|
96
|
+
_, rows = db.select_data("SELECT id FROM bom_tbl ORDER BY id;")
|
|
97
|
+
assert len(rows) == 2
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ===========================================================================
|
|
101
|
+
# Unicode and special characters
|
|
102
|
+
# ===========================================================================
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestUnicodeData:
|
|
106
|
+
def test_unicode_values(self, db, tmp_path):
|
|
107
|
+
csv = tmp_path / "unicode.csv"
|
|
108
|
+
csv.write_text('id,name\n1,"café résumé"\n2,"日本語"\n', encoding="utf-8")
|
|
109
|
+
importtable(db, None, "uni_tbl", str(csv), is_new=1)
|
|
110
|
+
_, rows = db.select_data("SELECT name FROM uni_tbl ORDER BY id;")
|
|
111
|
+
assert rows[0][0] == "café résumé"
|
|
112
|
+
assert rows[1][0] == "日本語"
|
|
113
|
+
|
|
114
|
+
def test_embedded_commas_in_quoted_field(self, db, tmp_path):
|
|
115
|
+
csv = tmp_path / "commas.csv"
|
|
116
|
+
csv.write_text('id,description\n1,"hello, world"\n', encoding="utf-8")
|
|
117
|
+
importtable(db, None, "comma_tbl", str(csv), is_new=1)
|
|
118
|
+
_, rows = db.select_data("SELECT description FROM comma_tbl;")
|
|
119
|
+
assert rows[0][0] == "hello, world"
|
|
120
|
+
|
|
121
|
+
def test_embedded_newlines_in_quoted_field(self, db, tmp_path):
|
|
122
|
+
"""Embedded newlines in quoted CSV fields are not preserved by the
|
|
123
|
+
execsql CSV reader — the newline is treated as a row break. This test
|
|
124
|
+
documents the current behavior: the import succeeds but the value is
|
|
125
|
+
split across rows or truncated."""
|
|
126
|
+
csv = tmp_path / "newlines.csv"
|
|
127
|
+
csv.write_text('id,val\n1,"line1\nline2"\n', encoding="utf-8")
|
|
128
|
+
# The import should not crash — whether the newline is preserved
|
|
129
|
+
# depends on the parser path (csv module fast-path vs character parser).
|
|
130
|
+
try:
|
|
131
|
+
importtable(db, None, "nl_tbl", str(csv), is_new=1)
|
|
132
|
+
except ErrInfo:
|
|
133
|
+
pass # Some parser paths may raise on the malformed row; that's acceptable
|
|
134
|
+
|
|
135
|
+
def test_embedded_quotes_doubled(self, db, tmp_path):
|
|
136
|
+
csv = tmp_path / "quotes.csv"
|
|
137
|
+
csv.write_text('id,description\n1,"he said ""hello"""\n', encoding="utf-8")
|
|
138
|
+
importtable(db, None, "qt_tbl", str(csv), is_new=1)
|
|
139
|
+
_, rows = db.select_data("SELECT description FROM qt_tbl;")
|
|
140
|
+
assert rows[0][0] == 'he said "hello"'
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ===========================================================================
|
|
144
|
+
# Long fields
|
|
145
|
+
# ===========================================================================
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TestLongFields:
|
|
149
|
+
def test_long_string_value(self, db, tmp_path):
|
|
150
|
+
"""Values over 255 chars should be imported as TEXT type."""
|
|
151
|
+
long_val = "x" * 500
|
|
152
|
+
csv = tmp_path / "long.csv"
|
|
153
|
+
csv.write_text(f"id,data\n1,{long_val}\n", encoding="utf-8")
|
|
154
|
+
importtable(db, None, "long_tbl", str(csv), is_new=1)
|
|
155
|
+
_, rows = db.select_data("SELECT data FROM long_tbl;")
|
|
156
|
+
assert len(rows[0][0]) == 500
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ===========================================================================
|
|
160
|
+
# Encoding parameter
|
|
161
|
+
# ===========================================================================
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TestEncodingOverride:
|
|
165
|
+
def test_latin1_encoding(self, db, tmp_path):
|
|
166
|
+
csv = tmp_path / "latin1.csv"
|
|
167
|
+
csv.write_bytes(b"id,name\n1,caf\xe9\n")
|
|
168
|
+
importtable(db, None, "lat_tbl", str(csv), is_new=1, encoding="latin-1")
|
|
169
|
+
_, rows = db.select_data("SELECT name FROM lat_tbl;")
|
|
170
|
+
assert rows[0][0] == "café"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ===========================================================================
|
|
174
|
+
# Junk header lines
|
|
175
|
+
# ===========================================================================
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestJunkHeaderLines:
|
|
179
|
+
def test_skip_junk_lines(self, db, tmp_path):
|
|
180
|
+
csv = tmp_path / "junk.csv"
|
|
181
|
+
csv.write_text("Report Title\nGenerated: today\nid,name\n1,Alice\n", encoding="utf-8")
|
|
182
|
+
importtable(db, None, "junk_tbl", str(csv), is_new=1, junk_header_lines=2)
|
|
183
|
+
_, rows = db.select_data("SELECT name FROM junk_tbl;")
|
|
184
|
+
assert rows[0][0] == "Alice"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ===========================================================================
|
|
188
|
+
# Append to existing table with type compatibility
|
|
189
|
+
# ===========================================================================
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestAppendEdgeCases:
|
|
193
|
+
def test_append_with_null_values(self, db, tmp_path):
|
|
194
|
+
db.execute("CREATE TABLE t (id INTEGER, name TEXT);")
|
|
195
|
+
db.execute("INSERT INTO t VALUES (1, 'existing');")
|
|
196
|
+
db.commit()
|
|
197
|
+
csv = tmp_path / "nulls.csv"
|
|
198
|
+
csv.write_text("id,name\n2,\n", encoding="utf-8")
|
|
199
|
+
importtable(db, None, "t", str(csv), is_new=False)
|
|
200
|
+
_, rows = db.select_data("SELECT name FROM t WHERE id = 2;")
|
|
201
|
+
assert len(rows) == 1
|
|
202
|
+
# With empty_strings=True (default), empty CSV fields are imported as empty strings
|
|
203
|
+
assert rows[0][0] == "" or rows[0][0] is None
|
|
204
|
+
|
|
205
|
+
def test_append_nonexistent_table(self, db, tmp_path):
|
|
206
|
+
csv = tmp_path / "data.csv"
|
|
207
|
+
csv.write_text("x\n1\n", encoding="utf-8")
|
|
208
|
+
with pytest.raises(ErrInfo, match="Non-existent table"):
|
|
209
|
+
importtable(db, None, "no_such", str(csv), is_new=False)
|