execsql2 2.28.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 (485) hide show
  1. {execsql2-2.28.0 → execsql2-2.29.0}/CHANGELOG.md +15 -0
  2. {execsql2-2.28.0 → execsql2-2.29.0}/PKG-INFO +2 -2
  3. {execsql2-2.28.0 → execsql2-2.29.0}/README.md +1 -1
  4. {execsql2-2.28.0 → execsql2-2.29.0}/docs/about/divergence.md +5 -0
  5. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/formatter.md +1 -1
  6. {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/lint.md +1 -1
  7. {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/metacommands.md +2 -0
  8. {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/security.md +1 -1
  9. {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/substitution_vars.md +1 -1
  10. {execsql2-2.28.0 → execsql2-2.29.0}/pyproject.toml +2 -2
  11. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/base.py +6 -9
  12. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/mysql.py +19 -3
  13. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/postgres.py +1 -1
  14. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/xls.py +3 -2
  15. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/variables.py +42 -10
  16. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/types.py +8 -0
  17. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_postgres_inprocess.py +3 -0
  18. execsql2-2.29.0/tests/db/test_quoted_substitution_roundtrip.py +40 -0
  19. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_xls_importer.py +32 -0
  20. {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_mysql.py +56 -0
  21. {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_postgres.py +56 -0
  22. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke.sql +3 -3
  23. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_substitution_injection.py +64 -20
  24. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_types.py +17 -0
  25. {execsql2-2.28.0 → execsql2-2.29.0}/uv.lock +1 -1
  26. {execsql2-2.28.0 → execsql2-2.29.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  27. {execsql2-2.28.0 → execsql2-2.29.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  28. {execsql2-2.28.0 → execsql2-2.29.0}/.github/dependabot.yml +0 -0
  29. {execsql2-2.28.0 → execsql2-2.29.0}/.github/workflows/ci-cd.yml +0 -0
  30. {execsql2-2.28.0 → execsql2-2.29.0}/.gitignore +0 -0
  31. {execsql2-2.28.0 → execsql2-2.29.0}/.pre-commit-config.yaml +0 -0
  32. {execsql2-2.28.0 → execsql2-2.29.0}/.pre-commit-hooks.yaml +0 -0
  33. {execsql2-2.28.0 → execsql2-2.29.0}/.python-version +0 -0
  34. {execsql2-2.28.0 → execsql2-2.29.0}/.readthedocs.yaml +0 -0
  35. {execsql2-2.28.0 → execsql2-2.29.0}/CONTRIBUTING.md +0 -0
  36. {execsql2-2.28.0 → execsql2-2.29.0}/LICENSE.txt +0 -0
  37. {execsql2-2.28.0 → execsql2-2.29.0}/NOTICE +0 -0
  38. {execsql2-2.28.0 → execsql2-2.29.0}/SECURITY.md +0 -0
  39. {execsql2-2.28.0 → execsql2-2.29.0}/docs/about/contributors.md +0 -0
  40. {execsql2-2.28.0 → execsql2-2.29.0}/docs/about/copyright.md +0 -0
  41. {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/cli.md +0 -0
  42. {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/db.md +0 -0
  43. {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/exporters.md +0 -0
  44. {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/importers.md +0 -0
  45. {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/index.md +0 -0
  46. {execsql2-2.28.0 → execsql2-2.29.0}/docs/api/metacommands.md +0 -0
  47. {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/adding_db_adapters.md +0 -0
  48. {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/adding_exporters.md +0 -0
  49. {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/adding_importers.md +0 -0
  50. {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/adding_metacommands.md +0 -0
  51. {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/architecture.md +0 -0
  52. {execsql2-2.28.0 → execsql2-2.29.0}/docs/dev/releasing.md +0 -0
  53. {execsql2-2.28.0 → execsql2-2.29.0}/docs/getting-started/installation.md +0 -0
  54. {execsql2-2.28.0 → execsql2-2.29.0}/docs/getting-started/requirements.md +0 -0
  55. {execsql2-2.28.0 → execsql2-2.29.0}/docs/getting-started/syntax.md +0 -0
  56. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/debugging.md +0 -0
  57. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/documentation.md +0 -0
  58. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/editors.md +0 -0
  59. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/encoding.md +0 -0
  60. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/examples.md +0 -0
  61. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/logging.md +0 -0
  62. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/sql_syntax.md +0 -0
  63. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/usage.md +0 -0
  64. {execsql2-2.28.0 → execsql2-2.29.0}/docs/guides/using_scripts.md +0 -0
  65. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/Compare_planets.png +0 -0
  66. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/actions.png +0 -0
  67. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/actions2.png +0 -0
  68. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/checkboxes.png +0 -0
  69. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/connect.b64 +0 -0
  70. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/connect.png +0 -0
  71. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/create_conf.png +0 -0
  72. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/data_error1_screenshot.jpg +0 -0
  73. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/entry_form.png +0 -0
  74. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/execsql_console.png +0 -0
  75. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/execsql_logo_01.png +0 -0
  76. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/fatals.png +0 -0
  77. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/logo_small.png +0 -0
  78. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/pause_terminal.png +0 -0
  79. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/pause_terminal_sm.b64 +0 -0
  80. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/pause_terminal_sm.png +0 -0
  81. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/prompt_compare.png +0 -0
  82. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/set_build_commands.jpg +0 -0
  83. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/unit_conversions.b64 +0 -0
  84. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/unit_conversions_029.png +0 -0
  85. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/unmatched.png +0 -0
  86. {execsql2-2.28.0 → execsql2-2.29.0}/docs/images/vim_execsql_highlight.png +0 -0
  87. {execsql2-2.28.0 → execsql2-2.29.0}/docs/index.md +0 -0
  88. {execsql2-2.28.0 → execsql2-2.29.0}/docs/reference/configuration.md +0 -0
  89. {execsql2-2.28.0 → execsql2-2.29.0}/extras/plugin-template/README.md +0 -0
  90. {execsql2-2.28.0 → execsql2-2.29.0}/extras/plugin-template/pyproject.toml +0 -0
  91. {execsql2-2.28.0 → execsql2-2.29.0}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  92. {execsql2-2.28.0 → execsql2-2.29.0}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  93. {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/.vscodeignore +0 -0
  94. {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/LICENSE.txt +0 -0
  95. {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/README.md +0 -0
  96. {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/extension.js +0 -0
  97. {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/icon.png +0 -0
  98. {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/package-lock.json +0 -0
  99. {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/package.json +0 -0
  100. {execsql2-2.28.0 → execsql2-2.29.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  101. {execsql2-2.28.0 → execsql2-2.29.0}/justfile +0 -0
  102. {execsql2-2.28.0 → execsql2-2.29.0}/scripts/benchmarks/README.md +0 -0
  103. {execsql2-2.28.0 → execsql2-2.29.0}/scripts/benchmarks/results/.gitignore +0 -0
  104. {execsql2-2.28.0 → execsql2-2.29.0}/scripts/benchmarks/run_benchmarks.py +0 -0
  105. {execsql2-2.28.0 → execsql2-2.29.0}/scripts/generate_vscode_grammar.py +0 -0
  106. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/__init__.py +0 -0
  107. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/__main__.py +0 -0
  108. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/api.py +0 -0
  109. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/__init__.py +0 -0
  110. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/application.py +0 -0
  111. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/__init__.py +0 -0
  112. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/config.py +0 -0
  113. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/format.py +0 -0
  114. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/init.py +0 -0
  115. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/lint.py +0 -0
  116. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/listing.py +0 -0
  117. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/lsp.py +0 -0
  118. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/commands/run.py +0 -0
  119. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/dispatch.py +0 -0
  120. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/dsn.py +0 -0
  121. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/help.py +0 -0
  122. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/lint.py +0 -0
  123. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/options.py +0 -0
  124. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/cli/run.py +0 -0
  125. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/config.py +0 -0
  126. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/config_validation.py +0 -0
  127. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/data/__init__.py +0 -0
  128. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/data/execsql.conf.template +0 -0
  129. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/data/script_header.sql +0 -0
  130. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/__init__.py +0 -0
  131. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/access.py +0 -0
  132. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/dsn.py +0 -0
  133. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/duckdb.py +0 -0
  134. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/factory.py +0 -0
  135. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/firebird.py +0 -0
  136. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/oracle.py +0 -0
  137. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/sqlite.py +0 -0
  138. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/sqlserver.py +0 -0
  139. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/db/tiers.py +0 -0
  140. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/debug/__init__.py +0 -0
  141. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/debug/repl.py +0 -0
  142. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exceptions.py +0 -0
  143. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/__init__.py +0 -0
  144. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/base.py +0 -0
  145. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/delimited.py +0 -0
  146. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/duckdb.py +0 -0
  147. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/feather.py +0 -0
  148. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/html.py +0 -0
  149. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/json.py +0 -0
  150. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/latex.py +0 -0
  151. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/markdown.py +0 -0
  152. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/ods.py +0 -0
  153. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/parquet.py +0 -0
  154. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/pretty.py +0 -0
  155. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/protocol.py +0 -0
  156. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/raw.py +0 -0
  157. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/sqlite.py +0 -0
  158. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/templates.py +0 -0
  159. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/values.py +0 -0
  160. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/xlsx.py +0 -0
  161. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/xml.py +0 -0
  162. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/yaml.py +0 -0
  163. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/exporters/zip.py +0 -0
  164. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/format.py +0 -0
  165. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/__init__.py +0 -0
  166. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/base.py +0 -0
  167. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/console.py +0 -0
  168. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/desktop.py +0 -0
  169. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/gui/tui.py +0 -0
  170. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/__init__.py +0 -0
  171. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/base.py +0 -0
  172. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/csv.py +0 -0
  173. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/feather.py +0 -0
  174. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/json.py +0 -0
  175. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/ods.py +0 -0
  176. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/importers/xls.py +0 -0
  177. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/interactive.py +0 -0
  178. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/__init__.py +0 -0
  179. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/code_actions.py +0 -0
  180. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/completion.py +0 -0
  181. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/diagnostics.py +0 -0
  182. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/document.py +0 -0
  183. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/folding.py +0 -0
  184. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/formatting.py +0 -0
  185. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/hover.py +0 -0
  186. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/navigation.py +0 -0
  187. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/rename.py +0 -0
  188. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/lsp/server.py +0 -0
  189. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/manifest.py +0 -0
  190. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/__init__.py +0 -0
  191. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/conditions.py +0 -0
  192. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/connect.py +0 -0
  193. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/control.py +0 -0
  194. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/data.py +0 -0
  195. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/debug.py +0 -0
  196. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/dispatch.py +0 -0
  197. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/effects.py +0 -0
  198. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io.py +0 -0
  199. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io_export.py +0 -0
  200. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io_fileops.py +0 -0
  201. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io_import.py +0 -0
  202. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/io_write.py +0 -0
  203. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/prompt.py +0 -0
  204. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/reference.py +0 -0
  205. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/script_ext.py +0 -0
  206. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/system.py +0 -0
  207. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/metacommands/upsert.py +0 -0
  208. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/models.py +0 -0
  209. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/parser.py +0 -0
  210. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/plugins.py +0 -0
  211. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/py.typed +0 -0
  212. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/__init__.py +0 -0
  213. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/ast.py +0 -0
  214. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/control.py +0 -0
  215. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/engine.py +0 -0
  216. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/executor.py +0 -0
  217. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/script/parser.py +0 -0
  218. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/state.py +0 -0
  219. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/state.pyi +0 -0
  220. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/__init__.py +0 -0
  221. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/auth.py +0 -0
  222. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/color.py +0 -0
  223. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/crypto.py +0 -0
  224. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/datetime.py +0 -0
  225. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/errors.py +0 -0
  226. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/fileio.py +0 -0
  227. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/gui.py +0 -0
  228. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/mail.py +0 -0
  229. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/numeric.py +0 -0
  230. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/regex.py +0 -0
  231. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/strings.py +0 -0
  232. {execsql2-2.28.0 → execsql2-2.29.0}/src/execsql/utils/timer.py +0 -0
  233. {execsql2-2.28.0 → execsql2-2.29.0}/templates/README.md +0 -0
  234. {execsql2-2.28.0 → execsql2-2.29.0}/templates/config_settings.sqlite +0 -0
  235. {execsql2-2.28.0 → execsql2-2.29.0}/templates/example_config_prompt.sql +0 -0
  236. {execsql2-2.28.0 → execsql2-2.29.0}/templates/make_config_db.sql +0 -0
  237. {execsql2-2.28.0 → execsql2-2.29.0}/templates/md_compare.sql +0 -0
  238. {execsql2-2.28.0 → execsql2-2.29.0}/templates/md_glossary.sql +0 -0
  239. {execsql2-2.28.0 → execsql2-2.29.0}/templates/md_upsert.sql +0 -0
  240. {execsql2-2.28.0 → execsql2-2.29.0}/templates/pg_compare.sql +0 -0
  241. {execsql2-2.28.0 → execsql2-2.29.0}/templates/pg_glossary.sql +0 -0
  242. {execsql2-2.28.0 → execsql2-2.29.0}/templates/pg_upsert.sql +0 -0
  243. {execsql2-2.28.0 → execsql2-2.29.0}/templates/script_template.sql +0 -0
  244. {execsql2-2.28.0 → execsql2-2.29.0}/templates/ss_compare.sql +0 -0
  245. {execsql2-2.28.0 → execsql2-2.29.0}/templates/ss_glossary.sql +0 -0
  246. {execsql2-2.28.0 → execsql2-2.29.0}/templates/ss_upsert.sql +0 -0
  247. {execsql2-2.28.0 → execsql2-2.29.0}/tests/__init__.py +0 -0
  248. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/__init__.py +0 -0
  249. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_cli.py +0 -0
  250. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_cli_e2e.py +0 -0
  251. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_cli_run.py +0 -0
  252. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_cli_surface.py +0 -0
  253. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_commands.py +0 -0
  254. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_config_validate.py +0 -0
  255. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_dispatch.py +0 -0
  256. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_error_location.py +0 -0
  257. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_init.py +0 -0
  258. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_lint.py +0 -0
  259. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_lint_fixtures.py +0 -0
  260. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_log_exit_record.py +0 -0
  261. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_manifest.py +0 -0
  262. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_ping.py +0 -0
  263. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_profile.py +0 -0
  264. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_sigterm.py +0 -0
  265. {execsql2-2.28.0 → execsql2-2.29.0}/tests/cli/test_undeclared_imports.py +0 -0
  266. {execsql2-2.28.0 → execsql2-2.29.0}/tests/conftest.py +0 -0
  267. {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/formatter_shapes/comment_anchoring.sql +0 -0
  268. {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/formatter_shapes/escapes_and_literals.sql +0 -0
  269. {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/formatter_shapes/substitution_and_metacommands.sql +0 -0
  270. {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/formatter_shapes/unparsable_sql.sql +0 -0
  271. {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/lint/conditions_and_blocks.sql +0 -0
  272. {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/lint/does_not_parse.sql +0 -0
  273. {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/lint/every_metacommand.sql +0 -0
  274. {execsql2-2.28.0 → execsql2-2.29.0}/tests/data/lint/invalid.sql +0 -0
  275. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/__init__.py +0 -0
  276. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_access_windows.py +0 -0
  277. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_base.py +0 -0
  278. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_copy_new.py +0 -0
  279. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_db_adapters_mocked.py +0 -0
  280. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  281. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_dsn.py +0 -0
  282. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_duckdb.py +0 -0
  283. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_factory.py +0 -0
  284. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_factory_postgres_encoding.py +0 -0
  285. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_import_atomic.py +0 -0
  286. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_import_replacement_quoted.py +0 -0
  287. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_import_short_rows.py +0 -0
  288. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_import_timezones.py +0 -0
  289. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_mysql_case_folding.py +0 -0
  290. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_mysql_inprocess.py +0 -0
  291. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_new_table_checks.py +0 -0
  292. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_oracle_driver.py +0 -0
  293. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_postgres.py +0 -0
  294. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_roundtrip_adapters.py +0 -0
  295. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_sqlite.py +0 -0
  296. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_sqlite_extra.py +0 -0
  297. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_sqlserver_inprocess.py +0 -0
  298. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_tiers.py +0 -0
  299. {execsql2-2.28.0 → execsql2-2.29.0}/tests/db/test_transactions.py +0 -0
  300. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/__init__.py +0 -0
  301. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/conftest.py +0 -0
  302. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_base.py +0 -0
  303. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_db.py +0 -0
  304. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_delimited.py +0 -0
  305. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  306. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_exporters.py +0 -0
  307. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_feather.py +0 -0
  308. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_html_extended.py +0 -0
  309. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_html_latex.py +0 -0
  310. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_json.py +0 -0
  311. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_json_extended.py +0 -0
  312. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_latex_extended.py +0 -0
  313. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_markdown.py +0 -0
  314. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_ods.py +0 -0
  315. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_ods_export.py +0 -0
  316. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_parquet.py +0 -0
  317. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_pretty_extended.py +0 -0
  318. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_raw_extended.py +0 -0
  319. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_columnar.py +0 -0
  320. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_dbfile.py +0 -0
  321. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_delimited.py +0 -0
  322. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_json.py +0 -0
  323. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_latex_xml.py +0 -0
  324. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_markup.py +0 -0
  325. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_ods.py +0 -0
  326. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_pretty_raw.py +0 -0
  327. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_template.py +0 -0
  328. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_values.py +0 -0
  329. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_roundtrip_xlsx.py +0 -0
  330. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  331. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_templates.py +0 -0
  332. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_templates_extended.py +0 -0
  333. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_values_extended.py +0 -0
  334. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_xls_xlsx.py +0 -0
  335. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_xlsx.py +0 -0
  336. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_xml.py +0 -0
  337. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_yaml.py +0 -0
  338. {execsql2-2.28.0 → execsql2-2.29.0}/tests/exporters/test_zip.py +0 -0
  339. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/__init__.py +0 -0
  340. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_backends.py +0 -0
  341. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_backends_extended.py +0 -0
  342. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_compare_stats.py +0 -0
  343. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_compute_row_diffs.py +0 -0
  344. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_desktop_dialogs.py +0 -0
  345. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_format_table.py +0 -0
  346. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_tui_pilot.py +0 -0
  347. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_tui_pilot_complex.py +0 -0
  348. {execsql2-2.28.0 → execsql2-2.29.0}/tests/gui/test_utils_gui_extended.py +0 -0
  349. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/__init__.py +0 -0
  350. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/conftest.py +0 -0
  351. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_base_extended.py +0 -0
  352. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_csv_blank_lines.py +0 -0
  353. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_csv_edge_cases.py +0 -0
  354. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_csv_importer.py +0 -0
  355. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_feather_importer.py +0 -0
  356. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_json_importer.py +0 -0
  357. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_ods_importer.py +0 -0
  358. {execsql2-2.28.0 → execsql2-2.29.0}/tests/importers/test_roundtrip_csv.py +0 -0
  359. {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/__init__.py +0 -0
  360. {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/conftest.py +0 -0
  361. {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_dsn.py +0 -0
  362. {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_duckdb.py +0 -0
  363. {execsql2-2.28.0 → execsql2-2.29.0}/tests/integration/test_sqlite.py +0 -0
  364. {execsql2-2.28.0 → execsql2-2.29.0}/tests/live_db.py +0 -0
  365. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/__init__.py +0 -0
  366. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_code_actions.py +0 -0
  367. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_completion.py +0 -0
  368. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_diagnostics.py +0 -0
  369. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_document.py +0 -0
  370. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_folding.py +0 -0
  371. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_formatting.py +0 -0
  372. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_hover.py +0 -0
  373. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_lsp_command.py +0 -0
  374. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_navigation.py +0 -0
  375. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_rename.py +0 -0
  376. {execsql2-2.28.0 → execsql2-2.29.0}/tests/lsp/test_server.py +0 -0
  377. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/__init__.py +0 -0
  378. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_assert.py +0 -0
  379. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_breakpoint.py +0 -0
  380. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_conditions_extra.py +0 -0
  381. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_connect.py +0 -0
  382. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_io_export.py +0 -0
  383. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_io_import.py +0 -0
  384. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands.py +0 -0
  385. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  386. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_data.py +0 -0
  387. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_extended.py +0 -0
  388. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  389. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_io.py +0 -0
  390. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  391. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  392. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_system.py +0 -0
  393. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  394. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_pg_upsert.py +0 -0
  395. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_prompt.py +0 -0
  396. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_row_count.py +0 -0
  397. {execsql2-2.28.0 → execsql2-2.29.0}/tests/metacommands/test_show_scripts.py +0 -0
  398. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/__init__.py +0 -0
  399. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  400. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  401. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  402. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/config_runtime.sql +0 -0
  403. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/control_flow.sql +0 -0
  404. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  405. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/error_handling.sql +0 -0
  406. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  407. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/import_options.sql +0 -0
  408. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/includes/helper.sql +0 -0
  409. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/includes.sql +0 -0
  410. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/io_formats.sql +0 -0
  411. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  412. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/multi_db.sql +0 -0
  413. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  414. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  415. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/predicates.sql +0 -0
  416. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/scripts.sql +0 -0
  417. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/smoke.sql +0 -0
  418. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  419. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  420. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  421. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/template_export.sql +0 -0
  422. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/timer.sql +0 -0
  423. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/transactions.sql +0 -0
  424. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/write_create_table.sql +0 -0
  425. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  426. {execsql2-2.28.0 → execsql2-2.29.0}/tests/scripts/test_sql_scripts.py +0 -0
  427. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/__init__.py +0 -0
  428. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_env_detection.py +0 -0
  429. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_expansion_bomb.py +0 -0
  430. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_import_regex_hardening.py +0 -0
  431. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_log_redaction.py +0 -0
  432. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_log_secrets.py +0 -0
  433. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_odbc_injection.py +0 -0
  434. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_path_containment.py +0 -0
  435. {execsql2-2.28.0 → execsql2-2.29.0}/tests/security/test_zip_bomb.py +0 -0
  436. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api.py +0 -0
  437. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_errors.py +0 -0
  438. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_extended.py +0 -0
  439. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_filewriter.py +0 -0
  440. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_isolation.py +0 -0
  441. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_api_subprocess.py +0 -0
  442. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_ast.py +0 -0
  443. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_ast_parser.py +0 -0
  444. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_bundled_templates.py +0 -0
  445. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_config.py +0 -0
  446. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_config_data.py +0 -0
  447. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_config_extended.py +0 -0
  448. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_date_inference.py +0 -0
  449. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_debug_repl.py +0 -0
  450. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_engine.py +0 -0
  451. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_error_messages.py +0 -0
  452. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_exceptions.py +0 -0
  453. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_executor.py +0 -0
  454. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_executor_inprocess.py +0 -0
  455. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_format.py +0 -0
  456. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_interactive.py +0 -0
  457. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_mail.py +0 -0
  458. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_models.py +0 -0
  459. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_package.py +0 -0
  460. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_parser.py +0 -0
  461. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_parser_params.py +0 -0
  462. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_plugins.py +0 -0
  463. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_reference.py +0 -0
  464. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_registry.py +0 -0
  465. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_script.py +0 -0
  466. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_state.py +0 -0
  467. {execsql2-2.28.0 → execsql2-2.29.0}/tests/test_tooling_versions.py +0 -0
  468. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/__init__.py +0 -0
  469. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_auth.py +0 -0
  470. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_auth_extra.py +0 -0
  471. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_crypto.py +0 -0
  472. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_datetime.py +0 -0
  473. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_encodedfile_context.py +0 -0
  474. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_errors.py +0 -0
  475. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_errors_extra.py +0 -0
  476. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_fileio.py +0 -0
  477. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_fileio_extra.py +0 -0
  478. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_keyring_rules.py +0 -0
  479. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_mail_tls.py +0 -0
  480. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_numeric.py +0 -0
  481. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_regex.py +0 -0
  482. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_strings.py +0 -0
  483. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_timer.py +0 -0
  484. {execsql2-2.28.0 → execsql2-2.29.0}/tests/utils/test_timer_extra.py +0 -0
  485. {execsql2-2.28.0 → execsql2-2.29.0}/zensical.toml +0 -0
@@ -13,6 +13,21 @@ ______________________________________________________________________
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
+
16
31
  ## [2.28.0] - 2026-10-13
17
32
 
18
33
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.28.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
@@ -466,7 +466,7 @@ execsql format --no-sql --in-place scripts/
466
466
  ```yaml
467
467
  repos:
468
468
  - repo: https://github.com/geocoug/execsql
469
- rev: v2.28.0
469
+ rev: v2.29.0
470
470
  hooks:
471
471
  - id: execsql-format
472
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.28.0
324
+ rev: v2.29.0
325
325
  hooks:
326
326
  - id: execsql-format
327
327
  - id: execsql-lint
@@ -362,6 +362,11 @@ These are behavioral changes driven by security or correctness issues in the ups
362
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`. |
363
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
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. |
365
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. |
366
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. |
367
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. |
@@ -270,7 +270,7 @@ select id,name,created_at from users where active = true order by name;
270
270
  ```yaml
271
271
  repos:
272
272
  - repo: https://github.com/geocoug/execsql
273
- rev: v2.28.0
273
+ rev: v2.29.0
274
274
  hooks:
275
275
  - id: execsql-format
276
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.28.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.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.28.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" }
@@ -189,7 +189,7 @@ line-ending = "auto"
189
189
  exclude = ["*.md"]
190
190
 
191
191
  [tool.bumpversion]
192
- current_version = "2.28.0"
192
+ current_version = "2.29.0"
193
193
  commit = true
194
194
  tag = true
195
195
  tag_name = "v{new_version}"
@@ -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."""
@@ -67,6 +67,20 @@ class MySQLDatabase(Database):
67
67
  re.I,
68
68
  )
69
69
 
70
+ def quote_literal(self, value: Any) -> str:
71
+ """Return *value* as a string literal for MySQL/MariaDB.
72
+
73
+ Unless ``NO_BACKSLASH_ESCAPES`` is set, MySQL reads a backslash in a
74
+ literal as an escape, so ``\\'`` would end it: backslashes are doubled
75
+ as well as apostrophes.
76
+ """
77
+ if value is None:
78
+ return "NULL"
79
+ s = str(value)
80
+ if "\x00" in s:
81
+ raise ValueError("SQL literal contains a NUL byte; refusing to quote.")
82
+ return "'" + s.replace("\\", "\\\\").replace("'", "''") + "'"
83
+
70
84
  def __init__(
71
85
  self,
72
86
  server_name: str,
@@ -307,10 +321,12 @@ class MySQLDatabase(Database):
307
321
  table_cols = self.table_columns(table_name, schema_name)
308
322
  data_table_cols = [d.lower() for d in table_cols]
309
323
  csv_file_cols = csv_file_obj.column_headers()
324
+ # MySQL column names are case-insensitive: compare without case, and
325
+ # send the file's own spelling.
310
326
  if _state.conf.import_common_cols_only:
311
- import_cols = [col for col in csv_file_cols if col in data_table_cols]
327
+ import_cols = [col for col in csv_file_cols if col.lower() in data_table_cols]
312
328
  else:
313
- unmatched_cols = list(set(csv_file_cols) - set(data_table_cols))
329
+ unmatched_cols = [col for col in csv_file_cols if col.lower() not in data_table_cols]
314
330
  if len(unmatched_cols) > 0:
315
331
  raise ErrInfo(
316
332
  type="error",
@@ -326,7 +342,7 @@ class MySQLDatabase(Database):
326
342
  # reads the format that was detected.
327
343
  if (
328
344
  csv_file_obj.escapechar is None
329
- and data_table_cols == csv_file_cols
345
+ and data_table_cols == [col.lower() for col in csv_file_cols]
330
346
  and _state.conf.empty_strings
331
347
  and _state.conf.empty_rows
332
348
  and not _state.conf.del_empty_cols
@@ -353,7 +353,7 @@ class PostgresDatabase(Database):
353
353
  from execsql.exporters.delimited import blank_lines_as_empty_rows
354
354
 
355
355
  # Use Postgres' COPY FROM method via psycopg3's cursor.copy() context manager.
356
- rf = csv_file_obj.open("rt")
356
+ rf = csv_file_obj.openclean("rt") # past the SKIP lines, as every other reader starts
357
357
  if skipheader:
358
358
  next(rf)
359
359
  # Copy_from() requires a delimiter, but if there is none, feed it an
@@ -158,7 +158,7 @@ class XlsFile:
158
158
  break
159
159
  ncols += 1
160
160
  sheet_data = []
161
- for r in range(junk_header_rows, sheet.nrows - junk_header_rows):
161
+ for r in range(junk_header_rows, sheet.nrows):
162
162
  datarow = row_data(r, ncols)
163
163
  if datarow.count(None) == len(datarow):
164
164
  break
@@ -270,7 +270,8 @@ class XlsxFile:
270
270
  sheet_data = []
271
271
  rowsrc = sheet.iter_rows(min_row=junk_header_rows + 1, values_only=True)
272
272
  for r in rowsrc:
273
- if not any(r):
273
+ # A row of zeros or FALSE values is data; only a row of empty cells ends it.
274
+ if all(c is None or c == "" for c in r):
274
275
  break
275
276
  sheet_data.append(list(r))
276
277
  for r in range(len(sheet_data)):
@@ -17,6 +17,42 @@ from execsql.exceptions import ErrInfo
17
17
  __all__ = ["CounterVars", "SubVarSet", "LocalSubVarSet", "ScriptArgSubVarSet"]
18
18
 
19
19
 
20
+ def _after_escape_string_prefix(command: str, start: int) -> bool:
21
+ """Whether the ``!'!`` token at *start* follows an ``E`` that makes it a PostgreSQL escape string (``E!'!v!'!``).
22
+
23
+ The ``E`` must stand alone: in ``TYPE!'!v!'!`` it ends a word.
24
+ """
25
+ if start < 1 or command[start - 1] not in "Ee":
26
+ return False
27
+ return start < 2 or not (command[start - 2].isalnum() or command[start - 2] == "_")
28
+
29
+
30
+ def _sql_string_literal(value: str, *, escape_string: bool = False) -> str:
31
+ """*value* as the string literal ``!'!var!'!`` substitutes, in the current connection's form.
32
+
33
+ An apostrophe is doubled everywhere; a backslash only where the DBMS
34
+ reads it as an escape (MySQL/MariaDB), or inside an escape string the
35
+ script wrote as ``E!'!var!'!`` (*escape_string*), so ``C:\\data`` is
36
+ stored as written in every case. Without a connection, the standard SQL
37
+ form. (Upstream doubled backslashes only on Windows, to protect
38
+ ``re.sub``'s replacement template, not the SQL.)
39
+ """
40
+ if escape_string:
41
+ return "'" + value.replace("\\", "\\\\").replace("'", "''") + "'"
42
+ db = None
43
+ try:
44
+ import execsql.state as _state
45
+
46
+ dbs = _state.dbs
47
+ if dbs is not None and dbs.current_db is not None:
48
+ db = dbs.current()
49
+ except Exception:
50
+ db = None
51
+ if db is not None:
52
+ return db.quote_literal(value)
53
+ return "'" + value.replace("'", "''") + "'"
54
+
55
+
20
56
  # ---------------------------------------------------------------------------
21
57
  # CounterVars
22
58
  # ---------------------------------------------------------------------------
@@ -253,13 +289,10 @@ class SubVarSet:
253
289
  )
254
290
  quote = m.group("q")
255
291
  if quote == "'":
256
- # B07a/F002: wrap in single quotes, doubling embedded
257
- # apostrophes AND escaping backslashes so MySQL default
258
- # mode and PostgreSQL E-string literals can't end the
259
- # quoted region via ``\'``. The previous Windows-only
260
- # branch (``os.name != 'posix'``) applied the escape on
261
- # the wrong axis — host OS vs target DBMS.
262
- sub = "'" + sub.replace("\\", "\\\\").replace("'", "''") + "'"
292
+ sub = _sql_string_literal(
293
+ sub,
294
+ escape_string=_after_escape_string_prefix(command_str, m.start()),
295
+ )
263
296
  elif quote == '"':
264
297
  # B07a/F001: wrap in double quotes, doubling embedded
265
298
  # ``"`` so a value containing ``"; DROP TABLE x; --``
@@ -306,13 +339,12 @@ class SubVarSet:
306
339
  if idx != -1:
307
340
  _check_nul(varname, sub)
308
341
  return command_str[:idx] + sub + command_str[idx + len(token) :], True
309
- # Single-quote-wrapped token: !'!varname!'! — escape ``\`` and
310
- # double embedded ``'`` (see substitute_one for rationale).
342
+ # Single-quote-wrapped token: !'!varname!'! — a string literal.
311
343
  tokenq = f"!'!{varname}!'!"
312
344
  idxq = cmd_lower.find(tokenq)
313
345
  if idxq != -1:
314
346
  _check_nul(varname, sub)
315
- wrapped = "'" + sub.replace("\\", "\\\\").replace("'", "''") + "'"
347
+ wrapped = _sql_string_literal(sub, escape_string=_after_escape_string_prefix(command_str, idxq))
316
348
  return (
317
349
  command_str[:idxq] + wrapped + command_str[idxq + len(tokenq) :],
318
350
  True,
@@ -884,6 +884,14 @@ dbt_oracle.name_datatype(DT_TimestampTZ, "TIMESTAMP WITH TIME ZONE")
884
884
  dbt_oracle.name_datatype(DT_Timestamp, "TIMESTAMP", casting_name="TIMESTAMP")
885
885
  dbt_oracle.name_datatype(DT_Date, "DATE", casting_name="DATE")
886
886
  dbt_oracle.name_datatype(DT_Time_Oracle, "VARCHAR2", True, casting_name="VARCHAR(20)")
887
+ # Oracle has no time-of-day type, and oracledb cannot bind a datetime.time: a
888
+ # time column is stored as its ISO text (at most 15 characters, HH:MM:SS.ffffff).
889
+ dbt_oracle.name_datatype(
890
+ DT_Time,
891
+ "VARCHAR2(15)",
892
+ casting_name="VARCHAR(20)",
893
+ conv_mod_fn=lambda x: x.isoformat() if x is not None else None,
894
+ )
887
895
  dbt_oracle.name_datatype(DT_Integer, "NUMBER")
888
896
  dbt_oracle.name_datatype(DT_Long, "NUMBER")
889
897
  dbt_oracle.name_datatype(DT_Float, "FLOAT")
@@ -430,6 +430,9 @@ def _make_csv_file_obj(tmp_path, headers, rows, *, delimiter=",", quotechar='"',
430
430
  def open(self, _mode):
431
431
  return open(self.csvfname, encoding=encoding)
432
432
 
433
+ def openclean(self, mode):
434
+ return self.open(mode) # no junk lines before the header
435
+
433
436
  def reader(self):
434
437
  import csv as _csv
435
438
 
@@ -0,0 +1,40 @@
1
+ """``!'!var!'!`` stores a value exactly as written, backslashes and apostrophes included."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from execsql import run
8
+
9
+ VALUES = [r"C:\data\new", r"x\'; drop table t; --", "O'Brien", "\\", r"two\\backslashes"]
10
+
11
+
12
+ def _script(value: str) -> str:
13
+ return f"-- !x! SUB v {value}\ncreate table t (v text);\ninsert into t values (!'!v!'!);\n"
14
+
15
+
16
+ @pytest.mark.parametrize("value", VALUES)
17
+ def test_sqlite_round_trip(tmp_path, value):
18
+ import sqlite3
19
+
20
+ db = tmp_path / "t.db"
21
+ result = run(sql=_script(value), dsn=f"sqlite:///{db}", new_db=True)
22
+ assert result.success, result.errors
23
+ con = sqlite3.connect(db)
24
+ try:
25
+ assert con.execute("select v from t").fetchall() == [(value,)]
26
+ finally:
27
+ con.close()
28
+
29
+
30
+ @pytest.mark.parametrize("value", VALUES)
31
+ def test_duckdb_round_trip(tmp_path, value):
32
+ duckdb = pytest.importorskip("duckdb")
33
+ db = tmp_path / "t.duckdb"
34
+ result = run(sql=_script(value), dsn=f"duckdb:///{db}", new_db=True)
35
+ assert result.success, result.errors
36
+ con = duckdb.connect(str(db), read_only=True)
37
+ try:
38
+ assert con.execute("select v from t").fetchall() == [(value,)]
39
+ finally:
40
+ con.close()
@@ -182,3 +182,35 @@ class TestImportXls:
182
182
  names = [r[0] for r in rows]
183
183
  assert "existing" in names
184
184
  assert "new" in names
185
+
186
+
187
+ class TestSpreadsheetRowsAfterTheHeader:
188
+ """Every data row is read: a row of zeros is data, and SKIP does not shorten the sheet."""
189
+
190
+ def test_an_xlsx_row_of_zeros_and_false_does_not_end_the_data(self, tmp_path):
191
+ rows = [["id", "flag", "note"], [1, True, "a"], [0, False, None], [2, True, "c"]]
192
+ path = _make_xlsx(tmp_path, "Sheet1", rows)
193
+ _hdrs, data = xls_data(path, "Sheet1", 0)
194
+ assert len(data) == 3
195
+
196
+ def test_an_xlsx_row_of_empty_cells_still_ends_the_data(self, tmp_path):
197
+ path = _make_xlsx(tmp_path, "Sheet1", [["id"], [1], [None], [2]])
198
+ _hdrs, data = xls_data(path, "Sheet1", 0)
199
+ assert len(data) == 1
200
+
201
+ def test_xls_skip_keeps_the_last_rows(self):
202
+ from types import SimpleNamespace
203
+
204
+ from execsql.exporters.xls import XlsFile
205
+
206
+ def cell(value):
207
+ return SimpleNamespace(ctype=1 if isinstance(value, str) else 2, value=value)
208
+
209
+ sheet_rows = [["junk"], ["junk"], ["id", "v"], [1, 1], [2, 2], [3, 3]]
210
+ sheet = SimpleNamespace(nrows=len(sheet_rows), row_slice=lambda r: [cell(v) for v in sheet_rows[r]])
211
+ xls = object.__new__(XlsFile)
212
+ xls.filename = "fake.xls"
213
+ xls.sheet_named = lambda name: sheet
214
+ data = xls.sheet_data("Sheet1", junk_header_rows=2)
215
+ assert data[0] == ["id", "v"]
216
+ assert [r[0] for r in data[1:]] == [1, 2, 3]
@@ -485,3 +485,59 @@ class TestImportBackslashes:
485
485
 
486
486
  def test_a_backslash_in_the_file_path_is_kept(self, tmp_path):
487
487
  assert self._import(tmp_path, tmp_path / "dir\\with\\backslashes") == self.VALUES
488
+
489
+
490
+ class TestHeadersWithCapitals:
491
+ """MySQL column names are case-insensitive; a header with capitals must match its column."""
492
+
493
+ @pytest.mark.parametrize("quoted", [False, True], ids=["load-data", "row-by-row"])
494
+ def test_import_to_new_loads_every_column(self, tmp_path, quoted):
495
+ _exec_mysql("DROP TABLE IF EXISTS t_mixed")
496
+ _write_conf(tmp_path)
497
+ csv_path = tmp_path / "mixed.csv"
498
+ # An escape character sends the file down the row-by-row path.
499
+ body = 'Id,Name\n1,"Ann \\"A\\""\n2,Bo\n' if quoted else "Id,Name\n1,Ann\n2,Bo\n"
500
+ csv_path.write_text(body)
501
+ script = write_script(tmp_path, f"-- !x! IMPORT TO NEW t_mixed FROM {csv_path}\n")
502
+ try:
503
+ result = _run_execsql_mysql(tmp_path, script)
504
+ assert result.returncode == 0, f"stderr: {result.stderr}"
505
+ rows = _query_mysql("SELECT Id, Name FROM t_mixed ORDER BY Id")
506
+ assert [r[0] for r in rows] == [1, 2]
507
+ assert all(r[1] for r in rows)
508
+ finally:
509
+ _exec_mysql("DROP TABLE IF EXISTS t_mixed")
510
+
511
+ def test_common_columns_only_keeps_a_capitalized_column(self, tmp_path):
512
+ _exec_mysql("DROP TABLE IF EXISTS t_common")
513
+ _exec_mysql("CREATE TABLE t_common (id INTEGER, name VARCHAR(10))")
514
+ _write_conf(tmp_path)
515
+ csv_path = tmp_path / "common.csv"
516
+ csv_path.write_text("Id,Name,Extra\n1,Ann,x\n")
517
+ script = write_script(
518
+ tmp_path,
519
+ f"-- !x! IMPORT_COMMON_COLUMNS_ONLY YES\n-- !x! IMPORT TO t_common FROM {csv_path}\n",
520
+ )
521
+ try:
522
+ result = _run_execsql_mysql(tmp_path, script)
523
+ assert result.returncode == 0, f"stderr: {result.stderr}"
524
+ assert [tuple(r) for r in _query_mysql("SELECT id, name FROM t_common")] == [(1, "Ann")]
525
+ finally:
526
+ _exec_mysql("DROP TABLE IF EXISTS t_common")
527
+
528
+
529
+ @pytest.mark.parametrize("value", [r"C:\data\new", r"x\'; drop table t; --", "O'Brien", r"two\\backslashes"])
530
+ def test_quoted_substitution_stores_the_value_as_written(tmp_path, value):
531
+ """MySQL reads \\ as an escape, so !'!v!'! doubles it there, and \\' cannot end the literal."""
532
+ _exec_mysql("DROP TABLE IF EXISTS quoted_sub")
533
+ _write_conf(tmp_path)
534
+ script = write_script(
535
+ tmp_path,
536
+ f"-- !x! SUB v {value}\ncreate table quoted_sub (v text);\ninsert into quoted_sub values (!'!v!'!);\n",
537
+ )
538
+ try:
539
+ result = _run_execsql_mysql(tmp_path, script)
540
+ assert result.returncode == 0, f"stderr: {result.stderr}"
541
+ assert [tuple(r) for r in _query_mysql("SELECT v FROM quoted_sub")] == [(value,)]
542
+ finally:
543
+ _exec_mysql("DROP TABLE IF EXISTS quoted_sub")
@@ -686,3 +686,59 @@ def test_copy_moves_a_bytea_column_byte_for_byte(tmp_path):
686
686
  assert row == ("bytea", payload)
687
687
  finally:
688
688
  run(sql="drop table if exists bin_src;\ndrop table if exists bin_dst;\n", dsn=_PG_DSN)
689
+
690
+
691
+ def test_import_skip_applies_on_the_copy_path(tmp_path):
692
+ """A quoted CSV takes PostgreSQL's COPY path; SKIP n must drop n lines before the header there too."""
693
+ from execsql import run
694
+
695
+ csv = tmp_path / "skip.csv"
696
+ csv.write_text('exported by a tool\nsecond preamble line\nid,name\n"1","one"\n"2","two"\n', encoding="utf-8")
697
+ run(sql="drop table if exists skip_target;\n", dsn=_PG_DSN)
698
+ try:
699
+ result = run(sql=f"-- !x! IMPORT TO NEW skip_target FROM {csv} SKIP 2\n", dsn=_PG_DSN)
700
+ assert result.success, result.errors
701
+ with psycopg.connect(**_PG_CONNECT_KWARGS) as conn:
702
+ rows = conn.execute("select id, name from skip_target order by id").fetchall()
703
+ assert [tuple(map(str, r)) for r in rows] == [("1", "one"), ("2", "two")]
704
+ finally:
705
+ run(sql="drop table if exists skip_target;\n", dsn=_PG_DSN)
706
+
707
+
708
+ @pytest.mark.parametrize("value", [r"C:\data\new", r"x\'; drop table t; --", "O'Brien", r"two\\backslashes"])
709
+ def test_quoted_substitution_stores_the_value_as_written(value):
710
+ """standard_conforming_strings is on: a backslash in '...' is text, so !'!v!'! must not double it."""
711
+ from execsql import run
712
+
713
+ run(sql="drop table if exists quoted_sub;\n", dsn=_PG_DSN)
714
+ try:
715
+ result = run(
716
+ sql=f"-- !x! SUB v {value}\ncreate table quoted_sub (v text);\ninsert into quoted_sub values (!'!v!'!);\n",
717
+ dsn=_PG_DSN,
718
+ )
719
+ assert result.success, result.errors
720
+ with psycopg.connect(**_PG_CONNECT_KWARGS) as conn:
721
+ assert conn.execute("select v from quoted_sub").fetchall() == [(value,)]
722
+ finally:
723
+ run(sql="drop table if exists quoted_sub;\n", dsn=_PG_DSN)
724
+
725
+
726
+ @pytest.mark.parametrize("value", [r"C:\data\new", r"x\'; drop table t; --", r"two\\backslashes"])
727
+ def test_escape_string_form_stores_the_same_value(value):
728
+ """E!'!v!'! (a PostgreSQL escape string) stores exactly what !'!v!'! stores."""
729
+ from execsql import run
730
+
731
+ run(sql="drop table if exists quoted_sub_e;\n", dsn=_PG_DSN)
732
+ try:
733
+ result = run(
734
+ sql=(
735
+ f"-- !x! SUB v {value}\ncreate table quoted_sub_e (plain text, escaped text);\n"
736
+ "insert into quoted_sub_e values (!'!v!'!, E!'!v!'!);\n"
737
+ ),
738
+ dsn=_PG_DSN,
739
+ )
740
+ assert result.success, result.errors
741
+ with psycopg.connect(**_PG_CONNECT_KWARGS) as conn:
742
+ assert conn.execute("select plain, escaped from quoted_sub_e").fetchall() == [(value, value)]
743
+ finally:
744
+ run(sql="drop table if exists quoted_sub_e;\n", dsn=_PG_DSN)
@@ -143,11 +143,11 @@ drop table if exists q_bs_chk;
143
143
  CREATE TABLE q_bs_chk (val TEXT);
144
144
  INSERT INTO q_bs_chk SELECT val FROM q_bs;
145
145
 
146
- -- SQLite stores the doubled form verbatim (no escape interpretation).
146
+ -- SQLite reads no escapes in a literal, so the value round-trips byte for byte.
147
147
  -- !x! select_sub q_bs_chk
148
- -- !x! ASSERT EQUALS("!!@val!!", "c:\\path\\file") "backslash doubling is universal (security > byte-perfect SQLite round-trip)"
148
+ -- !x! ASSERT EQUALS("!!@val!!", "c:\path\file") "a backslash is stored as written outside MySQL"
149
149
 
150
- INSERT INTO _runlog VALUES ('phase5', 'backslash doubling confirmed');
150
+ INSERT INTO _runlog VALUES ('phase5', 'backslash stored as written');
151
151
 
152
152
 
153
153
  -- =========================================================================