execsql2 2.16.16__tar.gz → 2.16.18__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.16.16 → execsql2-2.16.18}/CHANGELOG.md +22 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/PKG-INFO +1 -1
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/about/divergence.md +9 -7
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/reference/metacommands.md +22 -2
- {execsql2-2.16.16 → execsql2-2.16.18}/pyproject.toml +2 -2
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/debug/repl.py +1 -1
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/format.py +22 -4
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/debug.py +14 -4
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/script/parser.py +45 -5
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_show_scripts.py +64 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_debug_repl.py +50 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_executor.py +66 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_format.py +101 -0
- execsql2-2.16.18/tests/test_parser_params.py +124 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/uv.lock +1 -1
- execsql2-2.16.16/tests/test_parser_params.py +0 -56
- {execsql2-2.16.16 → execsql2-2.16.18}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/.gitignore +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/.pre-commit-config.yaml +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/.python-version +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/.readthedocs.yaml +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/CONTRIBUTING.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/LICENSE.txt +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/NOTICE +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/README.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/SECURITY.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/about/contributors.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/about/copyright.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/api/cli.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/api/db.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/api/exporters.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/api/importers.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/api/index.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/api/metacommands.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/dev/architecture.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/getting-started/installation.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/debugging.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/documentation.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/encoding.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/examples.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/formatter.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/logging.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/usage.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/actions.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/actions2.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/checkboxes.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/connect.b64 +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/connect.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/create_conf.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/entry_form.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/execsql_console.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/fatals.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/logo_small.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/unmatched.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/index.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/reference/configuration.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/reference/security.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/extras/plugin-template/README.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/extras/plugin-template/pyproject.toml +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/extras/plugin-template/tests/test_plugin.py.example +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/justfile +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/__main__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/api.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/cli/help.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/cli/lint_ast.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/cli/run.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/config.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/data/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/data/execsql.conf.template +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/access.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/base.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/factory.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exceptions.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/gui/base.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/gui/console.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/importers/base.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/importers/json.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/control.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/metacommands/upsert.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/models.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/parser.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/plugins.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/py.typed +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/script/ast.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/script/control.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/script/engine.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/script/executor.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/script/variables.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/state.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/types.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/README.md +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/config_settings.sqlite +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/execsql.conf +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/make_config_db.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/md_compare.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/md_glossary.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/md_upsert.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/pg_compare.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/pg_glossary.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/pg_upsert.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/script_template.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/ss_compare.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/ss_glossary.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/templates/ss_upsert.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/cli/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/cli/test_cli.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/cli/test_lint.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/cli/test_ping.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/cli/test_profile.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/conftest.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/test_base.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/test_db_adapters_mocked.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/test_dsn.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/test_factory.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/test_postgres.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_base.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_db.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_html_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_json.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_json_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_latex_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_pretty_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_raw_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_templates_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_values_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/gui/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/gui/test_backends.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/gui/test_compare_stats.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/gui/test_compute_row_diffs.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/importers/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/importers/test_base_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/importers/test_csv_edge_cases.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/importers/test_json_importer.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/integration/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/integration/conftest.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_pg_upsert.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/scripts/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/scripts/fixtures/control_flow.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/scripts/fixtures/smoke.sql +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/scripts/test_sql_scripts.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_api.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_ast.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_ast_parser.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_config.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_config_data.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_config_extended.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_engine.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_error_messages.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_exceptions.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_mail.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_models.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_package.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_parser.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_plugins.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_registry.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_script.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_state.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/test_types.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/__init__.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_auth.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_errors.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_regex.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_strings.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_timer.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.16.16 → execsql2-2.16.18}/zensical.toml +0 -0
|
@@ -13,6 +13,28 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.16.18] - 2026-05-05
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `BEGIN SCRIPT` parameter defaults now correctly strip surrounding quotes when stored, mirroring the quote-handling already applied to passed arguments at the call site. Previously a default written as `default_unit_set="Default"` bound the literal string `"Default"` (with quotes intact), so a body like `WRITE "!!#default_unit_set!!"` produced `WRITE ""Default""` and failed. Defaults and passed arguments now resolve to the same value for the same source token.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `SHOW SCRIPTS <name>` metacommand and `.scripts <name>` debug REPL command now display the full source path to the script's source file (including `<inline>` for scripts loaded via `execsql -c`). The list views (`SHOW SCRIPTS` and `.scripts` without a name) continue to show the basename for compact column-aligned output.
|
|
25
|
+
- `BEGIN SCRIPT WITH PARAMETERS (...)` now accepts quoted default values containing spaces, commas, and other special characters — e.g. `proc(msg="hello, world", path="/var/log/app.log")`. Both single and double quotes are supported. Unquoted values continue to be accepted but cannot start with a quote character; an unterminated quoted value is now rejected as malformed instead of being silently stored as an unquoted literal.
|
|
26
|
+
|
|
27
|
+
______________________________________________________________________
|
|
28
|
+
|
|
29
|
+
## [2.16.17] - 2026-05-04
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Formatter no longer treats inline `IF (cond) { command }` metacommands as block openers. Previously every line below an inline `IF` was indented one extra level forever, since the formatter incremented its block depth as if the inline form required an `ENDIF`.
|
|
34
|
+
- Formatter no longer escapes blank lines that appear inside `-- !x! BEGIN SQL` / `-- !x! BEGIN BATCH` blocks. Blanks immediately after `BEGIN SQL` (and between SQL statements inside the block) were emitted flush-left, visually severing the block; they are now held with the SQL accumulator and consumed by the SQL pretty-printer.
|
|
35
|
+
|
|
36
|
+
______________________________________________________________________
|
|
37
|
+
|
|
16
38
|
## [2.16.16] - 2026-05-02
|
|
17
39
|
|
|
18
40
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.16.
|
|
3
|
+
Version: 2.16.18
|
|
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
|
|
@@ -58,16 +58,18 @@ ______________________________________________________________________
|
|
|
58
58
|
|
|
59
59
|
### SCRIPT Enhancements
|
|
60
60
|
|
|
61
|
-
| Feature | Description
|
|
62
|
-
| ------------------ |
|
|
63
|
-
| Default parameters | `BEGIN SCRIPT load(schema, table, batch=1000)` — parameters with defaults can be omitted at call site. Required parameters must precede optional parameters.
|
|
64
|
-
|
|
|
61
|
+
| Feature | Description |
|
|
62
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
63
|
+
| Default parameters | `BEGIN SCRIPT load(schema, table, batch=1000)` — parameters with defaults can be omitted at call site. Required parameters must precede optional parameters. |
|
|
64
|
+
| Quoted defaults | Default values may be quoted with single or double quotes to embed spaces, commas, or other special characters: `BEGIN SCRIPT proc(msg="hello, world", path="/var/log/app.log")`. Surrounding quotes are stripped at parse time so the bound substitution variable holds the value itself, matching the call-site quote-handling for passed arguments. |
|
|
65
|
+
| Docstrings | Comments (`--` or `/* */`) immediately following `BEGIN SCRIPT` are captured as documentation. A blank line terminates the docstring. Displayed by `SHOW SCRIPTS <name>` and `.scripts <name>` REPL command. |
|
|
65
66
|
|
|
66
67
|
### Bug Fixes
|
|
67
68
|
|
|
68
|
-
| Fix | Description
|
|
69
|
-
| ------------------------------- |
|
|
70
|
-
| Variable EXECUTE SCRIPT targets | `EXECUTE SCRIPT !!#var!!` now works — the parser accepts substitution variable patterns as script identifiers, and the executor resolves them at runtime.
|
|
69
|
+
| Fix | Description |
|
|
70
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
71
|
+
| Variable EXECUTE SCRIPT targets | `EXECUTE SCRIPT !!#var!!` now works — the parser accepts substitution variable patterns as script identifiers, and the executor resolves them at runtime. |
|
|
72
|
+
| Quoted parameter defaults | `BEGIN SCRIPT proc(name="value")` previously stored the literal `"value"` (with surrounding quotes), causing substitutions like `WRITE "!!#name!!"` to expand to `WRITE ""value""` and fail. Defaults now strip surrounding quotes at parse time, matching the `wo_quotes` handling already applied to passed arguments. |
|
|
71
73
|
|
|
72
74
|
### Conditional Tests
|
|
73
75
|
|
|
@@ -260,6 +260,24 @@ A required parameter after an optional parameter is a parse error:
|
|
|
260
260
|
-- !x! BEGIN SCRIPT bad(schema, batch=1000, table)
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
+
Default values may be quoted with single or double quotes when they contain spaces, commas, or other special characters. Surrounding quotes are stripped at parse time, so the bound substitution variable holds the value itself, not the quoted source token. This matches the quote-handling already applied to passed arguments at the call site:
|
|
264
|
+
|
|
265
|
+
```sql
|
|
266
|
+
-- !x! BEGIN SCRIPT std_chem_units (
|
|
267
|
+
-- selected_lr_rows,
|
|
268
|
+
-- output_table="std_chem",
|
|
269
|
+
-- default_unit_set="Default",
|
|
270
|
+
-- logfile="/tmp/run.log",
|
|
271
|
+
-- description="hello, world"
|
|
272
|
+
-- )
|
|
273
|
+
-- !!#default_unit_set!! resolves to: Default (no surrounding quotes)
|
|
274
|
+
-- !!#description!! resolves to: hello, world
|
|
275
|
+
-- !x! WRITE "Unit set: !!#default_unit_set!!"
|
|
276
|
+
-- !x! END SCRIPT
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Unquoted default values cannot contain whitespace and must not begin with a quote character. An unterminated quoted value (e.g. `name="value`) is rejected as a parse error rather than being silently stored as a literal.
|
|
280
|
+
|
|
263
281
|
### Docstrings
|
|
264
282
|
|
|
265
283
|
Comments (`--` or `/* */`) immediately following the BEGIN SCRIPT line are captured as the script's docstring. A blank line terminates the docstring. Docstrings are displayed by [SHOW SCRIPT](#show_script), [SHOW SCRIPTS](#show_scripts), and the `.scripts` REPL command.
|
|
@@ -2770,7 +2788,7 @@ The numeric expression may consist of the simple algebraic operations of additio
|
|
|
2770
2788
|
SHOW SCRIPTS [<name>]
|
|
2771
2789
|
```
|
|
2772
2790
|
|
|
2773
|
-
Without a name, lists all registered SCRIPT definitions with their parameter signatures and source locations. With a name, shows detail for that script including parameters, source
|
|
2791
|
+
Without a name, lists all registered SCRIPT definitions with their parameter signatures and source locations (basename only, for compact column-aligned output). With a name, shows detail for that script including parameters, full source path, line range, and docstring.
|
|
2774
2792
|
|
|
2775
2793
|
This is useful for discovering what scripts are available at runtime, especially when scripts are loaded from INCLUDEEd files whose paths are determined dynamically.
|
|
2776
2794
|
|
|
@@ -2796,7 +2814,7 @@ Registered scripts (3):
|
|
|
2796
2814
|
|
|
2797
2815
|
```
|
|
2798
2816
|
Script: load_data(schema, table, batch_size=1000)
|
|
2799
|
-
Source: pipeline.sql:15-42
|
|
2817
|
+
Source: /home/user/etl/pipeline.sql:15-42
|
|
2800
2818
|
Parameters:
|
|
2801
2819
|
schema (required)
|
|
2802
2820
|
table (required)
|
|
@@ -2805,6 +2823,8 @@ Parameters:
|
|
|
2805
2823
|
Load data from staging into the target table.
|
|
2806
2824
|
```
|
|
2807
2825
|
|
|
2826
|
+
The detail view (`SHOW SCRIPTS <name>`) shows the full source path so the file can be located unambiguously when multiple scripts share a basename. Scripts loaded inline via `execsql -c '<command>'` show `<inline>` as the source.
|
|
2827
|
+
|
|
2808
2828
|
If no scripts are registered, prints `No scripts registered.` If the named script is not found, prints `No script named '<name>' is registered.`
|
|
2809
2829
|
|
|
2810
2830
|
!!! tip
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.16.
|
|
7
|
+
version = "2.16.18"
|
|
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" }
|
|
@@ -165,7 +165,7 @@ skip-magic-trailing-comma = false
|
|
|
165
165
|
line-ending = "auto"
|
|
166
166
|
|
|
167
167
|
[tool.bumpversion]
|
|
168
|
-
current_version = "2.16.
|
|
168
|
+
current_version = "2.16.18"
|
|
169
169
|
commit = true
|
|
170
170
|
tag = true
|
|
171
171
|
tag_name = "v{new_version}"
|
|
@@ -546,7 +546,7 @@ def _print_script_detail(name: str) -> None:
|
|
|
546
546
|
return
|
|
547
547
|
block = scripts[script_name]
|
|
548
548
|
sig = _format_script_signature(block.name, block.param_defs)
|
|
549
|
-
src = _format_script_source(block.span)
|
|
549
|
+
src = _format_script_source(block.span, full_path=True)
|
|
550
550
|
_write_rule(f" {_c(_BOLD + _YELLOW, 'Script')} {_c(_DIM, '──')} {_c(_CYAN, sig)} ")
|
|
551
551
|
_write(f" {_c(_BOLD, 'Source:')} {src}\n")
|
|
552
552
|
if block.param_defs:
|
|
@@ -82,6 +82,14 @@ BLOCK_CLOSE = frozenset({"ENDIF", "END LOOP", "ENDLOOP", "END SCRIPT", "END BATC
|
|
|
82
82
|
PIVOT = frozenset({"ELSE", "ELSEIF"}) # decrease depth before emit, increase after
|
|
83
83
|
CONTINUATION = frozenset({"ANDIF", "ORIF"}) # emit at depth-1, no depth change
|
|
84
84
|
|
|
85
|
+
# Inline IF: "IF (cond) { command }" — self-contained, no ENDIF, no depth change.
|
|
86
|
+
# Mirrors src/execsql/cli/lint.py:_RX_IF_INLINE so formatter and linter agree.
|
|
87
|
+
_IF_INLINE_RE = re.compile(r"^\s*IF\s*\(\s*.+\s*\)\s*\{.+\}\s*$", re.I)
|
|
88
|
+
# BLOCK_OPEN keywords whose bodies are guaranteed-SQL (not metacommand-driven).
|
|
89
|
+
# Blank lines inside these belong to the SQL accumulator, not the output stream.
|
|
90
|
+
_SQL_BODY_BLOCKS = frozenset({"BEGIN SQL", "BEGIN BATCH"})
|
|
91
|
+
_SQL_BODY_BLOCK_CLOSES = frozenset({"END SQL", "END BATCH"})
|
|
92
|
+
|
|
85
93
|
|
|
86
94
|
# ---------------------------------------------------------------------------
|
|
87
95
|
# Keyword parsing
|
|
@@ -488,6 +496,10 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
|
|
|
488
496
|
# the accumulator — doing so would split a single statement into
|
|
489
497
|
# fragments that sqlglot cannot parse.
|
|
490
498
|
in_sql_statement = False
|
|
499
|
+
# True between BEGIN SQL/BATCH and END SQL/BATCH. Blank lines inside
|
|
500
|
+
# these blocks belong to the SQL accumulator so they re-emit at the
|
|
501
|
+
# block's indent depth, not flush-left in the output stream.
|
|
502
|
+
in_explicit_sql_block = False
|
|
491
503
|
|
|
492
504
|
def flush_sql() -> None:
|
|
493
505
|
nonlocal in_dollar_quote, in_sql_statement
|
|
@@ -520,12 +532,13 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
|
|
|
520
532
|
m = METACOMMAND_RE.match(raw_line)
|
|
521
533
|
|
|
522
534
|
if not stripped_line:
|
|
523
|
-
if not in_dollar_quote and not in_sql_statement:
|
|
535
|
+
if not in_dollar_quote and not in_sql_statement and not in_explicit_sql_block:
|
|
524
536
|
flush_sql()
|
|
525
537
|
output.append("")
|
|
526
538
|
else:
|
|
527
|
-
# Mid-statement blank line stays in
|
|
528
|
-
# will appear in the output
|
|
539
|
+
# Mid-statement OR mid-explicit-SQL-block blank line stays in
|
|
540
|
+
# the accumulator and will appear in the output at the block's
|
|
541
|
+
# indent depth when the SQL is formatted.
|
|
529
542
|
sql_acc.append(raw_line)
|
|
530
543
|
|
|
531
544
|
elif m:
|
|
@@ -536,6 +549,8 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
|
|
|
536
549
|
if keyword in BLOCK_CLOSE:
|
|
537
550
|
depth = max(0, depth - 1)
|
|
538
551
|
output.append(format_metacommand(payload, depth, indent))
|
|
552
|
+
if keyword in _SQL_BODY_BLOCK_CLOSES:
|
|
553
|
+
in_explicit_sql_block = False
|
|
539
554
|
|
|
540
555
|
elif keyword in PIVOT:
|
|
541
556
|
depth = max(0, depth - 1)
|
|
@@ -547,7 +562,10 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
|
|
|
547
562
|
|
|
548
563
|
elif keyword in BLOCK_OPEN:
|
|
549
564
|
output.append(format_metacommand(payload, depth, indent))
|
|
550
|
-
|
|
565
|
+
if not (keyword == "IF" and _IF_INLINE_RE.match(payload)):
|
|
566
|
+
depth += 1
|
|
567
|
+
if keyword in _SQL_BODY_BLOCKS:
|
|
568
|
+
in_explicit_sql_block = True
|
|
551
569
|
|
|
552
570
|
else:
|
|
553
571
|
output.append(format_metacommand(payload, depth, indent))
|
|
@@ -203,9 +203,19 @@ def _format_script_signature(name: str, param_defs: Any) -> str:
|
|
|
203
203
|
return f"{name}({', '.join(parts)})"
|
|
204
204
|
|
|
205
205
|
|
|
206
|
-
def _format_script_source(span: Any) -> str:
|
|
207
|
-
"""Return ``file:start-end`` from a SourceSpan.
|
|
208
|
-
|
|
206
|
+
def _format_script_source(span: Any, *, full_path: bool = False) -> str:
|
|
207
|
+
"""Return ``file:start-end`` from a SourceSpan.
|
|
208
|
+
|
|
209
|
+
By default the filename is the basename, suitable for compact list-view
|
|
210
|
+
output. Pass ``full_path=True`` to retain the full source path (used by
|
|
211
|
+
detail views like ``SHOW SCRIPTS <name>`` and ``.scripts <name>``).
|
|
212
|
+
"""
|
|
213
|
+
if not span or not span.file:
|
|
214
|
+
filename = "<unknown>"
|
|
215
|
+
elif full_path:
|
|
216
|
+
filename = span.file
|
|
217
|
+
else:
|
|
218
|
+
filename = Path(span.file).name
|
|
209
219
|
if span and span.start_line is not None:
|
|
210
220
|
if span.end_line is not None and span.end_line != span.start_line:
|
|
211
221
|
return f"{filename}:{span.start_line}-{span.end_line}"
|
|
@@ -235,7 +245,7 @@ def x_show_scripts(**kwargs: Any) -> None:
|
|
|
235
245
|
return
|
|
236
246
|
block = scripts[script_name]
|
|
237
247
|
sig = _format_script_signature(block.name, block.param_defs)
|
|
238
|
-
src = _format_script_source(block.span)
|
|
248
|
+
src = _format_script_source(block.span, full_path=True)
|
|
239
249
|
_state.output.write(f"Script: {sig}\n")
|
|
240
250
|
_state.output.write(f"Source: {src}\n")
|
|
241
251
|
if block.param_defs:
|
|
@@ -114,14 +114,48 @@ _EXEC_SCRIPT_RX = re.compile(
|
|
|
114
114
|
re.I,
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
+
# A parameter default value is either a double-quoted string, a single-quoted
|
|
118
|
+
# string (both may contain spaces, commas, and other special characters), or
|
|
119
|
+
# a bare run of non-whitespace characters. Unquoted values must not begin
|
|
120
|
+
# with a quote — that catches mismatched / unterminated quoted values
|
|
121
|
+
# (e.g. ``name="value``) instead of silently treating them as literals.
|
|
122
|
+
_PARAM_VALUE = r'(?:"[^"]*"|\'[^\']*\'|[^\s\'"]\S*)'
|
|
123
|
+
|
|
117
124
|
_WITH_PARAMS_RX = re.compile(
|
|
118
125
|
r"(?:\s+WITH)?(?:\s+PARAM(?:ETER)?S)?\s*\(\s*(?P<params>"
|
|
119
|
-
|
|
126
|
+
rf"\w+(?:\s*=\s*{_PARAM_VALUE})?(?:\s*,\s*\w+(?:\s*=\s*{_PARAM_VALUE})?)*"
|
|
120
127
|
r")\s*\)\s*$",
|
|
121
128
|
re.I,
|
|
122
129
|
)
|
|
123
130
|
|
|
124
|
-
_PARAM_TOKEN_RX = re.compile(
|
|
131
|
+
_PARAM_TOKEN_RX = re.compile(rf"(\w+)(?:\s*=\s*({_PARAM_VALUE}))?\s*\Z")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _split_param_tokens(params_str: str) -> list[str]:
|
|
135
|
+
"""Split a parameter list on commas, respecting quoted values.
|
|
136
|
+
|
|
137
|
+
Quoted segments (``"..."`` or ``'...'``) are kept intact so that commas
|
|
138
|
+
inside quotes do not split the token. Each returned token is stripped
|
|
139
|
+
of surrounding whitespace.
|
|
140
|
+
"""
|
|
141
|
+
tokens: list[str] = []
|
|
142
|
+
current: list[str] = []
|
|
143
|
+
in_quote: str | None = None
|
|
144
|
+
for ch in params_str:
|
|
145
|
+
if in_quote is not None:
|
|
146
|
+
current.append(ch)
|
|
147
|
+
if ch == in_quote:
|
|
148
|
+
in_quote = None
|
|
149
|
+
elif ch in ('"', "'"):
|
|
150
|
+
current.append(ch)
|
|
151
|
+
in_quote = ch
|
|
152
|
+
elif ch == ",":
|
|
153
|
+
tokens.append("".join(current))
|
|
154
|
+
current = []
|
|
155
|
+
else:
|
|
156
|
+
current.append(ch)
|
|
157
|
+
tokens.append("".join(current))
|
|
158
|
+
return [t.strip() for t in tokens]
|
|
125
159
|
|
|
126
160
|
|
|
127
161
|
def _parse_param_defs(
|
|
@@ -129,16 +163,21 @@ def _parse_param_defs(
|
|
|
129
163
|
lineno: int,
|
|
130
164
|
source: str,
|
|
131
165
|
) -> list[ParamDef]:
|
|
132
|
-
"""Parse ``'a, b, c=100, d=
|
|
166
|
+
"""Parse ``'a, b, c=100, d="hello world"'`` into a list of :class:`ParamDef`.
|
|
167
|
+
|
|
168
|
+
Defaults may be unquoted (``key=value``), double-quoted (``key="v a l"``),
|
|
169
|
+
or single-quoted (``key='v,a,l'``). Surrounding quotes are stripped so
|
|
170
|
+
that ``ParamDef.default`` always holds the resolved value, mirroring the
|
|
171
|
+
quote-handling done at the call site for passed arguments.
|
|
133
172
|
|
|
134
173
|
Required parameters (no default) must precede optional parameters
|
|
135
174
|
(with default). Raises :class:`ErrInfo` if ordering is violated.
|
|
136
175
|
"""
|
|
137
|
-
tokens =
|
|
176
|
+
tokens = _split_param_tokens(params_str)
|
|
138
177
|
defs: list[ParamDef] = []
|
|
139
178
|
seen_optional: str | None = None # name of first optional param
|
|
140
179
|
for token in tokens:
|
|
141
|
-
m = _PARAM_TOKEN_RX.match(token
|
|
180
|
+
m = _PARAM_TOKEN_RX.match(token)
|
|
142
181
|
if not m:
|
|
143
182
|
raise ErrInfo(
|
|
144
183
|
type="cmd",
|
|
@@ -146,6 +185,7 @@ def _parse_param_defs(
|
|
|
146
185
|
)
|
|
147
186
|
name, default = m.group(1), m.group(2)
|
|
148
187
|
if default is not None:
|
|
188
|
+
default = _strip_quotes(default)
|
|
149
189
|
if seen_optional is None:
|
|
150
190
|
seen_optional = name
|
|
151
191
|
elif seen_optional is not None:
|
|
@@ -65,6 +65,27 @@ class TestFormatScriptSource:
|
|
|
65
65
|
span = SourceSpan("test.sql", None)
|
|
66
66
|
assert _format_script_source(span) == "test.sql"
|
|
67
67
|
|
|
68
|
+
def test_full_path_keeps_directory(self):
|
|
69
|
+
span = SourceSpan("/long/path/to/script.sql", 1, 10)
|
|
70
|
+
assert _format_script_source(span, full_path=True) == "/long/path/to/script.sql:1-10"
|
|
71
|
+
|
|
72
|
+
def test_full_path_single_line(self):
|
|
73
|
+
span = SourceSpan("/abs/lib.sql", 7)
|
|
74
|
+
assert _format_script_source(span, full_path=True) == "/abs/lib.sql:7"
|
|
75
|
+
|
|
76
|
+
def test_full_path_unknown_file(self):
|
|
77
|
+
assert _format_script_source(None, full_path=True) == "<unknown>"
|
|
78
|
+
|
|
79
|
+
def test_full_path_inline_source(self):
|
|
80
|
+
"""`execsql -c <command>` parses with source_name='<inline>'."""
|
|
81
|
+
span = SourceSpan("<inline>", 1, 5)
|
|
82
|
+
assert _format_script_source(span, full_path=True) == "<inline>:1-5"
|
|
83
|
+
|
|
84
|
+
def test_basename_inline_source(self):
|
|
85
|
+
"""List-view rendering of an inline source is unchanged."""
|
|
86
|
+
span = SourceSpan("<inline>", 1, 5)
|
|
87
|
+
assert _format_script_source(span) == "<inline>:1-5"
|
|
88
|
+
|
|
68
89
|
|
|
69
90
|
# ---------------------------------------------------------------------------
|
|
70
91
|
# Handler tests (mock _state)
|
|
@@ -162,3 +183,46 @@ class TestShowScriptsDetail:
|
|
|
162
183
|
x_show_scripts(metacommandline="SHOW SCRIPTS")
|
|
163
184
|
output = mock_state.output.getvalue()
|
|
164
185
|
assert "Registered scripts (1)" in output
|
|
186
|
+
|
|
187
|
+
def test_detail_shows_full_path(self, mock_state):
|
|
188
|
+
"""SHOW SCRIPTS <name> renders the full source path, not just the basename."""
|
|
189
|
+
block = ScriptBlock(
|
|
190
|
+
span=SourceSpan("/home/user/etl/lib/load.sql", 12, 30),
|
|
191
|
+
name="proc",
|
|
192
|
+
param_defs=None,
|
|
193
|
+
doc=None,
|
|
194
|
+
)
|
|
195
|
+
mock_state.ast_scripts = {"proc": block}
|
|
196
|
+
x_show_scripts(script_id="proc", metacommandline="SHOW SCRIPTS proc")
|
|
197
|
+
output = mock_state.output.getvalue()
|
|
198
|
+
assert "Source: /home/user/etl/lib/load.sql:12-30" in output
|
|
199
|
+
|
|
200
|
+
def test_detail_inline_source(self, mock_state):
|
|
201
|
+
"""`execsql -c <command>` registers scripts with span.file == '<inline>'."""
|
|
202
|
+
block = ScriptBlock(
|
|
203
|
+
span=SourceSpan("<inline>", 1, 4),
|
|
204
|
+
name="proc",
|
|
205
|
+
param_defs=None,
|
|
206
|
+
doc=None,
|
|
207
|
+
)
|
|
208
|
+
mock_state.ast_scripts = {"proc": block}
|
|
209
|
+
x_show_scripts(script_id="proc", metacommandline="SHOW SCRIPTS proc")
|
|
210
|
+
output = mock_state.output.getvalue()
|
|
211
|
+
assert "Source: <inline>:1-4" in output
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TestShowScriptsListBasenamePreserved:
|
|
215
|
+
"""List view (no <name>) keeps basename — column-aligned output stays scannable."""
|
|
216
|
+
|
|
217
|
+
def test_list_uses_basename_for_long_paths(self, mock_state):
|
|
218
|
+
block = ScriptBlock(
|
|
219
|
+
span=SourceSpan("/home/user/etl/lib/load.sql", 12, 30),
|
|
220
|
+
name="proc",
|
|
221
|
+
param_defs=None,
|
|
222
|
+
doc=None,
|
|
223
|
+
)
|
|
224
|
+
mock_state.ast_scripts = {"proc": block}
|
|
225
|
+
x_show_scripts(metacommandline="SHOW SCRIPTS")
|
|
226
|
+
output = mock_state.output.getvalue()
|
|
227
|
+
assert "load.sql:12-30" in output
|
|
228
|
+
assert "/home/user/etl/lib/" not in output
|
|
@@ -453,6 +453,56 @@ class TestHandleDotCommand:
|
|
|
453
453
|
_handle_dot_command(".stack")
|
|
454
454
|
assert "empty" in capture.getvalue()
|
|
455
455
|
|
|
456
|
+
def test_scripts_detail_shows_full_path(self, capture):
|
|
457
|
+
""".scripts <name> renders the full source path, not just the basename."""
|
|
458
|
+
from execsql.script.ast import ScriptBlock, SourceSpan
|
|
459
|
+
|
|
460
|
+
_state.ast_scripts = {
|
|
461
|
+
"proc": ScriptBlock(
|
|
462
|
+
span=SourceSpan("/home/user/etl/lib/load.sql", 12, 30),
|
|
463
|
+
name="proc",
|
|
464
|
+
param_defs=None,
|
|
465
|
+
doc=None,
|
|
466
|
+
),
|
|
467
|
+
}
|
|
468
|
+
with patch("execsql.debug.repl._use_color", return_value=False):
|
|
469
|
+
_handle_dot_command(".scripts proc")
|
|
470
|
+
assert "/home/user/etl/lib/load.sql:12-30" in capture.getvalue()
|
|
471
|
+
|
|
472
|
+
def test_scripts_detail_inline_source(self, capture):
|
|
473
|
+
""".scripts <name> for an `execsql -c <command>` script renders <inline>."""
|
|
474
|
+
from execsql.script.ast import ScriptBlock, SourceSpan
|
|
475
|
+
|
|
476
|
+
_state.ast_scripts = {
|
|
477
|
+
"proc": ScriptBlock(
|
|
478
|
+
span=SourceSpan("<inline>", 1, 4),
|
|
479
|
+
name="proc",
|
|
480
|
+
param_defs=None,
|
|
481
|
+
doc=None,
|
|
482
|
+
),
|
|
483
|
+
}
|
|
484
|
+
with patch("execsql.debug.repl._use_color", return_value=False):
|
|
485
|
+
_handle_dot_command(".scripts proc")
|
|
486
|
+
assert "<inline>:1-4" in capture.getvalue()
|
|
487
|
+
|
|
488
|
+
def test_scripts_list_uses_basename(self, capture):
|
|
489
|
+
""".scripts (no name) keeps basename for compact column-aligned output."""
|
|
490
|
+
from execsql.script.ast import ScriptBlock, SourceSpan
|
|
491
|
+
|
|
492
|
+
_state.ast_scripts = {
|
|
493
|
+
"proc": ScriptBlock(
|
|
494
|
+
span=SourceSpan("/home/user/etl/lib/load.sql", 12, 30),
|
|
495
|
+
name="proc",
|
|
496
|
+
param_defs=None,
|
|
497
|
+
doc=None,
|
|
498
|
+
),
|
|
499
|
+
}
|
|
500
|
+
with patch("execsql.debug.repl._use_color", return_value=False):
|
|
501
|
+
_handle_dot_command(".scripts")
|
|
502
|
+
output = capture.getvalue()
|
|
503
|
+
assert "load.sql:12-30" in output
|
|
504
|
+
assert "/home/user/etl/lib/" not in output
|
|
505
|
+
|
|
456
506
|
|
|
457
507
|
# ---------------------------------------------------------------------------
|
|
458
508
|
# x_breakpoint — public entry point
|
|
@@ -1504,6 +1504,72 @@ class TestDefaultParameters:
|
|
|
1504
1504
|
assert result.returncode == 0, result.stderr
|
|
1505
1505
|
assert "load(schema, batch=1000)" in result.stdout
|
|
1506
1506
|
|
|
1507
|
+
def test_quoted_default_strips_quotes_at_substitution(self, tmp_path):
|
|
1508
|
+
"""`name="Default"` binds the value `Default`, not `"Default"`.
|
|
1509
|
+
|
|
1510
|
+
Regression for a parser bug where quoted defaults preserved their
|
|
1511
|
+
surrounding quotes — diverging from passed-args which strip them via
|
|
1512
|
+
``wo_quotes`` — so ``write "!!default_unit_set!!"`` produced
|
|
1513
|
+
``write ""Default""`` (a syntax error) instead of ``write "Default"``.
|
|
1514
|
+
"""
|
|
1515
|
+
result = _run_ast(
|
|
1516
|
+
'-- !x! BEGIN SCRIPT proc1(default_unit_set="Default")\n'
|
|
1517
|
+
"INSERT INTO t VALUES ('!!#default_unit_set!!');\n"
|
|
1518
|
+
"-- !x! END SCRIPT\n"
|
|
1519
|
+
"CREATE TABLE t (x TEXT);\n"
|
|
1520
|
+
"-- !x! EXECUTE SCRIPT proc1\n",
|
|
1521
|
+
tmp_path,
|
|
1522
|
+
)
|
|
1523
|
+
assert result.returncode == 0, result.stderr
|
|
1524
|
+
assert _query_db(tmp_path, "SELECT x FROM t") == [("Default",)]
|
|
1525
|
+
|
|
1526
|
+
def test_quoted_default_with_spaces(self, tmp_path):
|
|
1527
|
+
"""Quoted defaults with embedded spaces are preserved verbatim."""
|
|
1528
|
+
result = _run_ast(
|
|
1529
|
+
'-- !x! BEGIN SCRIPT proc1(msg="hello world")\n'
|
|
1530
|
+
"INSERT INTO t VALUES ('!!#msg!!');\n"
|
|
1531
|
+
"-- !x! END SCRIPT\n"
|
|
1532
|
+
"CREATE TABLE t (x TEXT);\n"
|
|
1533
|
+
"-- !x! EXECUTE SCRIPT proc1\n",
|
|
1534
|
+
tmp_path,
|
|
1535
|
+
)
|
|
1536
|
+
assert result.returncode == 0, result.stderr
|
|
1537
|
+
assert _query_db(tmp_path, "SELECT x FROM t") == [("hello world",)]
|
|
1538
|
+
|
|
1539
|
+
def test_quoted_default_with_special_chars(self, tmp_path):
|
|
1540
|
+
"""Path-like values with slashes survive parsing."""
|
|
1541
|
+
result = _run_ast(
|
|
1542
|
+
'-- !x! BEGIN SCRIPT proc1(logfile="/tmp/run.log")\n'
|
|
1543
|
+
"INSERT INTO t VALUES ('!!#logfile!!');\n"
|
|
1544
|
+
"-- !x! END SCRIPT\n"
|
|
1545
|
+
"CREATE TABLE t (x TEXT);\n"
|
|
1546
|
+
"-- !x! EXECUTE SCRIPT proc1\n",
|
|
1547
|
+
tmp_path,
|
|
1548
|
+
)
|
|
1549
|
+
assert result.returncode == 0, result.stderr
|
|
1550
|
+
assert _query_db(tmp_path, "SELECT x FROM t") == [("/tmp/run.log",)]
|
|
1551
|
+
|
|
1552
|
+
def test_quoted_default_passed_arg_consistency(self, tmp_path):
|
|
1553
|
+
"""A quoted default and a quoted passed-arg produce the same value.
|
|
1554
|
+
|
|
1555
|
+
Both code paths should resolve to the same unquoted value — this is
|
|
1556
|
+
the consistency guarantee the parser fix establishes.
|
|
1557
|
+
"""
|
|
1558
|
+
result = _run_ast(
|
|
1559
|
+
'-- !x! BEGIN SCRIPT proc1(label="alpha")\n'
|
|
1560
|
+
"INSERT INTO t VALUES ('!!#label!!');\n"
|
|
1561
|
+
"-- !x! END SCRIPT\n"
|
|
1562
|
+
"CREATE TABLE t (x TEXT);\n"
|
|
1563
|
+
"-- !x! EXECUTE SCRIPT proc1\n"
|
|
1564
|
+
'-- !x! EXECUTE SCRIPT proc1(label="alpha")\n',
|
|
1565
|
+
tmp_path,
|
|
1566
|
+
)
|
|
1567
|
+
assert result.returncode == 0, result.stderr
|
|
1568
|
+
assert _query_db(tmp_path, "SELECT x FROM t ORDER BY rowid") == [
|
|
1569
|
+
("alpha",),
|
|
1570
|
+
("alpha",),
|
|
1571
|
+
]
|
|
1572
|
+
|
|
1507
1573
|
|
|
1508
1574
|
# ---------------------------------------------------------------------------
|
|
1509
1575
|
# Docstrings
|
|
@@ -688,6 +688,107 @@ class TestFormatFileEdgeCases:
|
|
|
688
688
|
assert lines[1] == " -- !x! WRITE 'inside'"
|
|
689
689
|
assert lines[2] == "-- !x! END SCRIPT"
|
|
690
690
|
|
|
691
|
+
# ---- Inline IF (no ENDIF) — must not increment depth ---------------
|
|
692
|
+
|
|
693
|
+
def test_inline_if_does_not_indent_following_lines(self):
|
|
694
|
+
"""Inline `IF (cond) { cmd }` is self-contained; subsequent lines stay at the same depth."""
|
|
695
|
+
source = "-- !x! IF (1=1) { halt message \"stop\" }\n-- !x! WRITE 'after'\n"
|
|
696
|
+
result = format_file(source, use_sql=False)
|
|
697
|
+
lines = result.splitlines()
|
|
698
|
+
assert lines[0].startswith("-- !x! IF (1=1)")
|
|
699
|
+
assert lines[1] == "-- !x! WRITE 'after'"
|
|
700
|
+
|
|
701
|
+
def test_inline_if_with_substitution_vars(self):
|
|
702
|
+
"""Reproduces the user-reported case with substitution variables in the inline body."""
|
|
703
|
+
source = (
|
|
704
|
+
"-- !x! IF (not hasrows(cmp_primary_key_columns)) { "
|
|
705
|
+
'halt message "Table !!#table!! has no primary key columns." }\n'
|
|
706
|
+
"-- !x! WRITE 'after'\n"
|
|
707
|
+
)
|
|
708
|
+
result = format_file(source, use_sql=False)
|
|
709
|
+
lines = result.splitlines()
|
|
710
|
+
assert "IF (not hasrows" in lines[0]
|
|
711
|
+
assert "!!#table!!" in lines[0]
|
|
712
|
+
assert lines[1] == "-- !x! WRITE 'after'"
|
|
713
|
+
|
|
714
|
+
def test_inline_if_inside_block(self):
|
|
715
|
+
"""An inline IF nested inside a block-form IF emits at the current depth and does not push deeper."""
|
|
716
|
+
source = "-- !x! IF 1=1\n-- !x! IF (cond) { halt message \"x\" }\n-- !x! WRITE 'still inside'\n-- !x! ENDIF\n"
|
|
717
|
+
result = format_file(source, use_sql=False)
|
|
718
|
+
lines = result.splitlines()
|
|
719
|
+
assert lines[0] == "-- !x! IF 1=1"
|
|
720
|
+
assert lines[1].startswith(" -- !x! IF (cond)")
|
|
721
|
+
assert lines[2] == " -- !x! WRITE 'still inside'"
|
|
722
|
+
assert lines[3] == "-- !x! ENDIF"
|
|
723
|
+
|
|
724
|
+
def test_block_if_paren_form_still_indents(self):
|
|
725
|
+
"""Block-form `IF (cond)` (parens, no brace) still opens an indented block."""
|
|
726
|
+
source = "-- !x! IF (1=1)\n-- !x! WRITE 'yes'\n-- !x! ENDIF\n"
|
|
727
|
+
result = format_file(source, use_sql=False)
|
|
728
|
+
lines = result.splitlines()
|
|
729
|
+
assert lines[0] == "-- !x! IF (1=1)"
|
|
730
|
+
assert lines[1] == " -- !x! WRITE 'yes'"
|
|
731
|
+
assert lines[2] == "-- !x! ENDIF"
|
|
732
|
+
|
|
733
|
+
# ---- BEGIN SQL / BEGIN BATCH — blank lines must not escape ---------
|
|
734
|
+
|
|
735
|
+
def test_begin_sql_blank_line_after_does_not_escape(self):
|
|
736
|
+
"""A blank line right after `-- !x! BEGIN SQL` must not be emitted flush-left."""
|
|
737
|
+
source = "-- !x! BEGIN SQL\n\nSELECT 1;\n-- !x! END SQL\n"
|
|
738
|
+
result = format_file(source) # default use_sql=True; sqlglot collapses leading blank
|
|
739
|
+
lines = result.splitlines()
|
|
740
|
+
assert lines[0] == "-- !x! BEGIN SQL"
|
|
741
|
+
assert lines[-1] == "-- !x! END SQL"
|
|
742
|
+
middle = lines[1:-1]
|
|
743
|
+
assert "" not in middle, f"Flush-left blank inside BEGIN SQL block: {middle!r}"
|
|
744
|
+
select_lines = [line for line in middle if "SELECT" in line.upper()]
|
|
745
|
+
assert len(select_lines) == 1
|
|
746
|
+
assert select_lines[0].startswith(" ")
|
|
747
|
+
|
|
748
|
+
def test_begin_batch_blank_line_after_does_not_escape(self):
|
|
749
|
+
"""Same fix applies to BEGIN BATCH (also a guaranteed-SQL body)."""
|
|
750
|
+
source = "-- !x! BEGIN BATCH\n\nINSERT INTO t VALUES (1);\n-- !x! END BATCH\n"
|
|
751
|
+
result = format_file(source)
|
|
752
|
+
lines = result.splitlines()
|
|
753
|
+
assert lines[0] == "-- !x! BEGIN BATCH"
|
|
754
|
+
assert lines[-1] == "-- !x! END BATCH"
|
|
755
|
+
middle = lines[1:-1]
|
|
756
|
+
assert "" not in middle, f"Flush-left blank inside BEGIN BATCH block: {middle!r}"
|
|
757
|
+
|
|
758
|
+
def test_begin_sql_blank_line_between_statements_collapsed(self):
|
|
759
|
+
"""With sqlglot enabled, blanks between SQL statements inside BEGIN SQL are consumed
|
|
760
|
+
by pretty-print rather than escaping as flush-left blanks that split the block."""
|
|
761
|
+
source = "-- !x! BEGIN SQL\nSELECT 1;\n\nSELECT 2;\n-- !x! END SQL\n"
|
|
762
|
+
result = format_file(source)
|
|
763
|
+
lines = result.splitlines()
|
|
764
|
+
middle = lines[1:-1]
|
|
765
|
+
assert "" not in middle, f"Flush-left blank inside BEGIN SQL block: {middle!r}"
|
|
766
|
+
# sqlglot may pretty-print `SELECT 1` across multiple lines; check the
|
|
767
|
+
# body as a whole rather than per-line.
|
|
768
|
+
body = "\n".join(middle)
|
|
769
|
+
assert "SELECT" in body.upper()
|
|
770
|
+
assert "1;" in body and "2;" in body
|
|
771
|
+
|
|
772
|
+
def test_blank_lines_outside_explicit_sql_block_unchanged(self):
|
|
773
|
+
"""Regression guard: blanks outside BEGIN SQL/BATCH (top-level, or inside IF) still emit flush-left."""
|
|
774
|
+
# Top-level blank between metacommands
|
|
775
|
+
source = "-- !x! WRITE 'a'\n\n-- !x! WRITE 'b'\n"
|
|
776
|
+
result = format_file(source, use_sql=False)
|
|
777
|
+
lines = result.splitlines()
|
|
778
|
+
assert lines[0] == "-- !x! WRITE 'a'"
|
|
779
|
+
assert lines[1] == ""
|
|
780
|
+
assert lines[2] == "-- !x! WRITE 'b'"
|
|
781
|
+
|
|
782
|
+
# Blank between metacommands inside an IF block — still flush-left
|
|
783
|
+
source = "-- !x! IF 1=1\n-- !x! WRITE 'a'\n\n-- !x! WRITE 'b'\n-- !x! ENDIF\n"
|
|
784
|
+
result = format_file(source, use_sql=False)
|
|
785
|
+
lines = result.splitlines()
|
|
786
|
+
assert lines[0] == "-- !x! IF 1=1"
|
|
787
|
+
assert lines[1] == " -- !x! WRITE 'a'"
|
|
788
|
+
assert lines[2] == ""
|
|
789
|
+
assert lines[3] == " -- !x! WRITE 'b'"
|
|
790
|
+
assert lines[4] == "-- !x! ENDIF"
|
|
791
|
+
|
|
691
792
|
def test_only_sql_no_metacommands(self):
|
|
692
793
|
source = "SELECT 1;\nSELECT 2;\n"
|
|
693
794
|
result = format_file(source, use_sql=False)
|