execsql2 2.27.0__tar.gz → 2.28.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 (486) hide show
  1. {execsql2-2.27.0 → execsql2-2.28.0}/CHANGELOG.md +23 -0
  2. {execsql2-2.27.0 → execsql2-2.28.0}/PKG-INFO +4 -2
  3. {execsql2-2.27.0 → execsql2-2.28.0}/README.md +1 -1
  4. {execsql2-2.27.0 → execsql2-2.28.0}/docs/about/divergence.md +3 -1
  5. {execsql2-2.27.0 → execsql2-2.28.0}/docs/getting-started/syntax.md +3 -2
  6. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/editors.md +4 -4
  7. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/formatter.md +11 -2
  8. {execsql2-2.27.0 → execsql2-2.28.0}/docs/reference/lint.md +1 -1
  9. {execsql2-2.27.0 → execsql2-2.28.0}/extras/vscode-execsql/README.md +2 -3
  10. {execsql2-2.27.0 → execsql2-2.28.0}/pyproject.toml +4 -2
  11. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/commands/format.py +2 -1
  12. execsql2-2.28.0/src/execsql/cli/commands/lsp.py +64 -0
  13. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/lint.py +45 -22
  14. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/access.py +4 -4
  15. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/base.py +17 -3
  16. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/dsn.py +2 -2
  17. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/duckdb.py +14 -2
  18. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/firebird.py +2 -3
  19. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/mysql.py +2 -2
  20. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/oracle.py +5 -5
  21. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/postgres.py +2 -3
  22. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/sqlserver.py +2 -2
  23. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/duckdb.py +2 -2
  24. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/ods.py +2 -2
  25. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/templates.py +2 -2
  26. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/xls.py +3 -3
  27. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/xlsx.py +2 -2
  28. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/yaml.py +3 -4
  29. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/format.py +71 -21
  30. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/importers/base.py +6 -10
  31. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/document.py +7 -3
  32. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/debug.py +2 -2
  33. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/io_fileops.py +6 -6
  34. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/upsert.py +2 -5
  35. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/models.py +11 -0
  36. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/errors.py +16 -0
  37. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_cli_surface.py +1 -1
  38. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_lint.py +19 -0
  39. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_copy_new.py +37 -0
  40. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_duckdb.py +3 -1
  41. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_new_table_checks.py +24 -1
  42. {execsql2-2.27.0 → execsql2-2.28.0}/tests/integration/test_postgres.py +26 -0
  43. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_document.py +10 -0
  44. execsql2-2.28.0/tests/lsp/test_lsp_command.py +71 -0
  45. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_server.py +9 -0
  46. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_fileops_extra.py +4 -2
  47. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_pg_upsert.py +1 -1
  48. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_format.py +60 -0
  49. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_errors.py +27 -0
  50. {execsql2-2.27.0 → execsql2-2.28.0}/uv.lock +5 -1
  51. execsql2-2.27.0/src/execsql/cli/commands/lsp.py +0 -40
  52. execsql2-2.27.0/tests/lsp/test_lsp_command.py +0 -32
  53. {execsql2-2.27.0 → execsql2-2.28.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  54. {execsql2-2.27.0 → execsql2-2.28.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  55. {execsql2-2.27.0 → execsql2-2.28.0}/.github/dependabot.yml +0 -0
  56. {execsql2-2.27.0 → execsql2-2.28.0}/.github/workflows/ci-cd.yml +0 -0
  57. {execsql2-2.27.0 → execsql2-2.28.0}/.gitignore +0 -0
  58. {execsql2-2.27.0 → execsql2-2.28.0}/.pre-commit-config.yaml +0 -0
  59. {execsql2-2.27.0 → execsql2-2.28.0}/.pre-commit-hooks.yaml +0 -0
  60. {execsql2-2.27.0 → execsql2-2.28.0}/.python-version +0 -0
  61. {execsql2-2.27.0 → execsql2-2.28.0}/.readthedocs.yaml +0 -0
  62. {execsql2-2.27.0 → execsql2-2.28.0}/CONTRIBUTING.md +0 -0
  63. {execsql2-2.27.0 → execsql2-2.28.0}/LICENSE.txt +0 -0
  64. {execsql2-2.27.0 → execsql2-2.28.0}/NOTICE +0 -0
  65. {execsql2-2.27.0 → execsql2-2.28.0}/SECURITY.md +0 -0
  66. {execsql2-2.27.0 → execsql2-2.28.0}/docs/about/contributors.md +0 -0
  67. {execsql2-2.27.0 → execsql2-2.28.0}/docs/about/copyright.md +0 -0
  68. {execsql2-2.27.0 → execsql2-2.28.0}/docs/api/cli.md +0 -0
  69. {execsql2-2.27.0 → execsql2-2.28.0}/docs/api/db.md +0 -0
  70. {execsql2-2.27.0 → execsql2-2.28.0}/docs/api/exporters.md +0 -0
  71. {execsql2-2.27.0 → execsql2-2.28.0}/docs/api/importers.md +0 -0
  72. {execsql2-2.27.0 → execsql2-2.28.0}/docs/api/index.md +0 -0
  73. {execsql2-2.27.0 → execsql2-2.28.0}/docs/api/metacommands.md +0 -0
  74. {execsql2-2.27.0 → execsql2-2.28.0}/docs/dev/adding_db_adapters.md +0 -0
  75. {execsql2-2.27.0 → execsql2-2.28.0}/docs/dev/adding_exporters.md +0 -0
  76. {execsql2-2.27.0 → execsql2-2.28.0}/docs/dev/adding_importers.md +0 -0
  77. {execsql2-2.27.0 → execsql2-2.28.0}/docs/dev/adding_metacommands.md +0 -0
  78. {execsql2-2.27.0 → execsql2-2.28.0}/docs/dev/architecture.md +0 -0
  79. {execsql2-2.27.0 → execsql2-2.28.0}/docs/dev/releasing.md +0 -0
  80. {execsql2-2.27.0 → execsql2-2.28.0}/docs/getting-started/installation.md +0 -0
  81. {execsql2-2.27.0 → execsql2-2.28.0}/docs/getting-started/requirements.md +0 -0
  82. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/debugging.md +0 -0
  83. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/documentation.md +0 -0
  84. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/encoding.md +0 -0
  85. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/examples.md +0 -0
  86. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/logging.md +0 -0
  87. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/sql_syntax.md +0 -0
  88. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/usage.md +0 -0
  89. {execsql2-2.27.0 → execsql2-2.28.0}/docs/guides/using_scripts.md +0 -0
  90. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/Compare_planets.png +0 -0
  91. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/actions.png +0 -0
  92. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/actions2.png +0 -0
  93. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/checkboxes.png +0 -0
  94. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/connect.b64 +0 -0
  95. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/connect.png +0 -0
  96. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/create_conf.png +0 -0
  97. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/data_error1_screenshot.jpg +0 -0
  98. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/entry_form.png +0 -0
  99. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/execsql_console.png +0 -0
  100. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/execsql_logo_01.png +0 -0
  101. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/fatals.png +0 -0
  102. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/logo_small.png +0 -0
  103. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/pause_terminal.png +0 -0
  104. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/pause_terminal_sm.b64 +0 -0
  105. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/pause_terminal_sm.png +0 -0
  106. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/prompt_compare.png +0 -0
  107. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/set_build_commands.jpg +0 -0
  108. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/unit_conversions.b64 +0 -0
  109. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/unit_conversions_029.png +0 -0
  110. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/unmatched.png +0 -0
  111. {execsql2-2.27.0 → execsql2-2.28.0}/docs/images/vim_execsql_highlight.png +0 -0
  112. {execsql2-2.27.0 → execsql2-2.28.0}/docs/index.md +0 -0
  113. {execsql2-2.27.0 → execsql2-2.28.0}/docs/reference/configuration.md +0 -0
  114. {execsql2-2.27.0 → execsql2-2.28.0}/docs/reference/metacommands.md +0 -0
  115. {execsql2-2.27.0 → execsql2-2.28.0}/docs/reference/security.md +0 -0
  116. {execsql2-2.27.0 → execsql2-2.28.0}/docs/reference/substitution_vars.md +0 -0
  117. {execsql2-2.27.0 → execsql2-2.28.0}/extras/plugin-template/README.md +0 -0
  118. {execsql2-2.27.0 → execsql2-2.28.0}/extras/plugin-template/pyproject.toml +0 -0
  119. {execsql2-2.27.0 → execsql2-2.28.0}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  120. {execsql2-2.27.0 → execsql2-2.28.0}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  121. {execsql2-2.27.0 → execsql2-2.28.0}/extras/vscode-execsql/.vscodeignore +0 -0
  122. {execsql2-2.27.0 → execsql2-2.28.0}/extras/vscode-execsql/LICENSE.txt +0 -0
  123. {execsql2-2.27.0 → execsql2-2.28.0}/extras/vscode-execsql/extension.js +0 -0
  124. {execsql2-2.27.0 → execsql2-2.28.0}/extras/vscode-execsql/icon.png +0 -0
  125. {execsql2-2.27.0 → execsql2-2.28.0}/extras/vscode-execsql/package-lock.json +0 -0
  126. {execsql2-2.27.0 → execsql2-2.28.0}/extras/vscode-execsql/package.json +0 -0
  127. {execsql2-2.27.0 → execsql2-2.28.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  128. {execsql2-2.27.0 → execsql2-2.28.0}/justfile +0 -0
  129. {execsql2-2.27.0 → execsql2-2.28.0}/scripts/benchmarks/README.md +0 -0
  130. {execsql2-2.27.0 → execsql2-2.28.0}/scripts/benchmarks/results/.gitignore +0 -0
  131. {execsql2-2.27.0 → execsql2-2.28.0}/scripts/benchmarks/run_benchmarks.py +0 -0
  132. {execsql2-2.27.0 → execsql2-2.28.0}/scripts/generate_vscode_grammar.py +0 -0
  133. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/__init__.py +0 -0
  134. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/__main__.py +0 -0
  135. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/api.py +0 -0
  136. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/__init__.py +0 -0
  137. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/application.py +0 -0
  138. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/commands/__init__.py +0 -0
  139. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/commands/config.py +0 -0
  140. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/commands/init.py +0 -0
  141. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/commands/lint.py +0 -0
  142. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/commands/listing.py +0 -0
  143. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/commands/run.py +0 -0
  144. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/dispatch.py +0 -0
  145. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/dsn.py +0 -0
  146. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/help.py +0 -0
  147. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/options.py +0 -0
  148. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/cli/run.py +0 -0
  149. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/config.py +0 -0
  150. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/config_validation.py +0 -0
  151. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/data/__init__.py +0 -0
  152. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/data/execsql.conf.template +0 -0
  153. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/data/script_header.sql +0 -0
  154. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/__init__.py +0 -0
  155. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/factory.py +0 -0
  156. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/sqlite.py +0 -0
  157. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/db/tiers.py +0 -0
  158. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/debug/__init__.py +0 -0
  159. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/debug/repl.py +0 -0
  160. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exceptions.py +0 -0
  161. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/__init__.py +0 -0
  162. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/base.py +0 -0
  163. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/delimited.py +0 -0
  164. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/feather.py +0 -0
  165. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/html.py +0 -0
  166. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/json.py +0 -0
  167. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/latex.py +0 -0
  168. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/markdown.py +0 -0
  169. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/parquet.py +0 -0
  170. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/pretty.py +0 -0
  171. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/protocol.py +0 -0
  172. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/raw.py +0 -0
  173. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/sqlite.py +0 -0
  174. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/values.py +0 -0
  175. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/xml.py +0 -0
  176. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/exporters/zip.py +0 -0
  177. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/gui/__init__.py +0 -0
  178. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/gui/base.py +0 -0
  179. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/gui/console.py +0 -0
  180. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/gui/desktop.py +0 -0
  181. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/gui/tui.py +0 -0
  182. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/importers/__init__.py +0 -0
  183. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/importers/csv.py +0 -0
  184. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/importers/feather.py +0 -0
  185. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/importers/json.py +0 -0
  186. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/importers/ods.py +0 -0
  187. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/importers/xls.py +0 -0
  188. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/interactive.py +0 -0
  189. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/__init__.py +0 -0
  190. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/code_actions.py +0 -0
  191. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/completion.py +0 -0
  192. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/diagnostics.py +0 -0
  193. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/folding.py +0 -0
  194. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/formatting.py +0 -0
  195. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/hover.py +0 -0
  196. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/navigation.py +0 -0
  197. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/rename.py +0 -0
  198. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/lsp/server.py +0 -0
  199. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/manifest.py +0 -0
  200. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/__init__.py +0 -0
  201. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/conditions.py +0 -0
  202. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/connect.py +0 -0
  203. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/control.py +0 -0
  204. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/data.py +0 -0
  205. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/dispatch.py +0 -0
  206. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/effects.py +0 -0
  207. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/io.py +0 -0
  208. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/io_export.py +0 -0
  209. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/io_import.py +0 -0
  210. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/io_write.py +0 -0
  211. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/prompt.py +0 -0
  212. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/reference.py +0 -0
  213. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/script_ext.py +0 -0
  214. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/metacommands/system.py +0 -0
  215. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/parser.py +0 -0
  216. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/plugins.py +0 -0
  217. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/py.typed +0 -0
  218. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/script/__init__.py +0 -0
  219. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/script/ast.py +0 -0
  220. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/script/control.py +0 -0
  221. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/script/engine.py +0 -0
  222. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/script/executor.py +0 -0
  223. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/script/parser.py +0 -0
  224. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/script/variables.py +0 -0
  225. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/state.py +0 -0
  226. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/state.pyi +0 -0
  227. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/types.py +0 -0
  228. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/__init__.py +0 -0
  229. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/auth.py +0 -0
  230. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/color.py +0 -0
  231. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/crypto.py +0 -0
  232. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/datetime.py +0 -0
  233. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/fileio.py +0 -0
  234. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/gui.py +0 -0
  235. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/mail.py +0 -0
  236. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/numeric.py +0 -0
  237. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/regex.py +0 -0
  238. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/strings.py +0 -0
  239. {execsql2-2.27.0 → execsql2-2.28.0}/src/execsql/utils/timer.py +0 -0
  240. {execsql2-2.27.0 → execsql2-2.28.0}/templates/README.md +0 -0
  241. {execsql2-2.27.0 → execsql2-2.28.0}/templates/config_settings.sqlite +0 -0
  242. {execsql2-2.27.0 → execsql2-2.28.0}/templates/example_config_prompt.sql +0 -0
  243. {execsql2-2.27.0 → execsql2-2.28.0}/templates/make_config_db.sql +0 -0
  244. {execsql2-2.27.0 → execsql2-2.28.0}/templates/md_compare.sql +0 -0
  245. {execsql2-2.27.0 → execsql2-2.28.0}/templates/md_glossary.sql +0 -0
  246. {execsql2-2.27.0 → execsql2-2.28.0}/templates/md_upsert.sql +0 -0
  247. {execsql2-2.27.0 → execsql2-2.28.0}/templates/pg_compare.sql +0 -0
  248. {execsql2-2.27.0 → execsql2-2.28.0}/templates/pg_glossary.sql +0 -0
  249. {execsql2-2.27.0 → execsql2-2.28.0}/templates/pg_upsert.sql +0 -0
  250. {execsql2-2.27.0 → execsql2-2.28.0}/templates/script_template.sql +0 -0
  251. {execsql2-2.27.0 → execsql2-2.28.0}/templates/ss_compare.sql +0 -0
  252. {execsql2-2.27.0 → execsql2-2.28.0}/templates/ss_glossary.sql +0 -0
  253. {execsql2-2.27.0 → execsql2-2.28.0}/templates/ss_upsert.sql +0 -0
  254. {execsql2-2.27.0 → execsql2-2.28.0}/tests/__init__.py +0 -0
  255. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/__init__.py +0 -0
  256. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_cli.py +0 -0
  257. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_cli_e2e.py +0 -0
  258. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_cli_run.py +0 -0
  259. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_commands.py +0 -0
  260. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_config_validate.py +0 -0
  261. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_dispatch.py +0 -0
  262. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_error_location.py +0 -0
  263. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_init.py +0 -0
  264. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_lint_fixtures.py +0 -0
  265. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_log_exit_record.py +0 -0
  266. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_manifest.py +0 -0
  267. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_ping.py +0 -0
  268. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_profile.py +0 -0
  269. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_sigterm.py +0 -0
  270. {execsql2-2.27.0 → execsql2-2.28.0}/tests/cli/test_undeclared_imports.py +0 -0
  271. {execsql2-2.27.0 → execsql2-2.28.0}/tests/conftest.py +0 -0
  272. {execsql2-2.27.0 → execsql2-2.28.0}/tests/data/formatter_shapes/comment_anchoring.sql +0 -0
  273. {execsql2-2.27.0 → execsql2-2.28.0}/tests/data/formatter_shapes/escapes_and_literals.sql +0 -0
  274. {execsql2-2.27.0 → execsql2-2.28.0}/tests/data/formatter_shapes/substitution_and_metacommands.sql +0 -0
  275. {execsql2-2.27.0 → execsql2-2.28.0}/tests/data/formatter_shapes/unparsable_sql.sql +0 -0
  276. {execsql2-2.27.0 → execsql2-2.28.0}/tests/data/lint/conditions_and_blocks.sql +0 -0
  277. {execsql2-2.27.0 → execsql2-2.28.0}/tests/data/lint/does_not_parse.sql +0 -0
  278. {execsql2-2.27.0 → execsql2-2.28.0}/tests/data/lint/every_metacommand.sql +0 -0
  279. {execsql2-2.27.0 → execsql2-2.28.0}/tests/data/lint/invalid.sql +0 -0
  280. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/__init__.py +0 -0
  281. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_access_windows.py +0 -0
  282. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_base.py +0 -0
  283. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_db_adapters_mocked.py +0 -0
  284. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  285. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_dsn.py +0 -0
  286. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_factory.py +0 -0
  287. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_factory_postgres_encoding.py +0 -0
  288. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_import_atomic.py +0 -0
  289. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_import_replacement_quoted.py +0 -0
  290. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_import_short_rows.py +0 -0
  291. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_import_timezones.py +0 -0
  292. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_mysql_case_folding.py +0 -0
  293. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_mysql_inprocess.py +0 -0
  294. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_oracle_driver.py +0 -0
  295. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_postgres.py +0 -0
  296. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_postgres_inprocess.py +0 -0
  297. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_roundtrip_adapters.py +0 -0
  298. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_sqlite.py +0 -0
  299. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_sqlite_extra.py +0 -0
  300. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_sqlserver_inprocess.py +0 -0
  301. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_tiers.py +0 -0
  302. {execsql2-2.27.0 → execsql2-2.28.0}/tests/db/test_transactions.py +0 -0
  303. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/__init__.py +0 -0
  304. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/conftest.py +0 -0
  305. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_base.py +0 -0
  306. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_db.py +0 -0
  307. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_delimited.py +0 -0
  308. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  309. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_exporters.py +0 -0
  310. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_feather.py +0 -0
  311. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_html_extended.py +0 -0
  312. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_html_latex.py +0 -0
  313. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_json.py +0 -0
  314. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_json_extended.py +0 -0
  315. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_latex_extended.py +0 -0
  316. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_markdown.py +0 -0
  317. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_ods.py +0 -0
  318. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_ods_export.py +0 -0
  319. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_parquet.py +0 -0
  320. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_pretty_extended.py +0 -0
  321. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_raw_extended.py +0 -0
  322. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_columnar.py +0 -0
  323. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_dbfile.py +0 -0
  324. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_delimited.py +0 -0
  325. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_json.py +0 -0
  326. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_latex_xml.py +0 -0
  327. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_markup.py +0 -0
  328. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_ods.py +0 -0
  329. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_pretty_raw.py +0 -0
  330. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_template.py +0 -0
  331. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_values.py +0 -0
  332. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_roundtrip_xlsx.py +0 -0
  333. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  334. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_templates.py +0 -0
  335. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_templates_extended.py +0 -0
  336. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_values_extended.py +0 -0
  337. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_xls_xlsx.py +0 -0
  338. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_xlsx.py +0 -0
  339. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_xml.py +0 -0
  340. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_yaml.py +0 -0
  341. {execsql2-2.27.0 → execsql2-2.28.0}/tests/exporters/test_zip.py +0 -0
  342. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/__init__.py +0 -0
  343. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_backends.py +0 -0
  344. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_backends_extended.py +0 -0
  345. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_compare_stats.py +0 -0
  346. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_compute_row_diffs.py +0 -0
  347. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_desktop_dialogs.py +0 -0
  348. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_format_table.py +0 -0
  349. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_tui_pilot.py +0 -0
  350. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_tui_pilot_complex.py +0 -0
  351. {execsql2-2.27.0 → execsql2-2.28.0}/tests/gui/test_utils_gui_extended.py +0 -0
  352. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/__init__.py +0 -0
  353. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/conftest.py +0 -0
  354. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_base_extended.py +0 -0
  355. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_csv_blank_lines.py +0 -0
  356. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_csv_edge_cases.py +0 -0
  357. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_csv_importer.py +0 -0
  358. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_feather_importer.py +0 -0
  359. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_json_importer.py +0 -0
  360. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_ods_importer.py +0 -0
  361. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_roundtrip_csv.py +0 -0
  362. {execsql2-2.27.0 → execsql2-2.28.0}/tests/importers/test_xls_importer.py +0 -0
  363. {execsql2-2.27.0 → execsql2-2.28.0}/tests/integration/__init__.py +0 -0
  364. {execsql2-2.27.0 → execsql2-2.28.0}/tests/integration/conftest.py +0 -0
  365. {execsql2-2.27.0 → execsql2-2.28.0}/tests/integration/test_dsn.py +0 -0
  366. {execsql2-2.27.0 → execsql2-2.28.0}/tests/integration/test_duckdb.py +0 -0
  367. {execsql2-2.27.0 → execsql2-2.28.0}/tests/integration/test_mysql.py +0 -0
  368. {execsql2-2.27.0 → execsql2-2.28.0}/tests/integration/test_sqlite.py +0 -0
  369. {execsql2-2.27.0 → execsql2-2.28.0}/tests/live_db.py +0 -0
  370. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/__init__.py +0 -0
  371. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_code_actions.py +0 -0
  372. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_completion.py +0 -0
  373. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_diagnostics.py +0 -0
  374. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_folding.py +0 -0
  375. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_formatting.py +0 -0
  376. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_hover.py +0 -0
  377. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_navigation.py +0 -0
  378. {execsql2-2.27.0 → execsql2-2.28.0}/tests/lsp/test_rename.py +0 -0
  379. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/__init__.py +0 -0
  380. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_assert.py +0 -0
  381. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_breakpoint.py +0 -0
  382. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_conditions_extra.py +0 -0
  383. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_connect.py +0 -0
  384. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_io_export.py +0 -0
  385. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_io_import.py +0 -0
  386. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands.py +0 -0
  387. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  388. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_data.py +0 -0
  389. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_extended.py +0 -0
  390. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_io.py +0 -0
  391. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  392. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  393. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_system.py +0 -0
  394. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  395. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_prompt.py +0 -0
  396. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_row_count.py +0 -0
  397. {execsql2-2.27.0 → execsql2-2.28.0}/tests/metacommands/test_show_scripts.py +0 -0
  398. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/__init__.py +0 -0
  399. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  400. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  401. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  402. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/audit_smoke.sql +0 -0
  403. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/config_runtime.sql +0 -0
  404. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/control_flow.sql +0 -0
  405. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  406. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/error_handling.sql +0 -0
  407. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  408. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/import_options.sql +0 -0
  409. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/includes/helper.sql +0 -0
  410. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/includes.sql +0 -0
  411. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/io_formats.sql +0 -0
  412. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  413. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/multi_db.sql +0 -0
  414. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  415. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  416. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/predicates.sql +0 -0
  417. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/scripts.sql +0 -0
  418. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/smoke.sql +0 -0
  419. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  420. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  421. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  422. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/template_export.sql +0 -0
  423. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/timer.sql +0 -0
  424. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/transactions.sql +0 -0
  425. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/write_create_table.sql +0 -0
  426. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  427. {execsql2-2.27.0 → execsql2-2.28.0}/tests/scripts/test_sql_scripts.py +0 -0
  428. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/__init__.py +0 -0
  429. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_env_detection.py +0 -0
  430. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_expansion_bomb.py +0 -0
  431. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_import_regex_hardening.py +0 -0
  432. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_log_redaction.py +0 -0
  433. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_log_secrets.py +0 -0
  434. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_odbc_injection.py +0 -0
  435. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_path_containment.py +0 -0
  436. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_substitution_injection.py +0 -0
  437. {execsql2-2.27.0 → execsql2-2.28.0}/tests/security/test_zip_bomb.py +0 -0
  438. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_api.py +0 -0
  439. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_api_errors.py +0 -0
  440. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_api_extended.py +0 -0
  441. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_api_filewriter.py +0 -0
  442. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_api_isolation.py +0 -0
  443. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_api_subprocess.py +0 -0
  444. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_ast.py +0 -0
  445. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_ast_parser.py +0 -0
  446. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_bundled_templates.py +0 -0
  447. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_config.py +0 -0
  448. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_config_data.py +0 -0
  449. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_config_extended.py +0 -0
  450. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_date_inference.py +0 -0
  451. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_debug_repl.py +0 -0
  452. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_engine.py +0 -0
  453. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_error_messages.py +0 -0
  454. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_exceptions.py +0 -0
  455. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_executor.py +0 -0
  456. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_executor_inprocess.py +0 -0
  457. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_interactive.py +0 -0
  458. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_mail.py +0 -0
  459. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_models.py +0 -0
  460. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_package.py +0 -0
  461. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_parser.py +0 -0
  462. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_parser_params.py +0 -0
  463. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_plugins.py +0 -0
  464. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_reference.py +0 -0
  465. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_registry.py +0 -0
  466. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_script.py +0 -0
  467. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_state.py +0 -0
  468. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_tooling_versions.py +0 -0
  469. {execsql2-2.27.0 → execsql2-2.28.0}/tests/test_types.py +0 -0
  470. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/__init__.py +0 -0
  471. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_auth.py +0 -0
  472. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_auth_extra.py +0 -0
  473. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_crypto.py +0 -0
  474. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_datetime.py +0 -0
  475. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_encodedfile_context.py +0 -0
  476. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_errors_extra.py +0 -0
  477. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_fileio.py +0 -0
  478. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_fileio_extra.py +0 -0
  479. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_keyring_rules.py +0 -0
  480. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_mail_tls.py +0 -0
  481. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_numeric.py +0 -0
  482. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_regex.py +0 -0
  483. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_strings.py +0 -0
  484. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_timer.py +0 -0
  485. {execsql2-2.27.0 → execsql2-2.28.0}/tests/utils/test_timer_extra.py +0 -0
  486. {execsql2-2.27.0 → execsql2-2.28.0}/zensical.toml +0 -0
@@ -13,6 +13,29 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.28.0] - 2026-10-13
17
+
18
+ ### Added
19
+
20
+ - **`execsql lsp --check`** reports whether the language server can start (exit 0) or what is missing (exit 1).
21
+
22
+ ### Changed
23
+
24
+ - **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.
25
+
26
+ ### Fixed
27
+
28
+ - **`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.
29
+ - **`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.
30
+ - **`execsql format`:** a statement kept as written after another on the same line no longer starts with a space.
31
+ - **`execsql format` without sqlglot** printed its `pip install` hint without the `[formatter]` extra.
32
+ - **`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.
33
+ - **`execsql lsp` with an incompatible pygls** names the installed pygls version instead of saying the `lsp` extra is missing.
34
+ - **`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.
35
+ - **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.
36
+
37
+ ______________________________________________________________________
38
+
16
39
  ## [2.27.0] - 2026-10-12
17
40
 
18
41
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.27.0
3
+ Version: 2.28.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.28.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.28.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,11 @@ 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. |
363
365
  | 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
366
  | `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
367
  | `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. |
368
+ | `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
369
  | 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
370
  | 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
371
  | `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.28.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.28.0
162
162
  hooks:
163
163
  - id: execsql-lint
164
164
  args: [--ignore, V002] # optional: any execsql lint option
@@ -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.28.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.28.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()
@@ -405,8 +405,13 @@ class Database(ABC):
405
405
  rows = curs.fetchall()
406
406
  return [d[0] for d in curs.description], rows
407
407
 
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."""
408
+ def select_rowsource(self, sql: str, *, decode_bytes: bool = True) -> tuple[list[str], Generator]:
409
+ """Execute *sql* and return ``(column_names, row_generator)`` for streaming large result sets.
410
+
411
+ A binary value (``bytes``) is decoded to text with the connection's
412
+ encoding, as the exporters expect, unless *decode_bytes* is false:
413
+ COPY passes false so binary columns reach the target unchanged.
414
+ """
410
415
  # Return 1) a list of column names, and 2) an iterable that yields rows.
411
416
  curs = self.cursor()
412
417
  try:
@@ -433,7 +438,7 @@ class Database(ABC):
433
438
  break
434
439
  else:
435
440
  for row in rows:
436
- if self.encoding:
441
+ if self.encoding and decode_bytes:
437
442
  yield [
438
443
  c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c
439
444
  for c in row
@@ -501,6 +506,15 @@ class Database(ABC):
501
506
  """The schema (MySQL: database) an unqualified table name is created in, or ``None`` if unknown."""
502
507
  return None
503
508
 
509
+ def table_exists_where_created(self, table_name: str, schema_name: str | None = None) -> bool:
510
+ """Whether *table_name* exists where a ``CREATE TABLE`` on this connection would put it.
511
+
512
+ The same as :meth:`table_exists`, except on a connection that sees
513
+ tables of other databases (DuckDB ``ATTACH``): only the current
514
+ database counts there.
515
+ """
516
+ return self.table_exists(table_name, schema_name)
517
+
504
518
  def _query_current_schema(self, sql: str) -> str | None:
505
519
  with self._cursor() as curs:
506
520
  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__(
@@ -14,7 +14,7 @@ from typing import Any
14
14
 
15
15
  from execsql.db.base import Database, statement_start
16
16
  from execsql.exceptions import ErrInfo
17
- from execsql.utils.errors import exception_desc, fatal_error
17
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
18
18
  import execsql.state as _state
19
19
 
20
20
  __all__ = ["DuckDBDatabase"]
@@ -62,7 +62,7 @@ class DuckDBDatabase(Database):
62
62
  try:
63
63
  import duckdb # noqa: F401
64
64
  except Exception:
65
- fatal_error("The duckdb module is required.")
65
+ fatal_error(f"The duckdb module is required to use DuckDB. {install_hint('duckdb')}")
66
66
  from execsql.types import dbt_duckdb
67
67
 
68
68
  super().__init__(
@@ -167,6 +167,18 @@ class DuckDBDatabase(Database):
167
167
  self.rollback()
168
168
  raise
169
169
 
170
+ def table_exists_where_created(self, table_name: str, schema_name: str | None = None) -> bool:
171
+ """Whether *table_name* exists in the current database (not an ``ATTACH``ed one)."""
172
+ # information_schema.tables lists the tables of every attached database.
173
+ sql = "select table_name from information_schema.tables where table_name = ? and table_catalog = current_database()"
174
+ params: list = [table_name]
175
+ if schema_name:
176
+ sql += " and table_schema = ?"
177
+ params.append(schema_name)
178
+ with self._cursor() as curs:
179
+ curs.execute(sql, params)
180
+ return len(curs.fetchall()) > 0
181
+
170
182
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
171
183
  """Return True if the named view exists in the DuckDB database."""
172
184
  # DuckDB information_schema has no 'views' table; views are listed in 'tables'
@@ -11,7 +11,7 @@ via the ``firebird-driver`` package. Corresponds to ``-t f`` on the CLI.
11
11
  from execsql.db.base import Database
12
12
  from execsql.db.tiers import SupportTier
13
13
  from execsql.exceptions import ErrInfo
14
- from execsql.utils.errors import exception_desc, fatal_error
14
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
15
15
  from execsql.utils.auth import (
16
16
  begin_login,
17
17
  clear_stored_password,
@@ -46,8 +46,7 @@ class FirebirdDatabase(Database):
46
46
  from firebird import driver as firebird_lib # noqa: F401
47
47
  except Exception:
48
48
  fatal_error(
49
- "The firebird-driver module is required to connect to Firebird. "
50
- "See https://pypi.org/project/firebird-driver/",
49
+ f"The firebird-driver module is required to connect to Firebird. {install_hint('firebird')}",
51
50
  )
52
51
  from execsql.types import dbt_firebird
53
52
 
@@ -12,7 +12,7 @@ from typing import Any, cast
12
12
 
13
13
  from execsql.db.base import Database
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,
@@ -81,7 +81,7 @@ class MySQLDatabase(Database):
81
81
  import pymysql as mysql_lib # noqa: F401
82
82
  except Exception:
83
83
  fatal_error(
84
- "The pymysql module is required to connect to MySQL. See https://pypi.python.org/pypi/PyMySQL",
84
+ f"The pymysql module is required to connect to MySQL. {install_hint('mysql')}",
85
85
  )
86
86
  from execsql.types import dbt_mysql
87
87
 
@@ -14,7 +14,7 @@ from typing import Any
14
14
  from execsql.db.base import Database
15
15
  from execsql.db.tiers import SupportTier
16
16
  from execsql.exceptions import ErrInfo
17
- from execsql.utils.errors import exception_desc, fatal_error
17
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
18
18
  from execsql.utils.auth import (
19
19
  begin_login,
20
20
  clear_stored_password,
@@ -76,7 +76,7 @@ class OracleDatabase(Database):
76
76
  ) -> None:
77
77
  if _oracle_driver() is None:
78
78
  fatal_error(
79
- 'The oracledb module is required to connect to Oracle. Install it with: pip install "execsql2[oracle]"',
79
+ f"The oracledb module is required to connect to Oracle. {install_hint('oracle')}",
80
80
  )
81
81
  from execsql.types import dbt_oracle
82
82
 
@@ -164,12 +164,12 @@ class OracleDatabase(Database):
164
164
  else:
165
165
  return super().select_data(sql)
166
166
 
167
- def select_rowsource(self, sql: str) -> Any:
167
+ def select_rowsource(self, sql: str, *, decode_bytes: bool = True) -> Any:
168
168
  """Return column names and an iterable that yields rows one at a time."""
169
169
  if sql[-1:] == ";":
170
- return super().select_rowsource(sql[:-1])
170
+ return super().select_rowsource(sql[:-1], decode_bytes=decode_bytes)
171
171
  else:
172
- return super().select_rowsource(sql)
172
+ return super().select_rowsource(sql, decode_bytes=decode_bytes)
173
173
 
174
174
  def select_rowdict(self, sql: str) -> Any:
175
175
  """Return column names and an iterable that yields rows as dictionaries."""