execsql2 2.27.0__tar.gz → 2.29.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (487) hide show
  1. {execsql2-2.27.0 → execsql2-2.29.0}/CHANGELOG.md +38 -0
  2. {execsql2-2.27.0 → execsql2-2.29.0}/PKG-INFO +4 -2
  3. {execsql2-2.27.0 → execsql2-2.29.0}/README.md +1 -1
  4. {execsql2-2.27.0 → execsql2-2.29.0}/docs/about/divergence.md +8 -1
  5. {execsql2-2.27.0 → execsql2-2.29.0}/docs/getting-started/syntax.md +3 -2
  6. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/editors.md +4 -4
  7. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/formatter.md +11 -2
  8. {execsql2-2.27.0 → execsql2-2.29.0}/docs/reference/lint.md +1 -1
  9. {execsql2-2.27.0 → execsql2-2.29.0}/docs/reference/metacommands.md +2 -0
  10. {execsql2-2.27.0 → execsql2-2.29.0}/docs/reference/security.md +1 -1
  11. {execsql2-2.27.0 → execsql2-2.29.0}/docs/reference/substitution_vars.md +1 -1
  12. {execsql2-2.27.0 → execsql2-2.29.0}/extras/vscode-execsql/README.md +2 -3
  13. {execsql2-2.27.0 → execsql2-2.29.0}/pyproject.toml +4 -2
  14. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/commands/format.py +2 -1
  15. execsql2-2.29.0/src/execsql/cli/commands/lsp.py +64 -0
  16. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/lint.py +45 -22
  17. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/access.py +4 -4
  18. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/base.py +23 -12
  19. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/dsn.py +2 -2
  20. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/duckdb.py +14 -2
  21. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/firebird.py +2 -3
  22. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/mysql.py +21 -5
  23. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/oracle.py +5 -5
  24. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/postgres.py +3 -4
  25. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/sqlserver.py +2 -2
  26. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/duckdb.py +2 -2
  27. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/ods.py +2 -2
  28. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/templates.py +2 -2
  29. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/xls.py +6 -5
  30. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/xlsx.py +2 -2
  31. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/yaml.py +3 -4
  32. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/format.py +71 -21
  33. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/importers/base.py +6 -10
  34. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/document.py +7 -3
  35. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/debug.py +2 -2
  36. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/io_fileops.py +6 -6
  37. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/upsert.py +2 -5
  38. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/models.py +11 -0
  39. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/script/variables.py +42 -10
  40. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/types.py +8 -0
  41. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/errors.py +16 -0
  42. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_cli_surface.py +1 -1
  43. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_lint.py +19 -0
  44. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_copy_new.py +37 -0
  45. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_duckdb.py +3 -1
  46. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_new_table_checks.py +24 -1
  47. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_postgres_inprocess.py +3 -0
  48. execsql2-2.29.0/tests/db/test_quoted_substitution_roundtrip.py +40 -0
  49. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_xls_importer.py +32 -0
  50. {execsql2-2.27.0 → execsql2-2.29.0}/tests/integration/test_mysql.py +56 -0
  51. {execsql2-2.27.0 → execsql2-2.29.0}/tests/integration/test_postgres.py +82 -0
  52. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_document.py +10 -0
  53. execsql2-2.29.0/tests/lsp/test_lsp_command.py +71 -0
  54. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_server.py +9 -0
  55. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_fileops_extra.py +4 -2
  56. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_pg_upsert.py +1 -1
  57. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke.sql +3 -3
  58. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_substitution_injection.py +64 -20
  59. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_format.py +60 -0
  60. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_types.py +17 -0
  61. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_errors.py +27 -0
  62. {execsql2-2.27.0 → execsql2-2.29.0}/uv.lock +5 -1
  63. execsql2-2.27.0/src/execsql/cli/commands/lsp.py +0 -40
  64. execsql2-2.27.0/tests/lsp/test_lsp_command.py +0 -32
  65. {execsql2-2.27.0 → execsql2-2.29.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  66. {execsql2-2.27.0 → execsql2-2.29.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  67. {execsql2-2.27.0 → execsql2-2.29.0}/.github/dependabot.yml +0 -0
  68. {execsql2-2.27.0 → execsql2-2.29.0}/.github/workflows/ci-cd.yml +0 -0
  69. {execsql2-2.27.0 → execsql2-2.29.0}/.gitignore +0 -0
  70. {execsql2-2.27.0 → execsql2-2.29.0}/.pre-commit-config.yaml +0 -0
  71. {execsql2-2.27.0 → execsql2-2.29.0}/.pre-commit-hooks.yaml +0 -0
  72. {execsql2-2.27.0 → execsql2-2.29.0}/.python-version +0 -0
  73. {execsql2-2.27.0 → execsql2-2.29.0}/.readthedocs.yaml +0 -0
  74. {execsql2-2.27.0 → execsql2-2.29.0}/CONTRIBUTING.md +0 -0
  75. {execsql2-2.27.0 → execsql2-2.29.0}/LICENSE.txt +0 -0
  76. {execsql2-2.27.0 → execsql2-2.29.0}/NOTICE +0 -0
  77. {execsql2-2.27.0 → execsql2-2.29.0}/SECURITY.md +0 -0
  78. {execsql2-2.27.0 → execsql2-2.29.0}/docs/about/contributors.md +0 -0
  79. {execsql2-2.27.0 → execsql2-2.29.0}/docs/about/copyright.md +0 -0
  80. {execsql2-2.27.0 → execsql2-2.29.0}/docs/api/cli.md +0 -0
  81. {execsql2-2.27.0 → execsql2-2.29.0}/docs/api/db.md +0 -0
  82. {execsql2-2.27.0 → execsql2-2.29.0}/docs/api/exporters.md +0 -0
  83. {execsql2-2.27.0 → execsql2-2.29.0}/docs/api/importers.md +0 -0
  84. {execsql2-2.27.0 → execsql2-2.29.0}/docs/api/index.md +0 -0
  85. {execsql2-2.27.0 → execsql2-2.29.0}/docs/api/metacommands.md +0 -0
  86. {execsql2-2.27.0 → execsql2-2.29.0}/docs/dev/adding_db_adapters.md +0 -0
  87. {execsql2-2.27.0 → execsql2-2.29.0}/docs/dev/adding_exporters.md +0 -0
  88. {execsql2-2.27.0 → execsql2-2.29.0}/docs/dev/adding_importers.md +0 -0
  89. {execsql2-2.27.0 → execsql2-2.29.0}/docs/dev/adding_metacommands.md +0 -0
  90. {execsql2-2.27.0 → execsql2-2.29.0}/docs/dev/architecture.md +0 -0
  91. {execsql2-2.27.0 → execsql2-2.29.0}/docs/dev/releasing.md +0 -0
  92. {execsql2-2.27.0 → execsql2-2.29.0}/docs/getting-started/installation.md +0 -0
  93. {execsql2-2.27.0 → execsql2-2.29.0}/docs/getting-started/requirements.md +0 -0
  94. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/debugging.md +0 -0
  95. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/documentation.md +0 -0
  96. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/encoding.md +0 -0
  97. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/examples.md +0 -0
  98. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/logging.md +0 -0
  99. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/sql_syntax.md +0 -0
  100. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/usage.md +0 -0
  101. {execsql2-2.27.0 → execsql2-2.29.0}/docs/guides/using_scripts.md +0 -0
  102. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/Compare_planets.png +0 -0
  103. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/actions.png +0 -0
  104. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/actions2.png +0 -0
  105. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/checkboxes.png +0 -0
  106. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/connect.b64 +0 -0
  107. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/connect.png +0 -0
  108. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/create_conf.png +0 -0
  109. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/data_error1_screenshot.jpg +0 -0
  110. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/entry_form.png +0 -0
  111. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/execsql_console.png +0 -0
  112. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/execsql_logo_01.png +0 -0
  113. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/fatals.png +0 -0
  114. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/logo_small.png +0 -0
  115. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/pause_terminal.png +0 -0
  116. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/pause_terminal_sm.b64 +0 -0
  117. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/pause_terminal_sm.png +0 -0
  118. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/prompt_compare.png +0 -0
  119. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/set_build_commands.jpg +0 -0
  120. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/unit_conversions.b64 +0 -0
  121. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/unit_conversions_029.png +0 -0
  122. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/unmatched.png +0 -0
  123. {execsql2-2.27.0 → execsql2-2.29.0}/docs/images/vim_execsql_highlight.png +0 -0
  124. {execsql2-2.27.0 → execsql2-2.29.0}/docs/index.md +0 -0
  125. {execsql2-2.27.0 → execsql2-2.29.0}/docs/reference/configuration.md +0 -0
  126. {execsql2-2.27.0 → execsql2-2.29.0}/extras/plugin-template/README.md +0 -0
  127. {execsql2-2.27.0 → execsql2-2.29.0}/extras/plugin-template/pyproject.toml +0 -0
  128. {execsql2-2.27.0 → execsql2-2.29.0}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  129. {execsql2-2.27.0 → execsql2-2.29.0}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  130. {execsql2-2.27.0 → execsql2-2.29.0}/extras/vscode-execsql/.vscodeignore +0 -0
  131. {execsql2-2.27.0 → execsql2-2.29.0}/extras/vscode-execsql/LICENSE.txt +0 -0
  132. {execsql2-2.27.0 → execsql2-2.29.0}/extras/vscode-execsql/extension.js +0 -0
  133. {execsql2-2.27.0 → execsql2-2.29.0}/extras/vscode-execsql/icon.png +0 -0
  134. {execsql2-2.27.0 → execsql2-2.29.0}/extras/vscode-execsql/package-lock.json +0 -0
  135. {execsql2-2.27.0 → execsql2-2.29.0}/extras/vscode-execsql/package.json +0 -0
  136. {execsql2-2.27.0 → execsql2-2.29.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  137. {execsql2-2.27.0 → execsql2-2.29.0}/justfile +0 -0
  138. {execsql2-2.27.0 → execsql2-2.29.0}/scripts/benchmarks/README.md +0 -0
  139. {execsql2-2.27.0 → execsql2-2.29.0}/scripts/benchmarks/results/.gitignore +0 -0
  140. {execsql2-2.27.0 → execsql2-2.29.0}/scripts/benchmarks/run_benchmarks.py +0 -0
  141. {execsql2-2.27.0 → execsql2-2.29.0}/scripts/generate_vscode_grammar.py +0 -0
  142. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/__init__.py +0 -0
  143. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/__main__.py +0 -0
  144. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/api.py +0 -0
  145. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/__init__.py +0 -0
  146. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/application.py +0 -0
  147. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/commands/__init__.py +0 -0
  148. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/commands/config.py +0 -0
  149. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/commands/init.py +0 -0
  150. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/commands/lint.py +0 -0
  151. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/commands/listing.py +0 -0
  152. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/commands/run.py +0 -0
  153. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/dispatch.py +0 -0
  154. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/dsn.py +0 -0
  155. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/help.py +0 -0
  156. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/options.py +0 -0
  157. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/cli/run.py +0 -0
  158. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/config.py +0 -0
  159. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/config_validation.py +0 -0
  160. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/data/__init__.py +0 -0
  161. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/data/execsql.conf.template +0 -0
  162. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/data/script_header.sql +0 -0
  163. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/__init__.py +0 -0
  164. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/factory.py +0 -0
  165. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/sqlite.py +0 -0
  166. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/db/tiers.py +0 -0
  167. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/debug/__init__.py +0 -0
  168. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/debug/repl.py +0 -0
  169. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exceptions.py +0 -0
  170. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/__init__.py +0 -0
  171. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/base.py +0 -0
  172. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/delimited.py +0 -0
  173. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/feather.py +0 -0
  174. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/html.py +0 -0
  175. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/json.py +0 -0
  176. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/latex.py +0 -0
  177. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/markdown.py +0 -0
  178. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/parquet.py +0 -0
  179. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/pretty.py +0 -0
  180. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/protocol.py +0 -0
  181. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/raw.py +0 -0
  182. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/sqlite.py +0 -0
  183. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/values.py +0 -0
  184. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/xml.py +0 -0
  185. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/exporters/zip.py +0 -0
  186. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/gui/__init__.py +0 -0
  187. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/gui/base.py +0 -0
  188. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/gui/console.py +0 -0
  189. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/gui/desktop.py +0 -0
  190. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/gui/tui.py +0 -0
  191. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/importers/__init__.py +0 -0
  192. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/importers/csv.py +0 -0
  193. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/importers/feather.py +0 -0
  194. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/importers/json.py +0 -0
  195. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/importers/ods.py +0 -0
  196. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/importers/xls.py +0 -0
  197. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/interactive.py +0 -0
  198. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/__init__.py +0 -0
  199. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/code_actions.py +0 -0
  200. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/completion.py +0 -0
  201. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/diagnostics.py +0 -0
  202. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/folding.py +0 -0
  203. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/formatting.py +0 -0
  204. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/hover.py +0 -0
  205. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/navigation.py +0 -0
  206. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/rename.py +0 -0
  207. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/lsp/server.py +0 -0
  208. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/manifest.py +0 -0
  209. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/__init__.py +0 -0
  210. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/conditions.py +0 -0
  211. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/connect.py +0 -0
  212. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/control.py +0 -0
  213. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/data.py +0 -0
  214. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/dispatch.py +0 -0
  215. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/effects.py +0 -0
  216. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/io.py +0 -0
  217. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/io_export.py +0 -0
  218. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/io_import.py +0 -0
  219. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/io_write.py +0 -0
  220. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/prompt.py +0 -0
  221. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/reference.py +0 -0
  222. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/script_ext.py +0 -0
  223. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/metacommands/system.py +0 -0
  224. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/parser.py +0 -0
  225. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/plugins.py +0 -0
  226. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/py.typed +0 -0
  227. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/script/__init__.py +0 -0
  228. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/script/ast.py +0 -0
  229. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/script/control.py +0 -0
  230. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/script/engine.py +0 -0
  231. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/script/executor.py +0 -0
  232. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/script/parser.py +0 -0
  233. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/state.py +0 -0
  234. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/state.pyi +0 -0
  235. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/__init__.py +0 -0
  236. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/auth.py +0 -0
  237. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/color.py +0 -0
  238. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/crypto.py +0 -0
  239. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/datetime.py +0 -0
  240. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/fileio.py +0 -0
  241. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/gui.py +0 -0
  242. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/mail.py +0 -0
  243. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/numeric.py +0 -0
  244. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/regex.py +0 -0
  245. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/strings.py +0 -0
  246. {execsql2-2.27.0 → execsql2-2.29.0}/src/execsql/utils/timer.py +0 -0
  247. {execsql2-2.27.0 → execsql2-2.29.0}/templates/README.md +0 -0
  248. {execsql2-2.27.0 → execsql2-2.29.0}/templates/config_settings.sqlite +0 -0
  249. {execsql2-2.27.0 → execsql2-2.29.0}/templates/example_config_prompt.sql +0 -0
  250. {execsql2-2.27.0 → execsql2-2.29.0}/templates/make_config_db.sql +0 -0
  251. {execsql2-2.27.0 → execsql2-2.29.0}/templates/md_compare.sql +0 -0
  252. {execsql2-2.27.0 → execsql2-2.29.0}/templates/md_glossary.sql +0 -0
  253. {execsql2-2.27.0 → execsql2-2.29.0}/templates/md_upsert.sql +0 -0
  254. {execsql2-2.27.0 → execsql2-2.29.0}/templates/pg_compare.sql +0 -0
  255. {execsql2-2.27.0 → execsql2-2.29.0}/templates/pg_glossary.sql +0 -0
  256. {execsql2-2.27.0 → execsql2-2.29.0}/templates/pg_upsert.sql +0 -0
  257. {execsql2-2.27.0 → execsql2-2.29.0}/templates/script_template.sql +0 -0
  258. {execsql2-2.27.0 → execsql2-2.29.0}/templates/ss_compare.sql +0 -0
  259. {execsql2-2.27.0 → execsql2-2.29.0}/templates/ss_glossary.sql +0 -0
  260. {execsql2-2.27.0 → execsql2-2.29.0}/templates/ss_upsert.sql +0 -0
  261. {execsql2-2.27.0 → execsql2-2.29.0}/tests/__init__.py +0 -0
  262. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/__init__.py +0 -0
  263. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_cli.py +0 -0
  264. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_cli_e2e.py +0 -0
  265. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_cli_run.py +0 -0
  266. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_commands.py +0 -0
  267. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_config_validate.py +0 -0
  268. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_dispatch.py +0 -0
  269. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_error_location.py +0 -0
  270. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_init.py +0 -0
  271. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_lint_fixtures.py +0 -0
  272. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_log_exit_record.py +0 -0
  273. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_manifest.py +0 -0
  274. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_ping.py +0 -0
  275. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_profile.py +0 -0
  276. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_sigterm.py +0 -0
  277. {execsql2-2.27.0 → execsql2-2.29.0}/tests/cli/test_undeclared_imports.py +0 -0
  278. {execsql2-2.27.0 → execsql2-2.29.0}/tests/conftest.py +0 -0
  279. {execsql2-2.27.0 → execsql2-2.29.0}/tests/data/formatter_shapes/comment_anchoring.sql +0 -0
  280. {execsql2-2.27.0 → execsql2-2.29.0}/tests/data/formatter_shapes/escapes_and_literals.sql +0 -0
  281. {execsql2-2.27.0 → execsql2-2.29.0}/tests/data/formatter_shapes/substitution_and_metacommands.sql +0 -0
  282. {execsql2-2.27.0 → execsql2-2.29.0}/tests/data/formatter_shapes/unparsable_sql.sql +0 -0
  283. {execsql2-2.27.0 → execsql2-2.29.0}/tests/data/lint/conditions_and_blocks.sql +0 -0
  284. {execsql2-2.27.0 → execsql2-2.29.0}/tests/data/lint/does_not_parse.sql +0 -0
  285. {execsql2-2.27.0 → execsql2-2.29.0}/tests/data/lint/every_metacommand.sql +0 -0
  286. {execsql2-2.27.0 → execsql2-2.29.0}/tests/data/lint/invalid.sql +0 -0
  287. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/__init__.py +0 -0
  288. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_access_windows.py +0 -0
  289. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_base.py +0 -0
  290. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_db_adapters_mocked.py +0 -0
  291. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  292. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_dsn.py +0 -0
  293. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_factory.py +0 -0
  294. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_factory_postgres_encoding.py +0 -0
  295. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_import_atomic.py +0 -0
  296. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_import_replacement_quoted.py +0 -0
  297. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_import_short_rows.py +0 -0
  298. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_import_timezones.py +0 -0
  299. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_mysql_case_folding.py +0 -0
  300. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_mysql_inprocess.py +0 -0
  301. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_oracle_driver.py +0 -0
  302. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_postgres.py +0 -0
  303. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_roundtrip_adapters.py +0 -0
  304. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_sqlite.py +0 -0
  305. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_sqlite_extra.py +0 -0
  306. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_sqlserver_inprocess.py +0 -0
  307. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_tiers.py +0 -0
  308. {execsql2-2.27.0 → execsql2-2.29.0}/tests/db/test_transactions.py +0 -0
  309. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/__init__.py +0 -0
  310. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/conftest.py +0 -0
  311. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_base.py +0 -0
  312. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_db.py +0 -0
  313. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_delimited.py +0 -0
  314. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  315. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_exporters.py +0 -0
  316. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_feather.py +0 -0
  317. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_html_extended.py +0 -0
  318. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_html_latex.py +0 -0
  319. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_json.py +0 -0
  320. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_json_extended.py +0 -0
  321. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_latex_extended.py +0 -0
  322. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_markdown.py +0 -0
  323. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_ods.py +0 -0
  324. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_ods_export.py +0 -0
  325. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_parquet.py +0 -0
  326. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_pretty_extended.py +0 -0
  327. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_raw_extended.py +0 -0
  328. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_columnar.py +0 -0
  329. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_dbfile.py +0 -0
  330. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_delimited.py +0 -0
  331. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_json.py +0 -0
  332. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_latex_xml.py +0 -0
  333. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_markup.py +0 -0
  334. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_ods.py +0 -0
  335. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_pretty_raw.py +0 -0
  336. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_template.py +0 -0
  337. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_values.py +0 -0
  338. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_xlsx.py +0 -0
  339. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  340. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_templates.py +0 -0
  341. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_templates_extended.py +0 -0
  342. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_values_extended.py +0 -0
  343. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_xls_xlsx.py +0 -0
  344. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_xlsx.py +0 -0
  345. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_xml.py +0 -0
  346. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_yaml.py +0 -0
  347. {execsql2-2.27.0 → execsql2-2.29.0}/tests/exporters/test_zip.py +0 -0
  348. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/__init__.py +0 -0
  349. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_backends.py +0 -0
  350. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_backends_extended.py +0 -0
  351. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_compare_stats.py +0 -0
  352. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_compute_row_diffs.py +0 -0
  353. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_desktop_dialogs.py +0 -0
  354. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_format_table.py +0 -0
  355. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_tui_pilot.py +0 -0
  356. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_tui_pilot_complex.py +0 -0
  357. {execsql2-2.27.0 → execsql2-2.29.0}/tests/gui/test_utils_gui_extended.py +0 -0
  358. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/__init__.py +0 -0
  359. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/conftest.py +0 -0
  360. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_base_extended.py +0 -0
  361. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_csv_blank_lines.py +0 -0
  362. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_csv_edge_cases.py +0 -0
  363. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_csv_importer.py +0 -0
  364. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_feather_importer.py +0 -0
  365. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_json_importer.py +0 -0
  366. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_ods_importer.py +0 -0
  367. {execsql2-2.27.0 → execsql2-2.29.0}/tests/importers/test_roundtrip_csv.py +0 -0
  368. {execsql2-2.27.0 → execsql2-2.29.0}/tests/integration/__init__.py +0 -0
  369. {execsql2-2.27.0 → execsql2-2.29.0}/tests/integration/conftest.py +0 -0
  370. {execsql2-2.27.0 → execsql2-2.29.0}/tests/integration/test_dsn.py +0 -0
  371. {execsql2-2.27.0 → execsql2-2.29.0}/tests/integration/test_duckdb.py +0 -0
  372. {execsql2-2.27.0 → execsql2-2.29.0}/tests/integration/test_sqlite.py +0 -0
  373. {execsql2-2.27.0 → execsql2-2.29.0}/tests/live_db.py +0 -0
  374. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/__init__.py +0 -0
  375. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_code_actions.py +0 -0
  376. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_completion.py +0 -0
  377. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_diagnostics.py +0 -0
  378. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_folding.py +0 -0
  379. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_formatting.py +0 -0
  380. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_hover.py +0 -0
  381. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_navigation.py +0 -0
  382. {execsql2-2.27.0 → execsql2-2.29.0}/tests/lsp/test_rename.py +0 -0
  383. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/__init__.py +0 -0
  384. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_assert.py +0 -0
  385. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_breakpoint.py +0 -0
  386. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_conditions_extra.py +0 -0
  387. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_connect.py +0 -0
  388. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_io_export.py +0 -0
  389. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_io_import.py +0 -0
  390. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands.py +0 -0
  391. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  392. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_data.py +0 -0
  393. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_extended.py +0 -0
  394. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_io.py +0 -0
  395. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  396. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  397. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_system.py +0 -0
  398. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  399. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_prompt.py +0 -0
  400. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_row_count.py +0 -0
  401. {execsql2-2.27.0 → execsql2-2.29.0}/tests/metacommands/test_show_scripts.py +0 -0
  402. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/__init__.py +0 -0
  403. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  404. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  405. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  406. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/config_runtime.sql +0 -0
  407. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/control_flow.sql +0 -0
  408. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  409. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/error_handling.sql +0 -0
  410. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  411. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/import_options.sql +0 -0
  412. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/includes/helper.sql +0 -0
  413. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/includes.sql +0 -0
  414. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/io_formats.sql +0 -0
  415. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  416. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/multi_db.sql +0 -0
  417. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  418. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  419. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/predicates.sql +0 -0
  420. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/scripts.sql +0 -0
  421. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/smoke.sql +0 -0
  422. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  423. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  424. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  425. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/template_export.sql +0 -0
  426. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/timer.sql +0 -0
  427. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/transactions.sql +0 -0
  428. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/write_create_table.sql +0 -0
  429. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  430. {execsql2-2.27.0 → execsql2-2.29.0}/tests/scripts/test_sql_scripts.py +0 -0
  431. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/__init__.py +0 -0
  432. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_env_detection.py +0 -0
  433. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_expansion_bomb.py +0 -0
  434. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_import_regex_hardening.py +0 -0
  435. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_log_redaction.py +0 -0
  436. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_log_secrets.py +0 -0
  437. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_odbc_injection.py +0 -0
  438. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_path_containment.py +0 -0
  439. {execsql2-2.27.0 → execsql2-2.29.0}/tests/security/test_zip_bomb.py +0 -0
  440. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_api.py +0 -0
  441. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_api_errors.py +0 -0
  442. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_api_extended.py +0 -0
  443. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_api_filewriter.py +0 -0
  444. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_api_isolation.py +0 -0
  445. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_api_subprocess.py +0 -0
  446. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_ast.py +0 -0
  447. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_ast_parser.py +0 -0
  448. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_bundled_templates.py +0 -0
  449. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_config.py +0 -0
  450. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_config_data.py +0 -0
  451. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_config_extended.py +0 -0
  452. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_date_inference.py +0 -0
  453. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_debug_repl.py +0 -0
  454. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_engine.py +0 -0
  455. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_error_messages.py +0 -0
  456. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_exceptions.py +0 -0
  457. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_executor.py +0 -0
  458. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_executor_inprocess.py +0 -0
  459. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_interactive.py +0 -0
  460. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_mail.py +0 -0
  461. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_models.py +0 -0
  462. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_package.py +0 -0
  463. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_parser.py +0 -0
  464. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_parser_params.py +0 -0
  465. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_plugins.py +0 -0
  466. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_reference.py +0 -0
  467. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_registry.py +0 -0
  468. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_script.py +0 -0
  469. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_state.py +0 -0
  470. {execsql2-2.27.0 → execsql2-2.29.0}/tests/test_tooling_versions.py +0 -0
  471. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/__init__.py +0 -0
  472. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_auth.py +0 -0
  473. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_auth_extra.py +0 -0
  474. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_crypto.py +0 -0
  475. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_datetime.py +0 -0
  476. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_encodedfile_context.py +0 -0
  477. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_errors_extra.py +0 -0
  478. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_fileio.py +0 -0
  479. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_fileio_extra.py +0 -0
  480. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_keyring_rules.py +0 -0
  481. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_mail_tls.py +0 -0
  482. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_numeric.py +0 -0
  483. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_regex.py +0 -0
  484. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_strings.py +0 -0
  485. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_timer.py +0 -0
  486. {execsql2-2.27.0 → execsql2-2.29.0}/tests/utils/test_timer_extra.py +0 -0
  487. {execsql2-2.27.0 → execsql2-2.29.0}/zensical.toml +0 -0
@@ -13,6 +13,44 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.29.0] - 2026-10-13
17
+
18
+ ### Changed
19
+
20
+ - **`!'!var!'!` stores backslashes as written** on every DBMS: a value such as `C:\data\new` was stored as `C:\\data\\new` on SQLite, PostgreSQL, DuckDB, SQL Server, Oracle, Firebird and Access. MySQL/MariaDB (which read `\` as an escape) and PostgreSQL escape strings written as `E!'!var!'!` still get backslashes doubled, so both forms store the same value and the `E` workaround is no longer needed. Scripts that removed the doubled backslashes some other way should stop.
21
+
22
+ ### Fixed
23
+
24
+ - **Oracle IMPORT and COPY with a time column** create it as text (`VARCHAR2(15)`, holding `HH:MM:SS`); `TO NEW` stopped with "Oracle DBMS type has no specification for data type time".
25
+ - **MySQL IMPORT accepts a header with capital letters** (`Id,Name`): it stopped with "columns that are not in table" (leaving the new table behind), and with `IMPORT_COMMON_COLUMNS_ONLY` it loaded those columns as NULL.
26
+ - **PostgreSQL IMPORT honors `SKIP n`** for quoted CSV files, which load through PostgreSQL's COPY: it skipped only the header line, loading the lines before it as data or failing on them.
27
+ - **Spreadsheet IMPORT reads every data row:** an XLSX row whose values are all `0` or FALSE no longer ends the data (it and every row after it were dropped), and an XLS sheet imported with `SKIP n` no longer loses its last *n* rows.
28
+
29
+ ______________________________________________________________________
30
+
31
+ ## [2.28.0] - 2026-10-13
32
+
33
+ ### Added
34
+
35
+ - **`execsql lsp --check`** reports whether the language server can start (exit 0) or what is missing (exit 1).
36
+
37
+ ### Changed
38
+
39
+ - **Missing-package messages** (database drivers, spreadsheet, YAML and template exports, `PG_UPSERT`, the formatter and the language server) name the execsql2 extra and show how to add it while keeping the extras already installed.
40
+
41
+ ### Fixed
42
+
43
+ - **`execsql format` keeps optimizer hints and MySQL executable comments:** a statement holding `/*+ ... */` or `/*! ... */` is left as written in both modes. It was rewritten to `/* + ... */` and the hint moved off its keyword, so MySQL skipped the `/*! */` SQL (for example a `WHERE` or `PARTITION BY`) and Oracle/MySQL ignored the hint; re-check files the formatter or its pre-commit hook rewrote.
44
+ - **`execsql format` no longer ends a final statement with a `;` the script did not have:** a script execsql refused for an unfinished last statement became one that ran it.
45
+ - **`execsql format`:** a statement kept as written after another on the same line no longer starts with a space.
46
+ - **`execsql format` without sqlglot** printed its `pip install` hint without the `[formatter]` extra.
47
+ - **`execsql lint` and the language server read a `SUB_INI` file once** however many lines name it; a script naming a large INI file on many lines took minutes to lint and froze the editor. V002 for a variable a `SUB_INI` defines is reported at the `SUB_INI` line, not line 0.
48
+ - **`execsql lsp` with an incompatible pygls** names the installed pygls version instead of saying the `lsp` extra is missing.
49
+ - **`COPY` keeps binary columns** (SQLite/DuckDB BLOB, PostgreSQL `bytea`) byte for byte, and `COPY ... TO NEW` creates them as binary columns. They were copied as escaped text (`\x89PNG...`), and into PostgreSQL as text.
50
+ - **DuckDB `TO NEW`** is no longer refused because a table of the same name exists in an `ATTACH`ed database; only the current database counts.
51
+
52
+ ______________________________________________________________________
53
+
16
54
  ## [2.27.0] - 2026-10-12
17
55
 
18
56
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.27.0
3
+ Version: 2.29.0
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -95,8 +95,10 @@ Requires-Dist: openpyxl>=3.1; extra == 'dev'
95
95
  Requires-Dist: polars>=1.0; extra == 'dev'
96
96
  Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
97
97
  Requires-Dist: pygls<3,>=2.1; extra == 'dev'
98
+ Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
98
99
  Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
99
100
  Requires-Dist: pytest-lsp>=1.0; extra == 'dev'
101
+ Requires-Dist: pytest>=8; extra == 'dev'
100
102
  Requires-Dist: pyyaml>=6.0; extra == 'dev'
101
103
  Requires-Dist: ruff>=0.4; extra == 'dev'
102
104
  Requires-Dist: sqlglot<30.22,>=30.21.0; extra == 'dev'
@@ -464,7 +466,7 @@ execsql format --no-sql --in-place scripts/
464
466
  ```yaml
465
467
  repos:
466
468
  - repo: https://github.com/geocoug/execsql
467
- rev: v2.27.0
469
+ rev: v2.29.0
468
470
  hooks:
469
471
  - id: execsql-format
470
472
  - id: execsql-lint
@@ -321,7 +321,7 @@ execsql format --no-sql --in-place scripts/
321
321
  ```yaml
322
322
  repos:
323
323
  - repo: https://github.com/geocoug/execsql
324
- rev: v2.27.0
324
+ rev: v2.29.0
325
325
  hooks:
326
326
  - id: execsql-format
327
327
  - id: execsql-lint
@@ -286,6 +286,7 @@ The legacy flat command-list engine (`_parse_script_lines` / `runscripts` / `Com
286
286
  - **Exception hierarchy** — All custom exceptions inherit from `ExecSqlError`, enabling `except ExecSqlError` to catch any execsql-originated error.
287
287
  - **Exception chaining** — All `raise` statements inside `except` blocks preserve the original traceback via `from`.
288
288
  - **Database and driver error messages** — End with the error itself (`OperationalError: table t already exists`). Upstream appended "in <file> on line N of execsql", a line of its own source; in the package the file was often a driver's and the line number another file's. Where in execsql the error was raised goes to `execsql.log` as `Raised at: execsql/<module>.py:<line>`.
289
+ - **Missing optional package messages** — Name the execsql2 extra that installs the package and how to add it to a `uv tool` or `pipx` install without dropping the extras already there (`uv tool install "execsql2[...,postgres]"`). Upstream pointed at the package's web page.
289
290
  - **Config file errors** — An invalid value in `execsql.conf` stops a run with `Configuration error: <file>: <problem>` (and the line, for an unreadable file) plus a pointer to `execsql config --validate`. Upstream printed `Configuration error on line N of execsql.py`, where N was a line of its own source. A duplicate key, a missing section header or a bare `%` is reported the same way; upstream failed with an uncaught exception. Errors raised by `ConfigData` for a file are `ConfigFileError`, a subclass of `ConfigError`.
290
291
  - **ASSERT error type** — `ASSERT` failures now use a dedicated `"assert"` error type that produces `**** Assertion failed.` instead of `**** Error in metacommand.`. This distinguishes intentional script-level checks from actual metacommand errors. Upstream did not have `ASSERT`.
291
292
 
@@ -360,10 +361,16 @@ These are behavioral changes driven by security or correctness issues in the ups
360
361
  | `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. |
361
362
  | Partial dates inferred as timestamps | `dateutil` fills fields missing from a value with today's date, so `1-2`, `5/6`, `March 5`, `Jan` and `Monday` were inferred as timestamps and, on DuckDB, SQL Server, MySQL, Oracle, Firebird, Access and every ODS/XLSX/JSON/Parquet/Feather import, stored as dates in the current year. Inference now requires a four-digit year (or a month-first `m/d/yy` or `m-d-yy`) and a month and day, which restores upstream's behaviour for these values. `m-d-yy` is accepted in addition to upstream's `m/d/yy`. |
362
363
  | Offset values stored as plain timestamps | With `dateutil`, a value with a UTC offset (`2024-01-01 10:00+05:00`) also counted as a plain timestamp, so a column mixing offset and plain values was created as `timestamp` and the driver converted the offset values with the client's time zone: the stored value depended on the machine that ran the import. A value with an offset is again not a plain timestamp, as with upstream's offset-free format list, so a mixed column is text and keeps each value as written. A column where every value has an offset is still `timestamptz`. |
364
+ | Binary columns in `COPY` | Every value a source cursor returned as bytes was decoded to text (undecodable bytes as `\xNN` escapes) before `COPY` wrote it, and `TO NEW` typed the column as text. Upstream did the same for SQLite BLOBs; with psycopg2 a PostgreSQL `bytea` arrived as a `memoryview` and escaped the decode, so for PostgreSQL this was an execsql2 regression. `COPY` now moves bytes unchanged and creates binary columns. Exports still write the decoded text. |
365
+ | Backslashes in `!'!var!'!` | Upstream doubled backslashes on Windows only, to protect `re.sub`'s replacement template; execsql2 2.x doubled them on every platform as SQL escaping, which corrupted values on every DBMS but MySQL. A backslash is now doubled only where it is an escape: on a MySQL/MariaDB connection, and in a PostgreSQL escape string written as `E!'!var!'!`. Apostrophes are doubled everywhere. |
366
+ | Time columns on Oracle | `IMPORT` / `COPY ... TO NEW` of a column of times stopped with "Oracle DBMS type has no specification for data type time" (a `DT_Time_Oracle` type was registered but inference never produced it). A time column is now created as `VARCHAR2(15)` holding the ISO time text. |
367
+ | Capitalized headers on MySQL | MySQL compared the file's headers with the table's lower-cased column names, so `Id` did not match `id`: IMPORT stopped (after `TO NEW` had created the table), and with `IMPORT_COMMON_COLUMNS_ONLY` such columns were skipped. The comparison now ignores case, as on PostgreSQL and the other DBMSs. |
368
+ | `SKIP` on PostgreSQL's COPY path | The PostgreSQL fast path (a quoted CSV file) skipped one line, the header, whatever `SKIP` said, so the skipped lines were loaded as data. It now starts after the `SKIP` lines like every other reader. |
369
+ | Spreadsheet rows on import | XLSX reading stopped at the first row with no truthy value, so a row of `0` / FALSE values ended the data; XLS reading with `SKIP n` stopped *n* rows before the end of the sheet. Both now read to the first row of empty cells. |
363
370
  | Short data lines on import | A line with fewer values than the header was padded with NULLs only on PostgreSQL and MySQL. SQLite stopped with "Too few values", DuckDB and the other DBMSs with a driver parameter-count error. With `TO NEW` / `REPLACEMENT`, type inference skipped the missing values, so the column could be created `NOT NULL` and then reject the padded line, on PostgreSQL too. Missing trailing values are now NULL on every DBMS, and inference counts them as NULL. |
364
371
  | `COPY` re-typed text values | Upstream described the destination table by re-reading the source's text values as if they came from a file, so a text column of `1`/`0` became `boolean` and one of dates became `timestamp` with `NEW` / `REPLACEMENT`, and values copied into an existing text column were converted on the way in (`1` stored as `true`). Text from the source database now stays text. Upstream also ran the source query twice, once to describe the table and once to copy it; execsql2 runs it once and holds the rows in a temporary file in between. |
365
372
  | `REPLACEMENT` of a quoted or schema-qualified table | The SQLite, SQL Server, Oracle, Firebird and Access adapters quoted the table name a second time before dropping it, so `IMPORT` / `COPY ... TO REPLACEMENT "my tbl"` dropped a table named `"my tbl"` (quotes included) that did not exist and then failed to create the table, and on SQL Server, Oracle and Firebird `REPLACEMENT staging.t` tried to drop one table named `staging.t`. The name is now quoted once. |
366
- | `TO NEW` onto an existing table | `COPY`'s checks that the source exists and that a `NEW` target does not were dead code: each raised its error inside a `try` that discarded it. `IMPORT ... TO NEW` had no check. Both now stop with `Table <name> already exists` before any DDL (when the database cannot say whether the table exists, the command goes ahead). A name without a schema counts only in the schema the table would be created in (MySQL: the current database). The source check was removed instead of revived: PostgreSQL reads an unquoted `COPY Orders` as `orders`, which the check would have reported missing. |
373
+ | `TO NEW` onto an existing table | `COPY`'s checks that the source exists and that a `NEW` target does not were dead code: each raised its error inside a `try` that discarded it. `IMPORT ... TO NEW` had no check. Both now stop with `Table <name> already exists` before any DDL (when the database cannot say whether the table exists, the command goes ahead). A name without a schema counts only in the schema the table would be created in (MySQL: the current database), and on DuckDB only in the current database, not an `ATTACH`ed one. The source check was removed instead of revived: PostgreSQL reads an unquoted `COPY Orders` as `orders`, which the check would have reported missing. |
367
374
  | No `exit` log record on error or halt | The log was closed before the `exit` record was written, so a run that ended with an error, a `HALT` or a cancelled prompt left no `exit` line, and Ctrl-C was recorded as `unknown`. Every run now writes exactly one `exit` record; Ctrl-C is `interrupted`. |
368
375
  | SIGTERM lost the run's log | Python's default SIGTERM action ends the process without running exit handlers, so the buffered `execsql.log` (every record of the run) was never written, the transaction was not rolled back explicitly and a running `SYSTEM_CMD` kept going. The `execsql` command now turns SIGTERM into a normal exit with status 143 and an `exit` record of type `terminated`. `execsql.run()` does not install a handler. |
369
376
  | `access_use_numeric` ignored | The config option was read but never applied, so MS-Access tables created by `IMPORT` / `COPY` always got `DOUBLE` for decimal data. With `Yes` they get `NUMERIC(precision,scale)` again, as upstream. The setting is read when each table is created, so it applies per run (upstream changed the shared type map once at startup). |
@@ -300,8 +300,9 @@ The options below belong to `run`: `execsql run -tl script.sql mydb.sqlite`.
300
300
  `--version` and `-o`/`--online-help` work on their own (`execsql --version`)
301
301
  and anywhere on a `run` command line.
302
302
  `format` has its own options, described in the [formatter guide](../guides/formatter.md);
303
- `lint`'s are in the [lint rules reference](../reference/lint.md); `config`, `list` and `init`
304
- are described [above](#config_command).
303
+ `lint`'s are in the [lint rules reference](../reference/lint.md); `lsp`'s (`--check`) in
304
+ [Editor Support](../guides/editors.md#install); `config`, `list` and `init` are described
305
+ [above](#config_command).
305
306
 
306
307
  ### Connection options
307
308
 
@@ -123,9 +123,8 @@ pipx install --force "execsql2[postgres,lsp]"
123
123
  ```
124
124
 
125
125
  See [Adding an extra later](../getting-started/installation.md#adding-an-extra-later).
126
- Check it with `execsql lsp < /dev/null` (on Windows, `execsql lsp < NUL`): it
127
- exits at once without output when the server is installed, and says the `lsp`
128
- extra is needed when it is not.
126
+ Check it with `execsql lsp --check`: it prints `execsql lsp is ready (pygls 2.x.y)` and exits 0 when the server can start, and otherwise says what is
127
+ missing and exits 1.
129
128
 
130
129
  ## Settings { #settings }
131
130
 
@@ -163,7 +162,8 @@ execsql is the metacommand [plugins](../dev/architecture.md#plugin-system) insta
163
162
  environment, so lint knows their keywords, as `execsql lint` does. Besides the
164
163
  open script and the configuration files, it reads the files the script names
165
164
  in `INCLUDE` (to find the variables they define: up to 200 files of at most
166
- 10 MiB each) and in `SUB_INI` (at most 1 MiB). It reads only regular files,
165
+ 10 MiB each and 20 MiB together) and in `SUB_INI` (at most 1 MiB, read once
166
+ however many lines name it). It reads only regular files,
167
167
  never a FIFO or a device.
168
168
 
169
169
  ## VS Code { #vscode }
@@ -130,7 +130,7 @@ Conditions joined by `AND` / `OR` go on one line when their combined text is at
130
130
 
131
131
  #### Layout only, or rewrite { #layout-only }
132
132
 
133
- sqlglot does more than lay SQL out: it writes the SQL it parsed in its own preferred form. By default the formatter changes only layout and keyword case. A statement sqlglot would also rewrite is kept exactly as written, while the statements around it are still formatted.
133
+ sqlglot does more than lay SQL out: it writes the SQL it parsed in its own preferred form. By default the formatter changes only layout and the case of keywords and unquoted names (a function written `myFunc(x)` becomes `MYFUNC(x)`; quoted names and strings keep their case). A statement sqlglot would also rewrite is kept exactly as written, while the statements around it are still formatted.
134
134
 
135
135
  | You wrote | sqlglot writes |
136
136
  | --------------------------- | ------------------------------------- |
@@ -149,6 +149,15 @@ rewrite_sql = Yes
149
149
 
150
150
  With it on, the rewrites a statement gets can change when the pinned sqlglot changes (see above). Turning it off later does not undo rewrites already written; it only stops new ones.
151
151
 
152
+ In either mode, a statement holding an optimizer hint (`/*+ ... */`, Oracle and MySQL) or a MySQL executable comment (`/*! ... */`) is kept exactly as written: the server reads them as part of the statement, and sqlglot would turn them into ordinary comments or move a hint away from its keyword.
153
+
154
+ ```sql
155
+ INSERT /*+ APPEND */ INTO sales_hist SELECT * FROM sales_stage;
156
+ SELECT a FROM t /*!50001 WHERE a > 1 */;
157
+ ```
158
+
159
+ The formatter never adds a `;` to a final statement that has none: execsql refuses to run a script that ends with an unfinished statement, and formatting leaves that for you to fix.
160
+
152
161
  #### Comment handling
153
162
 
154
163
  Comments interleaved within SQL statements (e.g. `--` comments between SELECT columns, or inside CASE expressions) are preserved through formatting using a marker-based round-trip:
@@ -261,7 +270,7 @@ select id,name,created_at from users where active = true order by name;
261
270
  ```yaml
262
271
  repos:
263
272
  - repo: https://github.com/geocoug/execsql
264
- rev: v2.27.0
273
+ rev: v2.29.0
265
274
  hooks:
266
275
  - id: execsql-format
267
276
  - id: execsql-lint # optional: fail the commit on lint errors
@@ -158,7 +158,7 @@ The `execsql-lint` [pre-commit](https://pre-commit.com/) hook runs `execsql lint
158
158
  ```yaml
159
159
  repos:
160
160
  - repo: https://github.com/geocoug/execsql
161
- rev: v2.27.0
161
+ rev: v2.29.0
162
162
  hooks:
163
163
  - id: execsql-lint
164
164
  args: [--ignore, V002] # optional: any execsql lint option
@@ -1950,6 +1950,8 @@ The delimiter characters that will be recognized in a text file, and that can be
1950
1950
 
1951
1951
  The SKIP key phrase specifies the number of lines (or rows) at the beginning of the file (or worksheet) to discard before evaluating the remainder of the input as a data table.
1952
1952
 
1953
+ A worksheet's data ends at the first row whose cells are all empty; a row of zeros or FALSE values is data.
1954
+
1953
1955
  If the NEW keyword is used, the input will be scanned to determine the data type of each column, and a CREATE TABLE statement run to create a new table for the data. A column is typed as a date or timestamp only if every value gives a full date with the year written out (`2024-01-05`, `1/5/2024`, `Jan 5, 2024`) or as a month-first two-digit year (`1/5/24`, `01-05-24`); values such as `1-2`, `5/6`, `March 5`, `Jan`, `Monday` or a day-first `13-05-24` leave the column as text. Scanning of the file to determine data formats is separate from the scanning that may be done to determine the quote and delimiter characters. If a table of that name already exists, the IMPORT stops before creating or loading anything, with the error `Table <name> already exists`. A table name without a schema is looked for only in the schema the table would be created in (on MySQL, the current database), so a table of the same name in another schema does not stop the IMPORT. If the table already exists when the NEW keyword is used, a fatal error will result and *execsql* will halt. If the REPLACEMENT keyword is used, the result is the same as if the NEW keyword were used, except that an existing table of the given name will be deleted first. This replacement is not atomic: if the drop succeeds but a later create or load step fails, the old table has already been removed. If the drop fails, *execsql* stops the IMPORT command before creating or loading the replacement table. *execsql* uses a "drop table" statement to drop an existing table, and the "drop table" statement may not succeed if there are dependencies on that table (see the discussion of [implicit drop table
1954
1956
  statements](../guides/sql_syntax.md#implicit_drop)). If the table to be dropped does not exist, an informational message will be written to the log.
1955
1957
 
@@ -184,7 +184,7 @@ SELECT * FROM !!target_schema!!.mytable;
184
184
 
185
185
  If a variable value is derived from external input (command-line arguments, environment variables, `PROMPT` responses, or data read from a file), the script author is responsible for quoting or validating that value before it is substituted into SQL.
186
186
 
187
- The `!'!varname!'!` dereferencing form wraps the value in single quotes and doubles any embedded apostrophes, which helps when inserting text values into SQL literals:
187
+ The `!'!varname!'!` dereferencing form wraps the value in single quotes and doubles any embedded apostrophes (and, on MySQL/MariaDB, backslashes, so `\'` cannot end the literal there), which helps when inserting text values into SQL literals:
188
188
 
189
189
  ```sql
190
190
  -- !x! sub author_name !!&AUTHOR!!
@@ -11,7 +11,7 @@ SUB <match_string> <replacement_string>
11
11
  The \<match_string> identifies the substitution variable; the \<replacement_string> is the text inserted in its place. A substitution is recognized only when the match string is bracketed by one of three tokens:
12
12
 
13
13
  - `!!name!!` — insert the value verbatim.
14
- - `!'!name!'!` — wrap the value in single quotes, doubling any embedded apostrophe. Suitable for SQL string literals containing user data.
14
+ - `!'!name!'!` — wrap the value in single quotes, doubling any embedded apostrophe; on a MySQL or MariaDB connection, which reads `\` in a string literal as an escape, backslashes are doubled too. The value is stored exactly as written (`C:\data\new` stays `C:\data\new`). Suitable for SQL string literals containing user data. On PostgreSQL, `E!'!name!'!` (an escape string) doubles backslashes too, so it stores the same value as `!'!name!'!`; the `E` is not needed.
15
15
  - `!"!name!"!` — wrap the value in double quotes. Suitable for SQL quoted identifiers, or for metacommand arguments that must be quoted.
16
16
 
17
17
  Variables can also be set before the script starts: with `--var NAME=VALUE` on the [command line](../getting-started/syntax.md#options), or in the `[variables]` section of a [config file](configuration.md#configuration). `--var` wins over the config file, and a `SUB` in the script can reassign either.
@@ -196,9 +196,8 @@ ______________________________________________________________________
196
196
 
197
197
  **No lint findings or completion**
198
198
 
199
- - Run `execsql lsp < /dev/null` in a terminal (on Windows: `execsql lsp < NUL`). It exits at once
200
- without output when the server is installed; otherwise it says the `lsp` extra is needed. Add the
201
- extra with the extras you already have, e.g. `uv tool install "execsql2[postgres,lsp]"`
199
+ - Run `execsql lsp --check` in a terminal. It says `execsql lsp is ready` when the server can start;
200
+ otherwise it says what is missing. Add the extra with the extras you already have, e.g. `uv tool install "execsql2[postgres,lsp]"`
202
201
  ([Adding an extra later](https://execsql2.readthedocs.io/en/latest/getting-started/installation/#adding-an-extra-later)).
203
202
  - If `execsql` works in a terminal but not in VS Code, set `execsql.server.path` to its full path
204
203
  (`which execsql`).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.27.0"
7
+ version = "2.29.0"
8
8
  description = "Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = { file = "LICENSE.txt" }
@@ -103,6 +103,8 @@ dev = [
103
103
  "mkdocstrings-python>=2.0.3",
104
104
  "mypy>=1.10",
105
105
  "pre-commit>=3.5.0",
106
+ "pytest>=8",
107
+ "pytest-asyncio>=1.0",
106
108
  "pytest-cov>=5.0.0",
107
109
  "pytest-lsp>=1.0",
108
110
  "ruff>=0.4",
@@ -187,7 +189,7 @@ line-ending = "auto"
187
189
  exclude = ["*.md"]
188
190
 
189
191
  [tool.bumpversion]
190
- current_version = "2.27.0"
192
+ current_version = "2.29.0"
191
193
  commit = true
192
194
  tag = true
193
195
  tag_name = "v{new_version}"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
 
7
7
  import typer
8
+ from rich.markup import escape
8
9
 
9
10
  from execsql.cli.application import ExecsqlCommand, app
10
11
  from execsql.cli.help import _err_console
@@ -90,7 +91,7 @@ def format_cmd(
90
91
  try:
91
92
  _require_sqlglot()
92
93
  except ImportError as exc:
93
- _err_console.print(f"[bold red]Error:[/bold red] {exc}", highlight=False)
94
+ _err_console.print(f"[bold red]Error:[/bold red] {escape(str(exc))}", highlight=False)
94
95
  raise typer.Exit(code=1) from exc
95
96
  raise typer.Exit(
96
97
  code=run_formatter(
@@ -0,0 +1,64 @@
1
+ """The ``lsp`` command: the language server editors start for ``.sql`` files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+ from rich.markup import escape
7
+
8
+ from execsql.cli.application import ExecsqlCommand, app
9
+ from execsql.cli.help import _err_console
10
+ from execsql.utils.errors import install_hint
11
+
12
+ __all__ = ["lsp_cmd"]
13
+
14
+
15
+ def _lsp_libraries_installed() -> bool:
16
+ """Whether pygls and lsprotocol are installed (whether or not this execsql can use them)."""
17
+ from importlib.util import find_spec
18
+
19
+ return all(find_spec(name) is not None for name in ("pygls", "lsprotocol"))
20
+
21
+
22
+ def _pygls_version() -> str:
23
+ from importlib.metadata import PackageNotFoundError, version
24
+
25
+ try:
26
+ return version("pygls")
27
+ except PackageNotFoundError:
28
+ return "unknown"
29
+
30
+
31
+ @app.command(
32
+ cls=ExecsqlCommand,
33
+ name="lsp",
34
+ help=(
35
+ "Run the execsql language server on stdin/stdout, for an editor to start. "
36
+ "Shows execsql lint findings as you type, completes metacommands, "
37
+ "conditional tests and variables, explains them on hover, and finds where variables, "
38
+ "SCRIPTs and included files are defined, offers quick fixes, and formats documents. Requires the lsp "
39
+ "extra; --check reports whether it is installed."
40
+ ),
41
+ )
42
+ def lsp_cmd(
43
+ # Editors' language clients pass --stdio (VS Code always does); stdio is the only transport.
44
+ stdio: bool = typer.Option(False, "--stdio", hidden=True),
45
+ check: bool = typer.Option(False, "--check", help="Report whether the language server can start, and exit."),
46
+ ) -> None:
47
+ """The ``lsp`` command; its user-facing help is the decorator's ``help``."""
48
+ try:
49
+ from execsql.lsp.server import create_server
50
+ except ImportError as exc:
51
+ if _lsp_libraries_installed():
52
+ # pygls is there but this execsql cannot use it (another major version, a broken install).
53
+ message = (
54
+ f"execsql lsp cannot use the installed pygls {_pygls_version()} ({exc}). "
55
+ f"It needs pygls 2.x, which the lsp extra installs. {install_hint('lsp')}"
56
+ )
57
+ else:
58
+ message = f"execsql lsp needs the lsp extra. {install_hint('lsp')}"
59
+ _err_console.print(f"[bold red]Error:[/bold red] {escape(message)}", highlight=False)
60
+ raise typer.Exit(code=1) from exc
61
+ if check:
62
+ typer.echo(f"execsql lsp is ready (pygls {_pygls_version()}).")
63
+ return
64
+ create_server().start_io()
@@ -48,6 +48,7 @@ Public surface:
48
48
 
49
49
  from __future__ import annotations
50
50
 
51
+ import functools
51
52
  import json
52
53
  import re
53
54
  from collections import Counter
@@ -460,23 +461,40 @@ def _read_ini_vars(
460
461
  language server; a file that is unreadable or not valid INI defines
461
462
  nothing.
462
463
  """
463
- from configparser import ConfigParser
464
- from configparser import Error as ConfigError
465
-
466
464
  p = Path(ini_file)
467
465
  if not p.is_absolute() and script_dir is not None:
468
466
  p = script_dir / p
467
+ try:
468
+ if not p.is_file():
469
+ return
470
+ stat = p.stat()
471
+ if stat.st_size > _MAX_INI_BYTES:
472
+ return
473
+ resolved = str(p.resolve())
474
+ except OSError:
475
+ return
476
+ defined_vars.update(_ini_section_keys(resolved, stat.st_size, stat.st_mtime_ns, section))
477
+
478
+
479
+ @functools.lru_cache(maxsize=64)
480
+ def _ini_section_keys(path: str, size: int, mtime_ns: int, section: str) -> frozenset[str]:
481
+ """The upper-cased keys of *section* in the INI file at *path*.
482
+
483
+ Cached by the file's size and modification time: a script that names
484
+ the same file on many SUB_INI lines, and the language server re-linting
485
+ on every edit, read it once.
486
+ """
487
+ from configparser import ConfigParser
488
+ from configparser import Error as ConfigError
469
489
 
470
490
  cp = ConfigParser()
471
491
  try:
472
- if not p.is_file() or p.stat().st_size > _MAX_INI_BYTES:
473
- return
474
- cp.read_string(p.read_text(encoding="utf-8"), source=str(p))
492
+ cp.read_string(Path(path).read_text(encoding="utf-8"), source=path)
475
493
  except (OSError, UnicodeDecodeError, ConfigError):
476
- return
477
- if cp.has_section(section):
478
- for key, _value in cp.items(section):
479
- defined_vars.add(key.upper())
494
+ return frozenset()
495
+ if not cp.has_section(section):
496
+ return frozenset()
497
+ return frozenset(key.upper() for key, _value in cp.items(section))
480
498
 
481
499
 
482
500
  def _is_known_var(raw_name: str, defined_vars: set[str]) -> bool:
@@ -834,7 +852,12 @@ def _referencing_text(node: Node) -> list[str]:
834
852
  return texts
835
853
 
836
854
 
837
- def _check_unused_variables(script: Script, defined: set[str], issues: list[_Issue]) -> None:
855
+ def _check_unused_variables(
856
+ script: Script,
857
+ defined: set[str],
858
+ issues: list[_Issue],
859
+ script_dir: Path | None = None,
860
+ ) -> None:
838
861
  """Report variables a SUB defines that nothing ever reads.
839
862
 
840
863
  Almost always a typo in either the definition or the reference — the two
@@ -845,8 +868,11 @@ def _check_unused_variables(script: Script, defined: set[str], issues: list[_Iss
845
868
  referenced: set[str] = set()
846
869
  _collect_var_references(script.body, referenced)
847
870
  unused = sorted(defined - referenced)
871
+ lines: dict[str, int] = {}
872
+ if unused:
873
+ _definition_lines(script.body, script_dir, lines)
848
874
  for name in unused:
849
- line = _definition_line(script.body, name)
875
+ line = lines.get(name, 0)
850
876
  issues.append(
851
877
  _issue(
852
878
  "V002",
@@ -857,18 +883,15 @@ def _check_unused_variables(script: Script, defined: set[str], issues: list[_Iss
857
883
  )
858
884
 
859
885
 
860
- def _definition_line(nodes: list[Node], name: str) -> int:
861
- """Line of the SUB that defines *name*, or 0 when it cannot be located."""
886
+ def _definition_lines(nodes: list[Node], script_dir: Path | None, lines: dict[str, int]) -> None:
887
+ """Add to *lines* the line of the first metacommand that defines each variable, in script order."""
862
888
  for node in nodes:
863
889
  if isinstance(node, MetaCommandStatement):
864
890
  probe: set[str] = set()
865
- _extract_var_definition(node.command, None, probe)
866
- if name in probe:
867
- return node.span.start_line
868
- found = _definition_line(list(node.children()), name)
869
- if found:
870
- return found
871
- return 0
891
+ _extract_var_definition(node.command, script_dir, probe)
892
+ for name in probe:
893
+ lines.setdefault(name, node.span.start_line)
894
+ _definition_lines(list(node.children()), script_dir, lines)
872
895
 
873
896
 
874
897
  # ---------------------------------------------------------------------------
@@ -1008,7 +1031,7 @@ def lint(
1008
1031
  )
1009
1032
 
1010
1033
  # Pass 3: whole-script rules, which need every reference collected first
1011
- _check_unused_variables(script, all_defined, issues)
1034
+ _check_unused_variables(script, all_defined, issues, script_dir)
1012
1035
 
1013
1036
  return issues
1014
1037
 
@@ -17,7 +17,7 @@ from typing import Any, cast
17
17
  from execsql.db.base import Database, _fetched
18
18
  from execsql.db.tiers import SupportTier
19
19
  from execsql.exceptions import ErrInfo
20
- from execsql.utils.errors import exception_desc, fatal_error
20
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
21
21
  from execsql.utils.auth import (
22
22
  begin_login,
23
23
  clear_stored_password,
@@ -68,7 +68,7 @@ class AccessDatabase(Database):
68
68
  try:
69
69
  import pyodbc # noqa: F401
70
70
  except Exception:
71
- fatal_error("The pyodbc module is required. See http://github.com/mkleehammer/pyodbc")
71
+ fatal_error(f"The pyodbc module is required to connect to Access. {install_hint('odbc')}")
72
72
  from execsql.types import dbt_access
73
73
 
74
74
  super().__init__(
@@ -327,8 +327,8 @@ class AccessDatabase(Database):
327
327
  rows = curs.fetchall()
328
328
  return [d[0] for d in curs.description], rows
329
329
 
330
- def select_rowsource(self, sql: str) -> tuple[list[str], Any]:
331
- """Return column names and an iterable that yields rows one at a time."""
330
+ def select_rowsource(self, sql: str, *, decode_bytes: bool = True) -> tuple[list[str], Any]: # noqa: ARG002
331
+ """Return column names and an iterable that yields rows one at a time (values as the driver returns them)."""
332
332
  # Return 1) a list of column names, and 2) an iterable that yields rows.
333
333
  self.dao_flush_check()
334
334
  curs = self.cursor()
@@ -215,21 +215,18 @@ class Database(ABC):
215
215
  def quote_literal(self, value: Any) -> str:
216
216
  """Return *value* as a SQL string literal, safely escaped.
217
217
 
218
- Default ANSI/Postgres behaviour: wrap in single quotes, double
219
- embedded apostrophes, escape backslashes (so a value containing
220
- ``\\'`` cannot terminate the literal on MySQL default mode or
221
- PostgreSQL E-strings), and reject embedded NUL bytes (most
222
- wire protocols truncate or reject them).
223
-
224
- Override per-DBMS only when the wire protocol requires a different
225
- literal form.
218
+ Standard SQL: wrap in single quotes and double embedded apostrophes;
219
+ a backslash is an ordinary character. Embedded NUL bytes are
220
+ rejected (most wire protocols truncate or reject them). A DBMS
221
+ that reads a backslash in a literal as an escape (MySQL) overrides
222
+ this.
226
223
  """
227
224
  if value is None:
228
225
  return "NULL"
229
226
  s = str(value)
230
227
  if "\x00" in s:
231
228
  raise ValueError("SQL literal contains a NUL byte; refusing to quote.")
232
- return "'" + s.replace("\\", "\\\\").replace("'", "''") + "'"
229
+ return "'" + s.replace("'", "''") + "'"
233
230
 
234
231
  def paramsubs(self, paramcount: int) -> str:
235
232
  """Return a comma-separated string of *paramcount* parameter placeholders."""
@@ -405,8 +402,13 @@ class Database(ABC):
405
402
  rows = curs.fetchall()
406
403
  return [d[0] for d in curs.description], rows
407
404
 
408
- def select_rowsource(self, sql: str) -> tuple[list[str], Generator]:
409
- """Execute *sql* and return ``(column_names, row_generator)`` for streaming large result sets."""
405
+ def select_rowsource(self, sql: str, *, decode_bytes: bool = True) -> tuple[list[str], Generator]:
406
+ """Execute *sql* and return ``(column_names, row_generator)`` for streaming large result sets.
407
+
408
+ A binary value (``bytes``) is decoded to text with the connection's
409
+ encoding, as the exporters expect, unless *decode_bytes* is false:
410
+ COPY passes false so binary columns reach the target unchanged.
411
+ """
410
412
  # Return 1) a list of column names, and 2) an iterable that yields rows.
411
413
  curs = self.cursor()
412
414
  try:
@@ -433,7 +435,7 @@ class Database(ABC):
433
435
  break
434
436
  else:
435
437
  for row in rows:
436
- if self.encoding:
438
+ if self.encoding and decode_bytes:
437
439
  yield [
438
440
  c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c
439
441
  for c in row
@@ -501,6 +503,15 @@ class Database(ABC):
501
503
  """The schema (MySQL: database) an unqualified table name is created in, or ``None`` if unknown."""
502
504
  return None
503
505
 
506
+ def table_exists_where_created(self, table_name: str, schema_name: str | None = None) -> bool:
507
+ """Whether *table_name* exists where a ``CREATE TABLE`` on this connection would put it.
508
+
509
+ The same as :meth:`table_exists`, except on a connection that sees
510
+ tables of other databases (DuckDB ``ATTACH``): only the current
511
+ database counts there.
512
+ """
513
+ return self.table_exists(table_name, schema_name)
514
+
504
515
  def _query_current_schema(self, sql: str) -> str | None:
505
516
  with self._cursor() as curs:
506
517
  curs.execute(sql)
@@ -12,7 +12,7 @@ the CLI.
12
12
  from execsql.db.base import Database
13
13
  from execsql.db.tiers import SupportTier
14
14
  from execsql.exceptions import ErrInfo
15
- from execsql.utils.errors import exception_desc, fatal_error
15
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
16
16
  from execsql.utils.auth import (
17
17
  begin_login,
18
18
  clear_stored_password,
@@ -51,7 +51,7 @@ class DsnDatabase(Database):
51
51
  try:
52
52
  import pyodbc # noqa: F401
53
53
  except Exception:
54
- fatal_error("The pyodbc module is required. See http://github.com/mkleehammer/pyodbc")
54
+ fatal_error(f"The pyodbc module is required to connect through an ODBC DSN. {install_hint('odbc')}")
55
55
  from execsql.types import dbt_dsn
56
56
 
57
57
  super().__init__(