execsql2 2.22.9__tar.gz → 2.23.1__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 (400) hide show
  1. {execsql2-2.22.9 → execsql2-2.23.1}/.github/workflows/ci-cd.yml +22 -15
  2. {execsql2-2.22.9 → execsql2-2.23.1}/.gitignore +1 -0
  3. {execsql2-2.22.9 → execsql2-2.23.1}/CHANGELOG.md +50 -0
  4. {execsql2-2.22.9 → execsql2-2.23.1}/PKG-INFO +22 -13
  5. {execsql2-2.22.9 → execsql2-2.23.1}/README.md +21 -12
  6. {execsql2-2.22.9 → execsql2-2.23.1}/docs/about/divergence.md +40 -9
  7. {execsql2-2.22.9 → execsql2-2.23.1}/docs/api/index.md +17 -0
  8. {execsql2-2.22.9 → execsql2-2.23.1}/docs/getting-started/requirements.md +26 -0
  9. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/encoding.md +1 -1
  10. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/formatter.md +1 -1
  11. {execsql2-2.22.9 → execsql2-2.23.1}/docs/reference/configuration.md +4 -0
  12. {execsql2-2.22.9 → execsql2-2.23.1}/docs/reference/metacommands.md +15 -0
  13. {execsql2-2.22.9 → execsql2-2.23.1}/docs/reference/substitution_vars.md +1 -1
  14. {execsql2-2.22.9 → execsql2-2.23.1}/pyproject.toml +3 -3
  15. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/api.py +71 -0
  16. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/config.py +2 -0
  17. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/data/execsql.conf.template +5 -0
  18. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/access.py +34 -10
  19. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/base.py +120 -2
  20. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/dsn.py +14 -7
  21. execsql2-2.23.1/src/execsql/db/duckdb.py +182 -0
  22. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/firebird.py +12 -7
  23. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/mysql.py +55 -10
  24. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/oracle.py +24 -11
  25. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/postgres.py +17 -9
  26. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/sqlite.py +23 -8
  27. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/sqlserver.py +7 -7
  28. execsql2-2.23.1/src/execsql/db/tiers.py +79 -0
  29. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/delimited.py +15 -1
  30. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/html.py +8 -2
  31. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/latex.py +35 -2
  32. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/markdown.py +9 -3
  33. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/ods.py +22 -4
  34. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/pretty.py +13 -0
  35. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/templates.py +18 -1
  36. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/values.py +25 -4
  37. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/xlsx.py +7 -1
  38. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/state.py +5 -0
  39. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/state.pyi +3 -1
  40. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/types.py +4 -1
  41. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/fileio.py +47 -2
  42. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_postgres_inprocess.py +12 -0
  43. execsql2-2.23.1/tests/db/test_roundtrip_adapters.py +211 -0
  44. execsql2-2.23.1/tests/db/test_tiers.py +224 -0
  45. execsql2-2.23.1/tests/db/test_transactions.py +360 -0
  46. execsql2-2.23.1/tests/exporters/conftest.py +277 -0
  47. execsql2-2.23.1/tests/exporters/test_roundtrip_columnar.py +130 -0
  48. execsql2-2.23.1/tests/exporters/test_roundtrip_dbfile.py +207 -0
  49. execsql2-2.23.1/tests/exporters/test_roundtrip_delimited.py +147 -0
  50. execsql2-2.23.1/tests/exporters/test_roundtrip_json.py +81 -0
  51. execsql2-2.23.1/tests/exporters/test_roundtrip_latex_xml.py +133 -0
  52. execsql2-2.23.1/tests/exporters/test_roundtrip_markup.py +144 -0
  53. execsql2-2.23.1/tests/exporters/test_roundtrip_ods.py +131 -0
  54. execsql2-2.23.1/tests/exporters/test_roundtrip_pretty_raw.py +114 -0
  55. execsql2-2.23.1/tests/exporters/test_roundtrip_template.py +98 -0
  56. execsql2-2.23.1/tests/exporters/test_roundtrip_values.py +141 -0
  57. execsql2-2.23.1/tests/exporters/test_roundtrip_xlsx.py +103 -0
  58. execsql2-2.23.1/tests/importers/conftest.py +127 -0
  59. execsql2-2.23.1/tests/importers/test_roundtrip_csv.py +152 -0
  60. {execsql2-2.22.9 → execsql2-2.23.1}/tests/integration/test_postgres.py +69 -0
  61. execsql2-2.23.1/tests/live_db.py +108 -0
  62. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_data.py +23 -19
  63. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_extended.py +20 -14
  64. execsql2-2.23.1/tests/test_api_filewriter.py +158 -0
  65. {execsql2-2.22.9 → execsql2-2.23.1}/uv.lock +1 -1
  66. execsql2-2.22.9/src/execsql/db/duckdb.py +0 -90
  67. {execsql2-2.22.9 → execsql2-2.23.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  68. {execsql2-2.22.9 → execsql2-2.23.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  69. {execsql2-2.22.9 → execsql2-2.23.1}/.github/dependabot.yml +0 -0
  70. {execsql2-2.22.9 → execsql2-2.23.1}/.pre-commit-config.yaml +0 -0
  71. {execsql2-2.22.9 → execsql2-2.23.1}/.pre-commit-hooks.yaml +0 -0
  72. {execsql2-2.22.9 → execsql2-2.23.1}/.python-version +0 -0
  73. {execsql2-2.22.9 → execsql2-2.23.1}/.readthedocs.yaml +0 -0
  74. {execsql2-2.22.9 → execsql2-2.23.1}/CONTRIBUTING.md +0 -0
  75. {execsql2-2.22.9 → execsql2-2.23.1}/LICENSE.txt +0 -0
  76. {execsql2-2.22.9 → execsql2-2.23.1}/NOTICE +0 -0
  77. {execsql2-2.22.9 → execsql2-2.23.1}/SECURITY.md +0 -0
  78. {execsql2-2.22.9 → execsql2-2.23.1}/docs/about/contributors.md +0 -0
  79. {execsql2-2.22.9 → execsql2-2.23.1}/docs/about/copyright.md +0 -0
  80. {execsql2-2.22.9 → execsql2-2.23.1}/docs/api/cli.md +0 -0
  81. {execsql2-2.22.9 → execsql2-2.23.1}/docs/api/db.md +0 -0
  82. {execsql2-2.22.9 → execsql2-2.23.1}/docs/api/exporters.md +0 -0
  83. {execsql2-2.22.9 → execsql2-2.23.1}/docs/api/importers.md +0 -0
  84. {execsql2-2.22.9 → execsql2-2.23.1}/docs/api/metacommands.md +0 -0
  85. {execsql2-2.22.9 → execsql2-2.23.1}/docs/dev/adding_db_adapters.md +0 -0
  86. {execsql2-2.22.9 → execsql2-2.23.1}/docs/dev/adding_exporters.md +0 -0
  87. {execsql2-2.22.9 → execsql2-2.23.1}/docs/dev/adding_importers.md +0 -0
  88. {execsql2-2.22.9 → execsql2-2.23.1}/docs/dev/adding_metacommands.md +0 -0
  89. {execsql2-2.22.9 → execsql2-2.23.1}/docs/dev/architecture.md +0 -0
  90. {execsql2-2.22.9 → execsql2-2.23.1}/docs/dev/releasing.md +0 -0
  91. {execsql2-2.22.9 → execsql2-2.23.1}/docs/getting-started/installation.md +0 -0
  92. {execsql2-2.22.9 → execsql2-2.23.1}/docs/getting-started/syntax.md +0 -0
  93. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/debugging.md +0 -0
  94. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/documentation.md +0 -0
  95. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/examples.md +0 -0
  96. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/logging.md +0 -0
  97. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/sql_syntax.md +0 -0
  98. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/usage.md +0 -0
  99. {execsql2-2.22.9 → execsql2-2.23.1}/docs/guides/using_scripts.md +0 -0
  100. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/Compare_planets.png +0 -0
  101. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/actions.png +0 -0
  102. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/actions2.png +0 -0
  103. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/checkboxes.png +0 -0
  104. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/connect.b64 +0 -0
  105. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/connect.png +0 -0
  106. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/create_conf.png +0 -0
  107. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/data_error1_screenshot.jpg +0 -0
  108. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/entry_form.png +0 -0
  109. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/execsql_console.png +0 -0
  110. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/execsql_logo_01.png +0 -0
  111. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/fatals.png +0 -0
  112. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/logo_small.png +0 -0
  113. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/pause_terminal.png +0 -0
  114. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/pause_terminal_sm.b64 +0 -0
  115. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/pause_terminal_sm.png +0 -0
  116. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/prompt_compare.png +0 -0
  117. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/set_build_commands.jpg +0 -0
  118. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/unit_conversions.b64 +0 -0
  119. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/unit_conversions_029.png +0 -0
  120. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/unmatched.png +0 -0
  121. {execsql2-2.22.9 → execsql2-2.23.1}/docs/images/vim_execsql_highlight.png +0 -0
  122. {execsql2-2.22.9 → execsql2-2.23.1}/docs/index.md +0 -0
  123. {execsql2-2.22.9 → execsql2-2.23.1}/docs/reference/security.md +0 -0
  124. {execsql2-2.22.9 → execsql2-2.23.1}/extras/plugin-template/README.md +0 -0
  125. {execsql2-2.22.9 → execsql2-2.23.1}/extras/plugin-template/pyproject.toml +0 -0
  126. {execsql2-2.22.9 → execsql2-2.23.1}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  127. {execsql2-2.22.9 → execsql2-2.23.1}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  128. {execsql2-2.22.9 → execsql2-2.23.1}/extras/vscode-execsql/README.md +0 -0
  129. {execsql2-2.22.9 → execsql2-2.23.1}/extras/vscode-execsql/package.json +0 -0
  130. {execsql2-2.22.9 → execsql2-2.23.1}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  131. {execsql2-2.22.9 → execsql2-2.23.1}/justfile +0 -0
  132. {execsql2-2.22.9 → execsql2-2.23.1}/scripts/benchmarks/README.md +0 -0
  133. {execsql2-2.22.9 → execsql2-2.23.1}/scripts/benchmarks/results/.gitignore +0 -0
  134. {execsql2-2.22.9 → execsql2-2.23.1}/scripts/benchmarks/run_benchmarks.py +0 -0
  135. {execsql2-2.22.9 → execsql2-2.23.1}/scripts/generate_vscode_grammar.py +0 -0
  136. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/__init__.py +0 -0
  137. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/__main__.py +0 -0
  138. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/cli/__init__.py +0 -0
  139. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/cli/dsn.py +0 -0
  140. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/cli/help.py +0 -0
  141. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/cli/lint.py +0 -0
  142. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/cli/run.py +0 -0
  143. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/data/__init__.py +0 -0
  144. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/__init__.py +0 -0
  145. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/db/factory.py +0 -0
  146. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/debug/__init__.py +0 -0
  147. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/debug/repl.py +0 -0
  148. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exceptions.py +0 -0
  149. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/__init__.py +0 -0
  150. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/base.py +0 -0
  151. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/duckdb.py +0 -0
  152. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/feather.py +0 -0
  153. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/json.py +0 -0
  154. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/parquet.py +0 -0
  155. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/protocol.py +0 -0
  156. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/raw.py +0 -0
  157. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/sqlite.py +0 -0
  158. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/xls.py +0 -0
  159. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/xml.py +0 -0
  160. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/yaml.py +0 -0
  161. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/exporters/zip.py +0 -0
  162. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/format.py +0 -0
  163. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/gui/__init__.py +0 -0
  164. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/gui/base.py +0 -0
  165. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/gui/console.py +0 -0
  166. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/gui/desktop.py +0 -0
  167. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/gui/tui.py +0 -0
  168. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/importers/__init__.py +0 -0
  169. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/importers/base.py +0 -0
  170. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/importers/csv.py +0 -0
  171. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/importers/feather.py +0 -0
  172. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/importers/json.py +0 -0
  173. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/importers/ods.py +0 -0
  174. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/importers/xls.py +0 -0
  175. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/__init__.py +0 -0
  176. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/conditions.py +0 -0
  177. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/connect.py +0 -0
  178. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/control.py +0 -0
  179. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/data.py +0 -0
  180. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/debug.py +0 -0
  181. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/dispatch.py +0 -0
  182. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/io.py +0 -0
  183. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/io_export.py +0 -0
  184. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/io_fileops.py +0 -0
  185. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/io_import.py +0 -0
  186. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/io_write.py +0 -0
  187. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/prompt.py +0 -0
  188. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/script_ext.py +0 -0
  189. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/system.py +0 -0
  190. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/metacommands/upsert.py +0 -0
  191. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/models.py +0 -0
  192. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/parser.py +0 -0
  193. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/plugins.py +0 -0
  194. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/py.typed +0 -0
  195. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/script/__init__.py +0 -0
  196. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/script/ast.py +0 -0
  197. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/script/control.py +0 -0
  198. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/script/engine.py +0 -0
  199. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/script/executor.py +0 -0
  200. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/script/parser.py +0 -0
  201. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/script/variables.py +0 -0
  202. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/__init__.py +0 -0
  203. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/auth.py +0 -0
  204. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/crypto.py +0 -0
  205. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/datetime.py +0 -0
  206. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/errors.py +0 -0
  207. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/gui.py +0 -0
  208. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/mail.py +0 -0
  209. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/numeric.py +0 -0
  210. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/regex.py +0 -0
  211. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/strings.py +0 -0
  212. {execsql2-2.22.9 → execsql2-2.23.1}/src/execsql/utils/timer.py +0 -0
  213. {execsql2-2.22.9 → execsql2-2.23.1}/templates/README.md +0 -0
  214. {execsql2-2.22.9 → execsql2-2.23.1}/templates/config_settings.sqlite +0 -0
  215. {execsql2-2.22.9 → execsql2-2.23.1}/templates/example_config_prompt.sql +0 -0
  216. {execsql2-2.22.9 → execsql2-2.23.1}/templates/make_config_db.sql +0 -0
  217. {execsql2-2.22.9 → execsql2-2.23.1}/templates/md_compare.sql +0 -0
  218. {execsql2-2.22.9 → execsql2-2.23.1}/templates/md_glossary.sql +0 -0
  219. {execsql2-2.22.9 → execsql2-2.23.1}/templates/md_upsert.sql +0 -0
  220. {execsql2-2.22.9 → execsql2-2.23.1}/templates/pg_compare.sql +0 -0
  221. {execsql2-2.22.9 → execsql2-2.23.1}/templates/pg_glossary.sql +0 -0
  222. {execsql2-2.22.9 → execsql2-2.23.1}/templates/pg_upsert.sql +0 -0
  223. {execsql2-2.22.9 → execsql2-2.23.1}/templates/script_template.sql +0 -0
  224. {execsql2-2.22.9 → execsql2-2.23.1}/templates/ss_compare.sql +0 -0
  225. {execsql2-2.22.9 → execsql2-2.23.1}/templates/ss_glossary.sql +0 -0
  226. {execsql2-2.22.9 → execsql2-2.23.1}/templates/ss_upsert.sql +0 -0
  227. {execsql2-2.22.9 → execsql2-2.23.1}/tests/__init__.py +0 -0
  228. {execsql2-2.22.9 → execsql2-2.23.1}/tests/cli/__init__.py +0 -0
  229. {execsql2-2.22.9 → execsql2-2.23.1}/tests/cli/test_cli.py +0 -0
  230. {execsql2-2.22.9 → execsql2-2.23.1}/tests/cli/test_cli_e2e.py +0 -0
  231. {execsql2-2.22.9 → execsql2-2.23.1}/tests/cli/test_cli_run.py +0 -0
  232. {execsql2-2.22.9 → execsql2-2.23.1}/tests/cli/test_lint.py +0 -0
  233. {execsql2-2.22.9 → execsql2-2.23.1}/tests/cli/test_ping.py +0 -0
  234. {execsql2-2.22.9 → execsql2-2.23.1}/tests/cli/test_profile.py +0 -0
  235. {execsql2-2.22.9 → execsql2-2.23.1}/tests/conftest.py +0 -0
  236. {execsql2-2.22.9 → execsql2-2.23.1}/tests/data/formatter_shapes/comment_anchoring.sql +0 -0
  237. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/__init__.py +0 -0
  238. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_access_windows.py +0 -0
  239. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_base.py +0 -0
  240. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_db_adapters_mocked.py +0 -0
  241. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  242. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_dsn.py +0 -0
  243. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_duckdb.py +0 -0
  244. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_factory.py +0 -0
  245. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_mysql_case_folding.py +0 -0
  246. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_mysql_inprocess.py +0 -0
  247. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_postgres.py +0 -0
  248. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_sqlite.py +0 -0
  249. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_sqlite_extra.py +0 -0
  250. {execsql2-2.22.9 → execsql2-2.23.1}/tests/db/test_sqlserver_inprocess.py +0 -0
  251. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/__init__.py +0 -0
  252. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_base.py +0 -0
  253. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_db.py +0 -0
  254. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_delimited.py +0 -0
  255. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_duckdb_exporter.py +0 -0
  256. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_exporters.py +0 -0
  257. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_feather.py +0 -0
  258. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_html_extended.py +0 -0
  259. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_html_latex.py +0 -0
  260. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_json.py +0 -0
  261. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_json_extended.py +0 -0
  262. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_latex_extended.py +0 -0
  263. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_markdown.py +0 -0
  264. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_ods.py +0 -0
  265. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_ods_export.py +0 -0
  266. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_parquet.py +0 -0
  267. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_pretty_extended.py +0 -0
  268. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_raw_extended.py +0 -0
  269. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_sqlite_exporter.py +0 -0
  270. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_templates.py +0 -0
  271. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_templates_extended.py +0 -0
  272. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_values_extended.py +0 -0
  273. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_xls_xlsx.py +0 -0
  274. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_xlsx.py +0 -0
  275. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_xml.py +0 -0
  276. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_yaml.py +0 -0
  277. {execsql2-2.22.9 → execsql2-2.23.1}/tests/exporters/test_zip.py +0 -0
  278. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/__init__.py +0 -0
  279. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_backends.py +0 -0
  280. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_backends_extended.py +0 -0
  281. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_compare_stats.py +0 -0
  282. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_compute_row_diffs.py +0 -0
  283. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_desktop_dialogs.py +0 -0
  284. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_format_table.py +0 -0
  285. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_tui_pilot.py +0 -0
  286. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_tui_pilot_complex.py +0 -0
  287. {execsql2-2.22.9 → execsql2-2.23.1}/tests/gui/test_utils_gui_extended.py +0 -0
  288. {execsql2-2.22.9 → execsql2-2.23.1}/tests/importers/__init__.py +0 -0
  289. {execsql2-2.22.9 → execsql2-2.23.1}/tests/importers/test_base_extended.py +0 -0
  290. {execsql2-2.22.9 → execsql2-2.23.1}/tests/importers/test_csv_edge_cases.py +0 -0
  291. {execsql2-2.22.9 → execsql2-2.23.1}/tests/importers/test_csv_importer.py +0 -0
  292. {execsql2-2.22.9 → execsql2-2.23.1}/tests/importers/test_feather_importer.py +0 -0
  293. {execsql2-2.22.9 → execsql2-2.23.1}/tests/importers/test_json_importer.py +0 -0
  294. {execsql2-2.22.9 → execsql2-2.23.1}/tests/importers/test_ods_importer.py +0 -0
  295. {execsql2-2.22.9 → execsql2-2.23.1}/tests/importers/test_xls_importer.py +0 -0
  296. {execsql2-2.22.9 → execsql2-2.23.1}/tests/integration/__init__.py +0 -0
  297. {execsql2-2.22.9 → execsql2-2.23.1}/tests/integration/conftest.py +0 -0
  298. {execsql2-2.22.9 → execsql2-2.23.1}/tests/integration/test_dsn.py +0 -0
  299. {execsql2-2.22.9 → execsql2-2.23.1}/tests/integration/test_duckdb.py +0 -0
  300. {execsql2-2.22.9 → execsql2-2.23.1}/tests/integration/test_mysql.py +0 -0
  301. {execsql2-2.22.9 → execsql2-2.23.1}/tests/integration/test_sqlite.py +0 -0
  302. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/__init__.py +0 -0
  303. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_assert.py +0 -0
  304. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_breakpoint.py +0 -0
  305. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_conditions_extra.py +0 -0
  306. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_connect.py +0 -0
  307. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_io_export.py +0 -0
  308. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_io_import.py +0 -0
  309. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands.py +0 -0
  310. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_connect.py +0 -0
  311. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  312. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_io.py +0 -0
  313. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  314. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  315. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_system.py +0 -0
  316. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  317. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_pg_upsert.py +0 -0
  318. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_prompt.py +0 -0
  319. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_row_count.py +0 -0
  320. {execsql2-2.22.9 → execsql2-2.23.1}/tests/metacommands/test_show_scripts.py +0 -0
  321. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/__init__.py +0 -0
  322. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  323. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  324. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  325. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/audit_smoke.sql +0 -0
  326. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/config_runtime.sql +0 -0
  327. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/control_flow.sql +0 -0
  328. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  329. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/error_handling.sql +0 -0
  330. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  331. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/import_options.sql +0 -0
  332. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/includes/helper.sql +0 -0
  333. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/includes.sql +0 -0
  334. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/io_formats.sql +0 -0
  335. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  336. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/multi_db.sql +0 -0
  337. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  338. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  339. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/predicates.sql +0 -0
  340. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/scripts.sql +0 -0
  341. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/smoke.sql +0 -0
  342. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  343. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  344. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  345. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/template_export.sql +0 -0
  346. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/timer.sql +0 -0
  347. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/transactions.sql +0 -0
  348. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/write_create_table.sql +0 -0
  349. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  350. {execsql2-2.22.9 → execsql2-2.23.1}/tests/scripts/test_sql_scripts.py +0 -0
  351. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/__init__.py +0 -0
  352. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/test_env_detection.py +0 -0
  353. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/test_expansion_bomb.py +0 -0
  354. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/test_import_regex_hardening.py +0 -0
  355. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/test_log_redaction.py +0 -0
  356. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/test_odbc_injection.py +0 -0
  357. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/test_path_containment.py +0 -0
  358. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/test_substitution_injection.py +0 -0
  359. {execsql2-2.22.9 → execsql2-2.23.1}/tests/security/test_zip_bomb.py +0 -0
  360. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_api.py +0 -0
  361. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_api_extended.py +0 -0
  362. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_ast.py +0 -0
  363. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_ast_parser.py +0 -0
  364. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_bundled_templates.py +0 -0
  365. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_config.py +0 -0
  366. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_config_data.py +0 -0
  367. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_config_extended.py +0 -0
  368. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_debug_repl.py +0 -0
  369. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_engine.py +0 -0
  370. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_error_messages.py +0 -0
  371. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_exceptions.py +0 -0
  372. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_executor.py +0 -0
  373. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_executor_inprocess.py +0 -0
  374. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_format.py +0 -0
  375. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_mail.py +0 -0
  376. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_models.py +0 -0
  377. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_package.py +0 -0
  378. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_parser.py +0 -0
  379. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_parser_params.py +0 -0
  380. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_plugins.py +0 -0
  381. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_registry.py +0 -0
  382. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_script.py +0 -0
  383. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_state.py +0 -0
  384. {execsql2-2.22.9 → execsql2-2.23.1}/tests/test_types.py +0 -0
  385. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/__init__.py +0 -0
  386. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_auth.py +0 -0
  387. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_auth_extra.py +0 -0
  388. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_crypto.py +0 -0
  389. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_datetime.py +0 -0
  390. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_encodedfile_context.py +0 -0
  391. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_errors.py +0 -0
  392. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_errors_extra.py +0 -0
  393. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_fileio.py +0 -0
  394. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_fileio_extra.py +0 -0
  395. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_numeric.py +0 -0
  396. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_regex.py +0 -0
  397. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_strings.py +0 -0
  398. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_timer.py +0 -0
  399. {execsql2-2.22.9 → execsql2-2.23.1}/tests/utils/test_timer_extra.py +0 -0
  400. {execsql2-2.22.9 → execsql2-2.23.1}/zensical.toml +0 -0
@@ -20,7 +20,7 @@ jobs:
20
20
  permissions:
21
21
  contents: read
22
22
  steps:
23
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
23
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24
24
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
25
25
  with:
26
26
  python-version: "3.13"
@@ -34,7 +34,7 @@ jobs:
34
34
  permissions:
35
35
  contents: read
36
36
  steps:
37
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
37
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
38
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
39
39
  with:
40
40
  python-version: "3.13"
@@ -49,7 +49,7 @@ jobs:
49
49
  permissions:
50
50
  contents: read
51
51
  steps:
52
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
52
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
53
53
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
54
54
  with:
55
55
  python-version: "3.13"
@@ -67,7 +67,7 @@ jobs:
67
67
  permissions:
68
68
  contents: read
69
69
  steps:
70
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
70
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
71
71
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
72
72
  with:
73
73
  python-version: "3.13"
@@ -97,13 +97,13 @@ jobs:
97
97
  contents: read
98
98
  steps:
99
99
  - name: Check out repository code
100
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
100
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
101
101
  - name: Setup Python ${{ matrix.python-version }}
102
102
  uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
103
103
  with:
104
104
  python-version: ${{ matrix.python-version }}
105
105
  - name: Cache pip dependencies
106
- uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
106
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
107
107
  with:
108
108
  path: ~/.cache/pip
109
109
  key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{
@@ -135,13 +135,13 @@ jobs:
135
135
  tox -e py
136
136
  - name: Upload coverage to Codecov
137
137
  if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
138
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
138
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
139
139
  with:
140
140
  token: ${{ secrets.CODECOV_TOKEN }}
141
141
  files: coverage.xml
142
142
 
143
143
  integration-tests:
144
- name: integration-tests-py${{ matrix.python-version }}
144
+ name: tier1-live-db-tests-py${{ matrix.python-version }}
145
145
  runs-on: ubuntu-latest
146
146
  strategy:
147
147
  fail-fast: false
@@ -188,7 +188,7 @@ jobs:
188
188
  --health-interval 10s --health-timeout 5s --health-retries 20
189
189
  steps:
190
190
  - name: Check out repository code
191
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
191
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
192
192
  - name: Setup Python ${{ matrix.python-version }}
193
193
  uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
194
194
  with:
@@ -203,6 +203,13 @@ jobs:
203
203
  sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
204
204
  odbcinst -j
205
205
  odbcinst -q -d
206
+ - name: Enable MySQL LOAD DATA LOCAL INFILE
207
+ # The MySQL importer uses LOAD DATA LOCAL INFILE, which MySQL 8
208
+ # disables server-side by default. A service container takes no
209
+ # command line, so it is switched on here; without it the import
210
+ # round-trip tests skip instead of running.
211
+ run: |
212
+ mysql -h 127.0.0.1 -P 3306 -u root -proot -e "SET GLOBAL local_infile = 1;"
206
213
  - name: Install dependencies
207
214
  run: |
208
215
  python -m pip install --upgrade pip
@@ -216,13 +223,13 @@ jobs:
216
223
  run: |
217
224
  python -m pytest --cov=execsql --cov-branch --cov-report=xml --cov-fail-under=86
218
225
  - name: Upload coverage to Codecov
219
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
226
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
220
227
  with:
221
228
  token: ${{ secrets.CODECOV_TOKEN }}
222
229
  files: coverage.xml
223
230
 
224
231
  access-tests-windows:
225
- name: access-tests-windows
232
+ name: tier2-access-tests-windows
226
233
  runs-on: windows-latest
227
234
  # The Access engine install step is best-effort (the redistributable
228
235
  # download from Microsoft can be flaky on hosted runners). The
@@ -236,7 +243,7 @@ jobs:
236
243
  permissions:
237
244
  contents: read
238
245
  steps:
239
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
246
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
240
247
  - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
241
248
  with:
242
249
  python-version: ${{ matrix.python-version }}
@@ -288,7 +295,7 @@ jobs:
288
295
  permissions:
289
296
  contents: read
290
297
  steps:
291
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
298
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
292
299
  - name: Set up Python
293
300
  uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
294
301
  with:
@@ -343,14 +350,14 @@ jobs:
343
350
  contents: write
344
351
  steps:
345
352
  - name: Checkout
346
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
353
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
347
354
  - name: Download dist
348
355
  uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
349
356
  with:
350
357
  name: ${{ github.event.repository.name }}
351
358
  path: dist/
352
359
  - name: Create release
353
- uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
360
+ uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
354
361
  with:
355
362
  draft: false
356
363
  prerelease: false
@@ -51,6 +51,7 @@ _execsql/
51
51
  CLAUDE.md
52
52
  AUDIT.md
53
53
  DOC_AUDIT.md
54
+ ROADMAP.md
54
55
  PROMPTS.md
55
56
  audit/
56
57
  AGENTS.md
@@ -13,6 +13,56 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.23.1] - 2026-09-29
17
+
18
+ ### Added
19
+
20
+ - A warning when MySQL, MariaDB or Oracle commits a statement on its own while `AUTOCOMMIT` is `OFF` or a batch is open — `CREATE TABLE`, `DROP`, `TRUNCATE` and other DDL, which the server commits together with everything before it, so none of it can be rolled back.
21
+ - A warning when `AUTOCOMMIT OFF` or `BEGIN BATCH` cannot hold statements at all: an ODBC DSN whose driver has no transactions, or an MS Access `CREATE TEMPORARY QUERY`, which reopens the connection and rolls back what was not yet committed.
22
+
23
+ ### Fixed
24
+
25
+ - SQLite: `CREATE TABLE`, `DROP` and other DDL run while `AUTOCOMMIT` is `OFF` or inside `BEGIN BATCH` are held until commit. They were committed at once, so `AUTOCOMMIT ON WITH ROLLBACK` and `ROLLBACK BATCH` left them in place.
26
+ - DuckDB: `AUTOCOMMIT OFF` and `BEGIN BATCH` hold statements until commit. Every statement, rows included, was committed as it ran, so nothing could be rolled back. A script's own `BEGIN;` and `COMMIT;` now work too; `COMMIT;` failed with "no transaction is active".
27
+ - DuckDB: `$LAST_ROWCOUNT` holds the number of rows an `INSERT`, `UPDATE` or `DELETE` changed. It was always `-1`.
28
+
29
+ ______________________________________________________________________
30
+
31
+ ## [2.23.0] - 2026-09-29
32
+
33
+ ### Added
34
+
35
+ - Database support tiers. PostgreSQL, MySQL/MariaDB, MS SQL Server, SQLite, and DuckDB are **supported** — verified against a live server or real database file on every CI run. MS Access, Firebird, Oracle, and ODBC DSN are **best effort** — present, unverified in CI, and may break. No adapter changes behavior or is scheduled for removal.
36
+ - `support_tier_notice` configuration option (`[interface]` section, default `Yes`). Opening a best-effort database connection writes one line to stderr naming the tier, once per DBMS per run. Set it to `No` to silence.
37
+
38
+ ### Changed
39
+
40
+ - MySQL and MariaDB connections now default to `utf8mb4` instead of `latin1`. A latin1 connection could not carry CJK, emoji, Greek, or Cyrillic at all — inserting such text raised `UnicodeEncodeError` before reaching the server — so scripts had to pass `-e utf8mb4` to move most of Unicode. Existing latin1 databases are unaffected: the connection charset governs only client/server transfer, and MySQL transcodes to and from each column's own charset. Pass `-e latin1` to restore the previous behavior.
41
+
42
+ ### Fixed
43
+
44
+ - `execsql.api.run()` no longer discards every `WRITE ... TO <file>` and `TEE` to a file. Those metacommands hand their output to a FileWriter subprocess that only the CLI started, so under the library API the file was never created and the run still reported success ([#46](https://github.com/geocoug/execsql/issues/46)). `run()` now starts the writer, and flushes and closes every file before returning, so output is readable as soon as it hands back control. A writer the caller started themselves is left untouched.
45
+ - File output that cannot be written because no FileWriter is running now reports a warning instead of being dropped in silence. The write is still skipped — queueing to a subprocess that is not draining the queue deadlocks the caller — but a script's logfile no longer comes back empty with no indication anything was missed.
46
+ - `IMPORT` reads CSV files containing a quoted field with a newline. RFC 4180 allows it and execsql's own CSV export produces it, but format diagnosis scanned physical lines, so the delimiter looked inconsistent and was rejected: the header arrived as a single column and the record split into two rows with the quote characters still in the data. Exporting a multi-line value and importing it back now round-trips.
47
+ - `IMPORT` to MySQL writes an empty field as NULL instead of the column's zero value. `LOAD DATA` coerced an empty numeric to `0` and an empty date to `0000-00-00`, so the same file imported to MySQL differed from every other backend.
48
+ - `IMPORT ... WITH QUOTE NONE` to PostgreSQL keeps double quotes in the data. `Well "A" 12` was stored as `Well A 12`, and the same happened when no quote character was detected ([#54](https://github.com/geocoug/execsql/issues/54)).
49
+ - `IMPORT` to PostgreSQL honors a detected backslash escape. A quoted field containing `\"` was stored with the backslash and without the quote, so `"Well \"A\" 12"` became `Well \A\ 12`.
50
+ - `IMPORT` to MySQL reads CRLF files correctly. `LOAD DATA` defaults to `\n` as its line terminator, so every row's last field kept a trailing carriage return — enough to turn an empty date column into `0000-00-00`.
51
+ - `EXPORT ... AS CSV` (and the other delimited formats) no longer corrupts values containing a newline when run on Windows. Universal-newline translation rewrote every `\n` as `\r\n` on write, including newlines *inside* a quoted field, so an exported value did not match the one in the database. Delimited files now use `\n` row terminators on every platform, so the same query produces the same bytes everywhere.
52
+ - `EXPORT ... AS TAB`/console pretty-printing keeps every row on one line. A newline or tab inside a value split the row across physical lines and misaligned every column after it; both are now shown as `\\n` and `\\t`, so the table stays square and the break is still visible.
53
+ - `EXPORT ... AS VALUES` quotes dates, timestamps, and every other non-numeric value in the generated `INSERT`. Only strings were quoted, so a date was written bare as `2026-09-24` — which SQL reads as arithmetic, not a date. PostgreSQL accepted the statement and stored `1993` without complaint; a timestamp, carrying a space, was a syntax error instead. Numbers are still written bare, and `NULL` is unchanged.
54
+ - `EXPORT ... AS LATEX` escapes every LaTeX special character, not only `_`. An `&` in a value acted as a column separator and produced a table with the wrong number of columns, which LaTeX refuses to compile ("Extra alignment tab has been changed to \\cr"); a `%` commented out the rest of the row. `\\ & % $ # _ { } ~ ^` are now all escaped.
55
+ - `EXPORT ... AS LATEX` writes NULL as an empty cell instead of the literal text `None`.
56
+ - `EXPORT ... AS HTML` renders zero and other falsy values instead of blank cells. Cells were written with `str(v) if v else ''`, so `0`, `False`, and empty strings came out empty — a zero measurement was indistinguishable from a NULL in the output.
57
+ - `EXPORT ... AS MARKDOWN` no longer breaks the table when a value contains a newline. A GitHub-flavoured Markdown row is a single line, so a literal newline ended the row mid-cell and left the remaining values on a stray line outside the table. Line breaks are now written as `<br>`.
58
+ - `EXPORT ... AS ODS` writes `numeric`/`decimal` columns as typed numbers. PostgreSQL, MySQL, and DuckDB return `Decimal` for those columns, which was written with an `office:value` but no `office:value-type` — not valid ODF, leaving a reader free to treat the number as text or ignore it.
59
+ - `EXPORT ... AS ODS` keeps newlines inside a value instead of replacing them with spaces. Multi-line text is now written as one line per `<text:p>`, which is how ODF represents it; previously the exported cell did not match the value in the database.
60
+ - `EXPORT ... AS XLSX` writes `numeric`/`decimal` columns as numbers instead of text. PostgreSQL, MySQL, and DuckDB return `Decimal` for those columns, which fell through to a string cell — a measurement column arrived in Excel as text and could not be summed, sorted, or charted. Integer and float columns were unaffected.
61
+ - Exports and copies to DuckDB no longer lose floating-point precision. `DT_Float` is IEEE double precision and a Python float always is one, but DuckDB columns were declared `REAL` — single precision — so `1234.567` came back as `1234.5670166015625`. DuckDB columns are now `DOUBLE`, matching every other backend. Affects `EXPORT ... AS DUCKDB` and `COPY` into a DuckDB database.
62
+ - Jinja2 report templates with an `.html`, `.htm`, or `.xml` extension now escape the values rendered into them. Autoescaping was off, so a database value containing markup — `<script>`, `&`, a quote — became live markup in the generated report. Templates with any other extension (`.csv`, `.tex`, `.txt`, `.sql`, `.md`) are unchanged, since escaping those would corrupt them. A template that means to emit markup uses `{{ value|safe }}`.
63
+
64
+ ______________________________________________________________________
65
+
16
66
  ## [2.22.9] - 2026-09-24
17
67
 
18
68
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.22.9
3
+ Version: 2.23.1
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
@@ -224,17 +224,26 @@ execsql script.sql # read connection from config file
224
224
 
225
225
  ### Supported Databases
226
226
 
227
- | Flag | Database |
228
- | ---- | --------------- |
229
- | `p` | PostgreSQL |
230
- | `m` | MySQL / MariaDB |
231
- | `s` | MS SQL Server |
232
- | `l` | SQLite |
233
- | `k` | DuckDB |
234
- | `a` | MS Access |
235
- | `f` | Firebird |
236
- | `o` | Oracle |
237
- | `d` | ODBC DSN |
227
+ | Flag | Database | Support |
228
+ | ---- | --------------- | ------------------------------------------------ |
229
+ | `p` | PostgreSQL | Supported — verified against a live server in CI |
230
+ | `m` | MySQL / MariaDB | Supported — verified against a live server in CI |
231
+ | `s` | MS SQL Server | Supported — verified against a live server in CI |
232
+ | `l` | SQLite | Supported — verified against real files in CI |
233
+ | `k` | DuckDB | Supported — verified against real files in CI |
234
+ | `a` | MS Access | Best effort — not verified in CI |
235
+ | `f` | Firebird | Best effort — not verified in CI |
236
+ | `o` | Oracle | Best effort — not verified in CI |
237
+ | `d` | ODBC DSN | Best effort — not verified in CI |
238
+
239
+ **Supported** adapters run against a live server or a real database file on
240
+ every CI run; a regression blocks the release and bugs get fixed. **Best
241
+ effort** adapters are carried forward from the upstream monolith and are not
242
+ exercised anywhere in CI — the code is there and may work, but nothing proves
243
+ it still does. Issues and pull requests are welcome for them; no guarantee is
244
+ made. Opening a best-effort connection prints one informational line, which
245
+ `support_tier_notice=No` in the `[interface]` section of `execsql.conf`
246
+ silences.
238
247
 
239
248
  ### Common options
240
249
 
@@ -384,7 +393,7 @@ execsql-format --no-sql --in-place scripts/
384
393
  ```yaml
385
394
  repos:
386
395
  - repo: https://github.com/geocoug/execsql
387
- rev: v2.22.9
396
+ rev: v2.23.1
388
397
  hooks:
389
398
  - id: execsql-format
390
399
  ```
@@ -93,17 +93,26 @@ execsql script.sql # read connection from config file
93
93
 
94
94
  ### Supported Databases
95
95
 
96
- | Flag | Database |
97
- | ---- | --------------- |
98
- | `p` | PostgreSQL |
99
- | `m` | MySQL / MariaDB |
100
- | `s` | MS SQL Server |
101
- | `l` | SQLite |
102
- | `k` | DuckDB |
103
- | `a` | MS Access |
104
- | `f` | Firebird |
105
- | `o` | Oracle |
106
- | `d` | ODBC DSN |
96
+ | Flag | Database | Support |
97
+ | ---- | --------------- | ------------------------------------------------ |
98
+ | `p` | PostgreSQL | Supported — verified against a live server in CI |
99
+ | `m` | MySQL / MariaDB | Supported — verified against a live server in CI |
100
+ | `s` | MS SQL Server | Supported — verified against a live server in CI |
101
+ | `l` | SQLite | Supported — verified against real files in CI |
102
+ | `k` | DuckDB | Supported — verified against real files in CI |
103
+ | `a` | MS Access | Best effort — not verified in CI |
104
+ | `f` | Firebird | Best effort — not verified in CI |
105
+ | `o` | Oracle | Best effort — not verified in CI |
106
+ | `d` | ODBC DSN | Best effort — not verified in CI |
107
+
108
+ **Supported** adapters run against a live server or a real database file on
109
+ every CI run; a regression blocks the release and bugs get fixed. **Best
110
+ effort** adapters are carried forward from the upstream monolith and are not
111
+ exercised anywhere in CI — the code is there and may work, but nothing proves
112
+ it still does. Issues and pull requests are welcome for them; no guarantee is
113
+ made. Opening a best-effort connection prints one informational line, which
114
+ `support_tier_notice=No` in the `[interface]` section of `execsql.conf`
115
+ silences.
107
116
 
108
117
  ### Common options
109
118
 
@@ -253,7 +262,7 @@ execsql-format --no-sql --in-place scripts/
253
262
  ```yaml
254
263
  repos:
255
264
  - repo: https://github.com/geocoug/execsql
256
- rev: v2.22.9
265
+ rev: v2.23.1
257
266
  hooks:
258
267
  - id: execsql-format
259
268
  ```
@@ -184,6 +184,18 @@ The CLI framework changed from `optparse` to [Typer](https://typer.tiangolo.com/
184
184
 
185
185
  Seven upstream long-form flags were renamed underscore → hyphen and the underscore forms are **not** accepted: `--database-encoding`, `--script-encoding`, `--output-encoding`, `--import-encoding`, `--import-buffer`, `--user-logfile`, `--visible-prompts` (upstream wrote these with underscores). Scripts and CI pipelines that invoke the long-form flags must update the spelling; the short letters (`-e`, `-f`, `-g`, `-i`, `-l`, `-v`, `-z`) are unchanged.
186
186
 
187
+ ### MySQL Connection Encoding
188
+
189
+ Upstream opened MySQL and MariaDB connections as `latin1`, and execsql2 did the same until this was measured against a live server. A latin1 connection cannot carry CJK, emoji, Greek, or Cyrillic — pymysql raises `UnicodeEncodeError` before the server is reached — so any script moving text outside Latin-1 had to pass `-e utf8mb4` explicitly or fail.
190
+
191
+ The default is now `utf8mb4`. This does not change stored data or column charsets: the connection charset governs only how text moves between client and server, and MySQL transcodes to and from each column's own charset. A latin1 column read over a utf8mb4 connection returns the same string it always did. `-e latin1` restores the upstream behavior.
192
+
193
+ ### Database Support Tiers
194
+
195
+ Upstream documented all nine DBMS adapters with equal confidence. execsql2 splits them by what is actually verified: **Supported** (PostgreSQL, MySQL/MariaDB, MS SQL Server, SQLite, DuckDB) run against a live server or a real database file on every CI run, while **best effort** (MS Access, Firebird, Oracle, ODBC DSN) are carried forward from the monolith and are exercised nowhere in CI.
196
+
197
+ No adapter is removed, deprecated, or changed in behavior — the tiers describe verification, not intent. Opening a best-effort connection writes one informational line to stderr per DBMS per run, which `support_tier_notice=No` in the `[interface]` section silences. Adapters declare their own tier through `Database.support_tier`, and `tests/db/test_tiers.py` fails if the code and the documented tables disagree.
198
+
187
199
  ### Default Database Type
188
200
 
189
201
  The default database type (`-t`) changed from Access (`a`) to SQLite (`l`). Upstream defaulted to Access, which requires Windows and pyodbc. SQLite is cross-platform, ships with Python, and is the most common use case. Users targeting Access databases should pass `-t a` explicitly.
@@ -216,7 +228,9 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
216
228
  - **Connection timeouts** — PostgreSQL and SQLite adapters accept a connection timeout parameter (default 30 seconds).
217
229
  - **DuckDB temporal types** — `TIMESTAMPTZ`, `TIMESTAMP`, `DATE`, `TIME` now map to native DuckDB types instead of `TEXT`.
218
230
  - **SQLite `DT_Long` mapping** — `DT_Long` maps to `"hugeint"` in the SQLite type table. SQLite does not have a native `HUGEINT` type; the value receives `TEXT` affinity. In practice this is harmless because SQLite's type affinity system handles large integers transparently, but the mapping name differs from upstream.
219
- - **`Database.auto_commits_ddl()` not provided** — 2.18.0 shipped this capability hook on the base `Database` class with `True` overrides on Oracle, MySQL, SQL Server, and MS Access (to signal that DDL on those drivers implicitly commits, so `rollback()` is a silent no-op across the DDL boundary). No call site ever consumed the hook, so 2.19.0 removed it. The asymmetry it described is still real — DDL inside a `BEGIN BATCH … END BATCH` block on Oracle / MySQL / SQL Server / MS Access cannot be rolled back — but execsql does not currently warn callers about it. (The companion hook `Database.needs_explicit_commit_after_ddl()`, used by the Firebird import path, remains.)
231
+ - **`Database.auto_commits_ddl()` not provided** — 2.18.0 shipped this capability hook on the base `Database` class with `True` overrides on Oracle, MySQL, SQL Server, and MS Access. No call site ever consumed the hook, so 2.19.0 removed it. Its replacement is `Database.implicit_commit_rx`: MySQL and Oracle name the statements their server commits on its own (DDL), and a warning is written when one runs while `AUTOCOMMIT` is `OFF` or a batch is open. SQL Server holds DDL in a transaction like any other statement. (The companion hook `Database.needs_explicit_commit_after_ddl()`, used by the Firebird import path, remains.)
232
+ - **Statements are held until commit on every adapter** — while `AUTOCOMMIT` is `OFF` or a batch is open, `Database.before_statement()` makes sure each statement is inside a transaction. Most drivers already opened one; SQLite's (Python's `sqlite3`) opened one only before `INSERT`/`UPDATE`/`DELETE`/`REPLACE`, and DuckDB's never did, so upstream committed SQLite DDL, and every DuckDB statement, at once. SQLite now begins a transaction before other statements (except `VACUUM`, `ATTACH`, `DETACH`, `PRAGMA`); DuckDB begins one itself, tracks it, and runs statements on the connection rather than on cursors while it is open, since a DuckDB cursor is a separate connection with its own transaction.
233
+ - **DuckDB `$LAST_ROWCOUNT`** — the duckdb driver's `rowcount` is always `-1`; DuckDB answers an `INSERT`, `UPDATE` or `DELETE` with a one-column `Count` result instead. The DuckDB adapter reads that result, so `$LAST_ROWCOUNT` holds the rows changed, as on the other adapters. Upstream set it to `-1`.
220
234
 
221
235
  ### Execution Engine
222
236
 
@@ -253,14 +267,15 @@ These are behavioral changes driven by security or correctness issues in the ups
253
267
 
254
268
  ### Template and Export Safety
255
269
 
256
- | Area | Fix |
257
- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
258
- | Jinja2 sandboxing | Templates run in `SandboxedEnvironment` instead of the default `jinja2.Template`. |
259
- | HTML export | Column headers and cell values are escaped with `html.escape()` to prevent XSS. |
260
- | XML export | Values are escaped with `xml.sax.saxutils.escape()`. Invalid XML element name characters are replaced. |
261
- | JSON export | The `description` field and all column names use `json.dumps()` instead of string interpolation. |
262
- | XLSX zip-bomb defence | `EXPORT … FORMAT xlsx`, `EXPORT … FORMAT xls`, and (as of 2.19.0) `IMPORT … FORMAT xlsx` reject zip-based workbooks whose decompression ratio exceeds 100:1 per member or whose aggregate uncompressed size exceeds 500 MB. Helper at `execsql.utils.fileio.check_zip_decompression_ratio`. Legacy `.xls` (OLE-CDF, not zip) is unaffected — the helper no-ops on it. |
263
- | ODS XML attack defence | `EXPORT … FORMAT ods` / `IMPORT … FORMAT ods` defuse the stdlib XML parsers via `defusedxml.defuse_stdlib()` on first `OdsFile` construction, protecting odfpy from billion-laughs and external-entity attacks. |
270
+ | Area | Fix |
271
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
272
+ | Jinja2 reports | `SandboxedEnvironment` was created without `autoescape`, so Jinja2's default (off) applied and a value from the database rendered into an HTML report as live markup — a `<script>` in a text column became a script tag. The sandbox guards against a hostile template; nothing guarded against hostile data. Autoescaping is now selected by the template's extension: `html`, `htm` and `xml` escape, every other extension renders byte-identically to before, and a template that means to emit markup asks with \`{{ value |
273
+ | Jinja2 sandboxing | Templates run in `SandboxedEnvironment` instead of the default `jinja2.Template`. |
274
+ | HTML export | Column headers and cell values are escaped with `html.escape()` to prevent XSS. |
275
+ | XML export | Values are escaped with `xml.sax.saxutils.escape()`. Invalid XML element name characters are replaced. |
276
+ | JSON export | The `description` field and all column names use `json.dumps()` instead of string interpolation. |
277
+ | XLSX zip-bomb defence | `EXPORT … FORMAT xlsx`, `EXPORT … FORMAT xls`, and (as of 2.19.0) `IMPORT … FORMAT xlsx` reject zip-based workbooks whose decompression ratio exceeds 100:1 per member or whose aggregate uncompressed size exceeds 500 MB. Helper at `execsql.utils.fileio.check_zip_decompression_ratio`. Legacy `.xls` (OLE-CDF, not zip) is unaffected — the helper no-ops on it. |
278
+ | ODS XML attack defence | `EXPORT … FORMAT ods` / `IMPORT … FORMAT ods` defuse the stdlib XML parsers via `defusedxml.defuse_stdlib()` on first `OdsFile` construction, protecting odfpy from billion-laughs and external-entity attacks. |
264
279
 
265
280
  ### Credential and Logging Safety
266
281
 
@@ -299,6 +314,8 @@ These are behavioral changes driven by security or correctness issues in the ups
299
314
  | `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. |
300
315
  | `CounterVars.substitute` skipped pos 0–1 | `re.I` was passed as the positional `pos` argument, skipping the first 2 characters of every string during counter variable expansion. Counter variables at the very start of a line were never matched. |
301
316
  | `exec_cmd` (SQLite/DuckDB) always raised TypeError | Passed bytes to `curs.execute()` via `.encode()`, which Python 3 `sqlite3`/`duckdb` reject. `EXECUTE PROCEDURE` metacommand was non-functional on these backends. |
317
+ | PostgreSQL `IMPORT` with no quote character | The `COPY` fast path uses COPY's CSV format, whose quote character is `"` unless another is named. With `QUOTE NONE`, or when no quote character was detected, none was named, so `"` was treated as a quote and stripped: `Well "A" 12` was stored as `Well A 12`. Such files now take the row-by-row path, which keeps every character (#54). |
318
+ | PostgreSQL `IMPORT` with a detected escape character | The `COPY` fast path was never told about a detected escape character, so `\"` inside a quoted field was stored as `\`. Such files now take the row-by-row path, which reads the detected format. |
302
319
  | MySQL `LOAD DATA INFILE` injection | File path, delimiter, and quotechar were interpolated without escaping. Now single-quotes are escaped consistent with the PostgreSQL COPY path. |
303
320
  | Missing server/database name at connect | Connecting to a server-based database without a server or database name now stops with a clear fatal error ("Configured database requires a server name, but none is provided.") instead of crashing inside the driver with a `TypeError`. |
304
321
  | Config file chain infinite loop | The `config_file` directive could chain config files without limit. A circular reference (via symlinks or different relative paths) caused an infinite loop at startup. Now capped at 20 files. |
@@ -325,6 +342,20 @@ These are behavioral changes driven by security or correctness issues in the ups
325
342
  | Formatter dropped clauses from unparsable SQL | The sqlglot parse ran at `ErrorLevel.IGNORE`, which answers a statement it cannot parse with a partial AST rather than an error — the tokens it failed on are absent, and regenerating from that AST wrote back SQL the author never had. `INSERT ... CROSS JOIN LATERAL (VALUES ...) AS g(a, b) ON CONFLICT (a) DO NOTHING` came back without its `DO NOTHING`, a syntax error that `--in-place` reported as `reformatted`. Every guard passed it: one statement for one semicolon, 88% of the input's alphanumeric characters, and no string literals to round-trip. The guards are quantitative and the loss is qualitative — a dropped `DISTINCT`, `CASCADE`, `DESC`, or `NOT` is smaller still — so the parse now runs at `ErrorLevel.RAISE` and SQL that does not fully parse is emitted exactly as written. |
326
343
  | Formatter moved a comment into the middle of an expression | Comments are hidden from sqlglot as `/* marker */ line`, and sqlglot attaches the marker to an AST node. When it then reflows that node across several lines the marker surfaces deep inside the expression — `COALESCE((a.meas_value).undetected, FALSE)` carried it onto the `).undetected, FALSE)` continuation line — so restoring the comment above the marker split the expression around it, and the resulting layout reformatted differently on the next run. Comments whose markers sqlglot dropped entirely (inside a `CASE`) were re-inserted by token matching, which picked continuation lines the same way. Both paths now place the comment above the line where its expression opens, found by walking back to the parenthesis depth the comment sat at in the input. |
327
344
  | Formatter left a space before punctuation where a comment had been | Marker runs are replaced by the one space that keeps their neighbours apart, so `AND /* m */ RTRIM(` does not become `ANDRTRIM(`. Where sqlglot placed the run against a `;`, `,`, `)`, or `.` that space was residue rather than a separator: `1 = 1 /* m */;` came back as `1 = 1 ;`, normalised only on the next run, so `--check` failed on output the formatter had just produced. The run is now removed without a space at the ends of a line and against punctuation that binds to the token before it. |
345
+ | Delimited export corrupted embedded newlines on Windows | `EncodedFile.open()` passed `newline=None`, so Python rewrote every `\n` written as `os.linesep`. That is right for row terminators and wrong for data: a newline inside a quoted CSV field was rewritten too, and the value read back was not the value exported. Python's own `csv` documentation calls for `newline=""` for exactly this reason. The delimited writer now opts out of translation, so row terminators are `\n` on every platform and a query produces identical bytes everywhere. Found by exporting a value containing a newline — nothing in the suite had ever done so — which failed on all five Windows runners and none of the others. |
346
+ | CSV import could not read a quoted field containing a newline | `diagnose_delim()` iterated physical lines, so a record split across two of them gave inconsistent delimiter counts and the real delimiter was rejected. The header came back as one column and the record as two rows with the quotes retained. Lines are now joined while a quote is open, so diagnosis sees logical records. RFC 4180 allows the construct, the stdlib reads it, and execsql's own CSV writer produces it — so this was a failure to read back what it had just written. |
347
+ | MySQL import turned empty fields into zero values | `LOAD DATA` coerces an empty field to the column's zero value: an empty numeric became `0` and an empty date `0000-00-00`, while every other backend produced NULL. Columns are now read into user variables and `NULLIF`-ed back. |
348
+ | MySQL import left a carriage return on the last field of each row | `LOAD DATA` defaults to `\n` as its line terminator, so a CRLF file — what Python's `csv` writer produces by default — left `\r` on each row's final value. It was not empty, so the `NULLIF` above could not see it, and a date column became `0000-00-00`. The statement now declares `lines terminated by '\r\n'` when the file is CRLF. |
349
+ | Pretty-printed tables were broken by a newline or tab in a value | The fixed-width writer emitted string values as-is, so a newline ended the row mid-cell — the remaining columns appeared on a line with no table structure — and a tab misaligned everything after it. Both are now rendered as their two-character escapes. This is a display format, so the escape keeps the row on one line, keeps the columns square, and still shows the reader that a break is present. |
350
+ | DuckDB float columns were single precision | `dbt_duckdb` mapped `DT_Float` to `REAL`. `DT_Float` is documented as IEEE double precision and a Python float always is one, so every float written to DuckDB was narrowed: `1234.567` was stored and returned as `1234.5670166015625`. PostgreSQL, SQLite, SQL Server, MySQL, Access, Firebird and DSN all map `DT_Float` to a double; DuckDB was the only backend that did not, and is now `DOUBLE`. |
351
+ | VALUES export wrote dates and timestamps unquoted | The literal renderer quoted `str` and emitted `NULL` for `None`, but sent everything else through `str()` bare. A `date` became `2026-09-24` in the generated `INSERT`, which is not a syntax error — it is a valid SQL expression evaluating to 1993, so PostgreSQL inserted that and reported nothing. A `timestamp` contains a space and failed outright. Quoting is now the default and only numbers are emitted bare, so a type the renderer has not been taught about is safe rather than silently wrong. |
352
+ | LaTeX export escaped only the underscore | Cells were written as `str(c).replace("_", r"\_")`, leaving every other LaTeX special raw. `&` is the column separator this exporter itself emits, so an ampersand in a value added a column and the `.tex` no longer compiled; `%` commented out the rest of the row. All of `\ & % $ # _ { } ~ ^` are now escaped, in both the header and the body. |
353
+ | LaTeX export wrote NULL as the literal "None" | `str(c)` was applied to every value including `None`, so a NULL appeared in the typeset table as the word None. It is now an empty cell, matching every other exporter. |
354
+ | HTML export rendered zero as an empty cell | Cells were written as `html.escape(str(v)) if v else ''`. `0`, `False`, and `""` are all falsy, so a zero measurement rendered exactly like a NULL and the two could not be told apart in the output. The test is now `if v is not None`. Both `export_html` and `export_cgi_html` were affected. |
355
+ | Markdown export broke the table on an embedded newline | `_cell()` escaped pipes but not newlines, and a GFM table row is a single line: a value containing one ended the row mid-cell and left the rest of that row's values on a stray line outside the table entirely. Newlines are now written as `<br>`, which is how GFM expresses a line break inside a cell. |
356
+ | ODS export wrote `numeric` columns with no type | The cell-type dispatch tested \`isinstance(item, float |
357
+ | ODS export replaced newlines inside a value with spaces | String cells were written after `item.replace("\n", " ")`, so an exported value silently differed from the one in the database. An XML parser does normalise a newline inside an attribute, so `office:string-value` cannot carry one — but ODF represents a multi-line cell as one `<text:p>` per line, which is what LibreOffice writes and what execsql2 now writes. Single-line values still use the attribute. |
358
+ | XLSX export wrote `numeric` columns as text | `_cell_value()` passed `int` and `float` straight to openpyxl but let every other type fall through to `str()`. PostgreSQL, MySQL, and DuckDB all return `Decimal` for `numeric`/`decimal` columns, so measurement values arrived in Excel as text cells — invisible in a screenshot, useless in a formula, a sort, or a chart. openpyxl lists `Decimal` in its own `NUMERIC_TYPES` and stores it as a real numeric cell, so the conversion was never needed. Found by exporting from a live PostgreSQL container and checking `cell.data_type` rather than `cell.value`. |
328
359
  | `EMAIL` SMTP login without a password | Upstream calls `smtplib.login()` with only a username when `[email] password` and `enc_password` are both unset, which raises `TypeError` from the stdlib — `login()` has no single-argument form. execsql2 reports a configuration error naming the missing password instead. |
329
360
 
330
361
  ______________________________________________________________________
@@ -48,6 +48,23 @@ for t in threads:
48
48
 
49
49
  Each thread gets its own database connections, IF/LOOP stacks, substitution variables, and error state. No locking is required.
50
50
 
51
+ ### File Output { #file-output }
52
+
53
+ `WRITE ... TO <file>`, `TEE`, and the other metacommands that write text files hand their output to a separate FileWriter process. `run()` starts one if none is running, and flushes and closes every file before returning — so a file a script wrote is on disk and complete the moment `run()` hands back control.
54
+
55
+ ```python
56
+ from execsql import run
57
+
58
+ result = run(sql='-- !x! WRITE "done" to report.txt\nselect 1;\n', dsn="sqlite:///data.db")
59
+ open("report.txt").read() # "done\n" — readable immediately
60
+ ```
61
+
62
+ The writer process stays up afterwards and is reused by later `run()` calls, exactly as the CLI keeps one for the life of the process; it is shut down automatically at interpreter exit. A writer you started yourself is left alone — `run()` neither replaces nor stops it.
63
+
64
+ !!! warning "Interactive interpreters"
65
+
66
+ On macOS and Windows, Python starts subprocesses with the `spawn` method, which re-imports the calling program's `__main__` module. That fails in a REPL, a notebook, or `python -c`, so the writer cannot start and file output is discarded — with a warning on stderr rather than in silence. Run scripts that write files from a `.py` file, guarded by `if __name__ == "__main__":`.
67
+
51
68
  ## Extension Guides
52
69
 
53
70
  | Extension type | Guide | API reference |
@@ -36,6 +36,32 @@ Multiple extras can be combined: `pip install "execsql2[postgres,duckdb,formats]
36
36
 
37
37
  The specific libraries installed by each extra are:
38
38
 
39
+ ### Database support tiers { #support-tiers }
40
+
41
+ execsql2 ships nine DBMS adapters, but they are not all verified to the same
42
+ standard. The tier tells you which ones have actually been run.
43
+
44
+ | Tier | Databases | What it means |
45
+ | --------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | **Supported** | PostgreSQL, MySQL/MariaDB, MS SQL Server, SQLite, DuckDB | Exercised against a live server (or a real database file) on every CI run. A regression blocks the release, and bugs are fixed. |
47
+ | **Best effort** | MS Access, Firebird, Oracle, ODBC DSN | Carried forward from the upstream monolith and not exercised in CI. The code is present and may work; nothing proves it still does. Issues and pull requests are welcome, but no guarantee is made. |
48
+
49
+ Opening a best-effort connection writes one informational line to stderr, once
50
+ per DBMS per run:
51
+
52
+ ```
53
+ Note: Firebird support is best-effort — it is not verified in CI and may break.
54
+ Set support_tier_notice=No in the [interface] section of execsql.conf to silence this.
55
+ ```
56
+
57
+ It goes to stderr rather than stdout, so it cannot corrupt piped query output.
58
+ To turn it off, set [`support_tier_notice`](../reference/configuration.md#support_tier_notice)
59
+ to `No`.
60
+
61
+ No adapter is deprecated or scheduled for removal. The tiers describe what is
62
+ verified, not what is wanted — a best-effort adapter moves up when it gains
63
+ tests that run against something real.
64
+
39
65
  ### Database drivers
40
66
 
41
67
  | Database / Format | Extra | Library |
@@ -8,7 +8,7 @@ If no encodings are specified, the following default encodings are used:
8
8
 
9
9
  > - Script file: utf8
10
10
  > - Firebird: latin1
11
- > - MySQL and MariaDB: latin1
11
+ > - MySQL and MariaDB: utf8mb4
12
12
  > - SQL Server: latin1
13
13
  > - Access: windows_1252
14
14
  > - DSN: None
@@ -188,7 +188,7 @@ select id,name,created_at from users where active = true order by name;
188
188
  ```yaml
189
189
  repos:
190
190
  - repo: https://github.com/geocoug/execsql
191
- rev: v2.22.9
191
+ rev: v2.23.1
192
192
  hooks:
193
193
  - id: execsql-format
194
194
  ```
@@ -233,6 +233,10 @@ The section and property names that may be used in a configuration file are list
233
233
 
234
234
  : Determines whether warning messages are written to the console as well as to the log file. The default value is "No", indicating that warnings will not be written to the console. If it is set to "Yes", warnings will be written to the console.
235
235
 
236
+ `support_tier_notice` { #support_tier_notice }
237
+
238
+ : Determines whether opening a connection to a [best-effort database](../getting-started/requirements.md#support-tiers) — MS Access, Firebird, Oracle, or an ODBC DSN — writes an informational line to stderr saying that the adapter is not verified in CI. The default value is "Yes". Set it to "No" to silence the notice. The message is written once per DBMS per run, and always to stderr rather than stdout, so it cannot corrupt piped query output. It is recorded in the log file regardless of this setting.
239
+
236
240
  `write_prefix`
237
241
 
238
242
  : Text that will be prefixed to any output from the WRITE metacommand, with a space separator. If substitution variables are used, deferred substitution may be appropriate.
@@ -115,6 +115,19 @@ SQL statements that were issued while AUTOCOMMIT is OFF can also be committed us
115
115
 
116
116
  The AUTOCOMMIT metacommand is database-specific, and affects only the database in use when the metacommand is used. This contrasts with the [BATCH](#batch) metacommand, which affects all databases.
117
117
 
118
+ **What each DBMS can hold back.** While AUTOCOMMIT is OFF, or inside a [batch](#batch), *execsql* keeps every statement in a transaction until it is committed or rolled back, including `CREATE TABLE` and other DDL. Some databases commit certain statements on their own regardless:
119
+
120
+ | DBMS | While AUTOCOMMIT is OFF or a batch is open |
121
+ | --- | --- |
122
+ | PostgreSQL, SQL Server, SQLite, DuckDB, Firebird | Every statement is held, DDL included, and can be rolled back. |
123
+ | MySQL / MariaDB | The server commits DDL (`CREATE`, `ALTER`, `DROP`, `RENAME`, `TRUNCATE`, `GRANT`, …) and `BEGIN` / `START TRANSACTION` at once, together with every statement before them. `CREATE TEMPORARY TABLE` is held. *execsql* writes a warning naming each such statement. |
124
+ | Oracle | The server commits every DDL statement at once, together with every statement before it. *execsql* writes a warning naming each one. |
125
+ | SQLite | `VACUUM`, `ATTACH`, `DETACH` and `PRAGMA` run outside a transaction, since SQLite refuses or ignores them inside one. |
126
+ | ODBC DSN | If the ODBC driver has no transactions, each statement is committed as it runs; *execsql* warns once. |
127
+ | MS Access | `CREATE TEMPORARY QUERY` reopens the connection, which rolls back statements not yet committed; *execsql* warns once. |
128
+
129
+ Before execsql2 fixed it, SQLite committed DDL at once and DuckDB committed every statement, even with AUTOCOMMIT OFF.
130
+
118
131
 
119
132
  The [IMPORT](#import) and [COPY](#copy) metacommands do not commit data changes while AUTOCOMMIT is off (except when the NEW or REPLACEMENT clauses are used with Firebird; in those cases the 'create table' statement that *execsql* generates and runs will be committed). The SQL statements generated by the [IMPORT](#import) and [COPY](#copy) metacommands are sent to the database, however. Therefore the AUTOCOMMIT metacommand is recommended when explicit transaction control is to be applied to the [IMPORT](#import) and [COPY](#copy) metacommands.
120
133
 
@@ -196,6 +209,8 @@ BEGIN BATCH marks the beginning of a set of SQL statements to be executed in a s
196
209
 
197
210
  Metacommands may be included inside a batch, but note that the [IMPORT](#import) and [COPY](#copy) metacommands always commit the changes they make, unless [AUTOCOMMIT](#autocommit) is OFF, so if IMPORT or COPY metacommands are used inside a batch, any preceding SQL statements in the batch will also be committed unless AUTOCOMMIT is OFF.
198
211
 
212
+ What a batch can hold back depends on the DBMS; see the table under [AUTOCOMMIT](#autocommit). MySQL and Oracle commit DDL inside a batch at once, with everything before it, and *execsql* warns when that happens.
213
+
199
214
  When the END BATCH metacommand is processed by *execsql*, a commit command is sent to all databases that have been used inside the batch. Multiple databases may be used inside a batch if the [USE](#use) metacommand is used inside the batch. The BATCH metacommands therefore provide a limited sort of cross-database transaction control — note, however, that the commit statements are issued sequentially, and if a later commit statement fails, this will not roll back changes to databases for which the commit has already succeeded.
200
215
 
201
216
  The BEGIN/END BATCH metacommands can be nested. However, the inner END BATCH metacommand will ordinarily commit all changes to the databases that have been used, which may include databases used in the outer batch as well. (This behavior may be DBMS-specific, and also depend on whether any additional explicit transaction control is being used.) Therefore completion of a nested batch may result in premature commitment of some or all SQL statements in the outer batch. Similarly, a ROLLBACK BATCH metacommand within the inner batch will also roll back any SQL commands sent to the same databases in the outer batch. Thus, although the BATCH commands can be nested, database transactions may not be, depending on the DBMS in use. Nesting of BATCH metacommands allows a script file or a [SCRIPT](#beginscript) containing a batch to be [INCLUDEEd](#include) or [EXECUTEEd](#executescript), respectively, within another batch.
@@ -148,7 +148,7 @@ $LAST_ERROR
148
148
  : The text of the last SQL statement or metacommand that caused an error. This value will only be available if the [ERROR_HALT OFF](metacommands.md#error_halt) or [METACOMMAND_ERROR_HALT OFF](metacommands.md#metacommanderrorhalt) metacommand has been used, or in an [ON ERROR_HALT EMAIL](metacommands.md#error_halt_email), [ON ERROR_HALT WRITE](metacommands.md#error_halt_write), or [ON ERROR_HALT EXECUTE SCRIPT](metacommands.md#error_halt_exec) metacommand.
149
149
 
150
150
  $LAST_ROWCOUNT
151
- : The number of rows that were affected by the last INSERT, UPDATE, or SELECT statement. Note that support for $LAST_ROWCOUNT varies among DBMSs. For example, for SELECT statements, Postgres provides an accurate count, SQLite always returns -1, Firebird always returns 0, and DuckDB does not provide a value.
151
+ : The number of rows that were affected by the last INSERT, UPDATE, or SELECT statement. Note that support for $LAST_ROWCOUNT varies among DBMSs. For example, for SELECT statements, Postgres provides an accurate count, SQLite and DuckDB always return -1, and Firebird always returns 0. For INSERT, UPDATE and DELETE, every supported DBMS reports the number of rows changed.
152
152
 
153
153
  $LAST_SQL
154
154
  : The text of the last SQL statement that ran without error.