execsql2 2.15.5__tar.gz → 2.15.7__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 (296) hide show
  1. {execsql2-2.15.5 → execsql2-2.15.7}/CHANGELOG.md +32 -0
  2. {execsql2-2.15.5 → execsql2-2.15.7}/PKG-INFO +1 -1
  3. {execsql2-2.15.5 → execsql2-2.15.7}/docs/about/divergence.md +10 -1
  4. {execsql2-2.15.5 → execsql2-2.15.7}/pyproject.toml +2 -2
  5. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/config.py +3 -0
  6. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/access.py +52 -51
  7. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/base.py +8 -8
  8. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/dsn.py +2 -1
  9. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/duckdb.py +1 -1
  10. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/firebird.py +56 -59
  11. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/mysql.py +99 -97
  12. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/oracle.py +63 -68
  13. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/postgres.py +141 -134
  14. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/sqlite.py +1 -1
  15. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/sqlserver.py +11 -12
  16. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exceptions.py +3 -3
  17. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/html.py +5 -3
  18. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/json.py +7 -2
  19. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/importers/csv.py +2 -2
  20. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/system.py +1 -2
  21. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/models.py +0 -1
  22. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/script/engine.py +2 -1
  23. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/script/variables.py +45 -1
  24. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/state.py +4 -1
  25. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/fileio.py +3 -2
  26. {execsql2-2.15.5 → execsql2-2.15.7}/tests/db/test_duckdb.py +1 -1
  27. {execsql2-2.15.5 → execsql2-2.15.7}/tests/db/test_sqlite_extra.py +7 -7
  28. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_script.py +91 -0
  29. {execsql2-2.15.5 → execsql2-2.15.7}/uv.lock +1 -1
  30. {execsql2-2.15.5 → execsql2-2.15.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  31. {execsql2-2.15.5 → execsql2-2.15.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  32. {execsql2-2.15.5 → execsql2-2.15.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  33. {execsql2-2.15.5 → execsql2-2.15.7}/.github/workflows/ci-cd.yml +0 -0
  34. {execsql2-2.15.5 → execsql2-2.15.7}/.gitignore +0 -0
  35. {execsql2-2.15.5 → execsql2-2.15.7}/.pre-commit-config.yaml +0 -0
  36. {execsql2-2.15.5 → execsql2-2.15.7}/.pre-commit-hooks.yaml +0 -0
  37. {execsql2-2.15.5 → execsql2-2.15.7}/.python-version +0 -0
  38. {execsql2-2.15.5 → execsql2-2.15.7}/.readthedocs.yaml +0 -0
  39. {execsql2-2.15.5 → execsql2-2.15.7}/CONTRIBUTING.md +0 -0
  40. {execsql2-2.15.5 → execsql2-2.15.7}/LICENSE.txt +0 -0
  41. {execsql2-2.15.5 → execsql2-2.15.7}/NOTICE +0 -0
  42. {execsql2-2.15.5 → execsql2-2.15.7}/README.md +0 -0
  43. {execsql2-2.15.5 → execsql2-2.15.7}/SECURITY.md +0 -0
  44. {execsql2-2.15.5 → execsql2-2.15.7}/docs/about/contributors.md +0 -0
  45. {execsql2-2.15.5 → execsql2-2.15.7}/docs/about/copyright.md +0 -0
  46. {execsql2-2.15.5 → execsql2-2.15.7}/docs/api/cli.md +0 -0
  47. {execsql2-2.15.5 → execsql2-2.15.7}/docs/api/db.md +0 -0
  48. {execsql2-2.15.5 → execsql2-2.15.7}/docs/api/exporters.md +0 -0
  49. {execsql2-2.15.5 → execsql2-2.15.7}/docs/api/importers.md +0 -0
  50. {execsql2-2.15.5 → execsql2-2.15.7}/docs/api/index.md +0 -0
  51. {execsql2-2.15.5 → execsql2-2.15.7}/docs/api/metacommands.md +0 -0
  52. {execsql2-2.15.5 → execsql2-2.15.7}/docs/dev/adding_db_adapters.md +0 -0
  53. {execsql2-2.15.5 → execsql2-2.15.7}/docs/dev/adding_exporters.md +0 -0
  54. {execsql2-2.15.5 → execsql2-2.15.7}/docs/dev/adding_importers.md +0 -0
  55. {execsql2-2.15.5 → execsql2-2.15.7}/docs/dev/adding_metacommands.md +0 -0
  56. {execsql2-2.15.5 → execsql2-2.15.7}/docs/dev/architecture.md +0 -0
  57. {execsql2-2.15.5 → execsql2-2.15.7}/docs/getting-started/installation.md +0 -0
  58. {execsql2-2.15.5 → execsql2-2.15.7}/docs/getting-started/requirements.md +0 -0
  59. {execsql2-2.15.5 → execsql2-2.15.7}/docs/getting-started/syntax.md +0 -0
  60. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/debugging.md +0 -0
  61. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/documentation.md +0 -0
  62. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/encoding.md +0 -0
  63. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/examples.md +0 -0
  64. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/formatter.md +0 -0
  65. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/logging.md +0 -0
  66. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/sql_syntax.md +0 -0
  67. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/usage.md +0 -0
  68. {execsql2-2.15.5 → execsql2-2.15.7}/docs/guides/using_scripts.md +0 -0
  69. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/Compare_planets.png +0 -0
  70. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/actions.png +0 -0
  71. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/actions2.png +0 -0
  72. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/checkboxes.png +0 -0
  73. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/connect.b64 +0 -0
  74. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/connect.png +0 -0
  75. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/create_conf.png +0 -0
  76. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/data_error1_screenshot.jpg +0 -0
  77. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/entry_form.png +0 -0
  78. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/execsql_console.png +0 -0
  79. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/execsql_logo_01.png +0 -0
  80. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/fatals.png +0 -0
  81. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/logo_small.png +0 -0
  82. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/pause_terminal.png +0 -0
  83. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/pause_terminal_sm.b64 +0 -0
  84. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/pause_terminal_sm.png +0 -0
  85. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/prompt_compare.png +0 -0
  86. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/set_build_commands.jpg +0 -0
  87. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/unit_conversions.b64 +0 -0
  88. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/unit_conversions_029.png +0 -0
  89. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/unmatched.png +0 -0
  90. {execsql2-2.15.5 → execsql2-2.15.7}/docs/images/vim_execsql_highlight.png +0 -0
  91. {execsql2-2.15.5 → execsql2-2.15.7}/docs/index.md +0 -0
  92. {execsql2-2.15.5 → execsql2-2.15.7}/docs/reference/configuration.md +0 -0
  93. {execsql2-2.15.5 → execsql2-2.15.7}/docs/reference/metacommands.md +0 -0
  94. {execsql2-2.15.5 → execsql2-2.15.7}/docs/reference/security.md +0 -0
  95. {execsql2-2.15.5 → execsql2-2.15.7}/docs/reference/substitution_vars.md +0 -0
  96. {execsql2-2.15.5 → execsql2-2.15.7}/extras/vscode-execsql/README.md +0 -0
  97. {execsql2-2.15.5 → execsql2-2.15.7}/extras/vscode-execsql/package.json +0 -0
  98. {execsql2-2.15.5 → execsql2-2.15.7}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  99. {execsql2-2.15.5 → execsql2-2.15.7}/justfile +0 -0
  100. {execsql2-2.15.5 → execsql2-2.15.7}/scripts/generate_vscode_grammar.py +0 -0
  101. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/__init__.py +0 -0
  102. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/__main__.py +0 -0
  103. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/cli/__init__.py +0 -0
  104. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/cli/dsn.py +0 -0
  105. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/cli/help.py +0 -0
  106. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/cli/lint.py +0 -0
  107. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/cli/run.py +0 -0
  108. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/__init__.py +0 -0
  109. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/db/factory.py +0 -0
  110. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/debug/__init__.py +0 -0
  111. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/debug/repl.py +0 -0
  112. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/__init__.py +0 -0
  113. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/base.py +0 -0
  114. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/delimited.py +0 -0
  115. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/duckdb.py +0 -0
  116. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/feather.py +0 -0
  117. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/latex.py +0 -0
  118. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/markdown.py +0 -0
  119. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/ods.py +0 -0
  120. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/parquet.py +0 -0
  121. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/pretty.py +0 -0
  122. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/protocol.py +0 -0
  123. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/raw.py +0 -0
  124. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/sqlite.py +0 -0
  125. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/templates.py +0 -0
  126. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/values.py +0 -0
  127. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/xls.py +0 -0
  128. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/xlsx.py +0 -0
  129. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/xml.py +0 -0
  130. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/yaml.py +0 -0
  131. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/exporters/zip.py +0 -0
  132. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/format.py +0 -0
  133. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/gui/__init__.py +0 -0
  134. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/gui/base.py +0 -0
  135. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/gui/console.py +0 -0
  136. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/gui/desktop.py +0 -0
  137. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/gui/tui.py +0 -0
  138. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/importers/__init__.py +0 -0
  139. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/importers/base.py +0 -0
  140. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/importers/feather.py +0 -0
  141. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/importers/json.py +0 -0
  142. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/importers/ods.py +0 -0
  143. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/importers/xls.py +0 -0
  144. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/__init__.py +0 -0
  145. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/conditions.py +0 -0
  146. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/connect.py +0 -0
  147. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/control.py +0 -0
  148. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/data.py +0 -0
  149. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/debug.py +0 -0
  150. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/dispatch.py +0 -0
  151. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/io.py +0 -0
  152. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/io_export.py +0 -0
  153. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/io_fileops.py +0 -0
  154. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/io_import.py +0 -0
  155. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/io_write.py +0 -0
  156. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/prompt.py +0 -0
  157. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/script_ext.py +0 -0
  158. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/metacommands/upsert.py +0 -0
  159. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/parser.py +0 -0
  160. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/py.typed +0 -0
  161. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/script/__init__.py +0 -0
  162. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/script/control.py +0 -0
  163. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/types.py +0 -0
  164. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/__init__.py +0 -0
  165. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/auth.py +0 -0
  166. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/crypto.py +0 -0
  167. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/datetime.py +0 -0
  168. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/errors.py +0 -0
  169. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/gui.py +0 -0
  170. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/mail.py +0 -0
  171. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/numeric.py +0 -0
  172. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/regex.py +0 -0
  173. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/strings.py +0 -0
  174. {execsql2-2.15.5 → execsql2-2.15.7}/src/execsql/utils/timer.py +0 -0
  175. {execsql2-2.15.5 → execsql2-2.15.7}/templates/README.md +0 -0
  176. {execsql2-2.15.5 → execsql2-2.15.7}/templates/config_settings.sqlite +0 -0
  177. {execsql2-2.15.5 → execsql2-2.15.7}/templates/example_config_prompt.sql +0 -0
  178. {execsql2-2.15.5 → execsql2-2.15.7}/templates/execsql.conf +0 -0
  179. {execsql2-2.15.5 → execsql2-2.15.7}/templates/make_config_db.sql +0 -0
  180. {execsql2-2.15.5 → execsql2-2.15.7}/templates/md_compare.sql +0 -0
  181. {execsql2-2.15.5 → execsql2-2.15.7}/templates/md_glossary.sql +0 -0
  182. {execsql2-2.15.5 → execsql2-2.15.7}/templates/md_upsert.sql +0 -0
  183. {execsql2-2.15.5 → execsql2-2.15.7}/templates/pg_compare.sql +0 -0
  184. {execsql2-2.15.5 → execsql2-2.15.7}/templates/pg_glossary.sql +0 -0
  185. {execsql2-2.15.5 → execsql2-2.15.7}/templates/pg_upsert.sql +0 -0
  186. {execsql2-2.15.5 → execsql2-2.15.7}/templates/script_template.sql +0 -0
  187. {execsql2-2.15.5 → execsql2-2.15.7}/templates/ss_compare.sql +0 -0
  188. {execsql2-2.15.5 → execsql2-2.15.7}/templates/ss_glossary.sql +0 -0
  189. {execsql2-2.15.5 → execsql2-2.15.7}/templates/ss_upsert.sql +0 -0
  190. {execsql2-2.15.5 → execsql2-2.15.7}/tests/__init__.py +0 -0
  191. {execsql2-2.15.5 → execsql2-2.15.7}/tests/cli/__init__.py +0 -0
  192. {execsql2-2.15.5 → execsql2-2.15.7}/tests/cli/test_cli.py +0 -0
  193. {execsql2-2.15.5 → execsql2-2.15.7}/tests/cli/test_cli_e2e.py +0 -0
  194. {execsql2-2.15.5 → execsql2-2.15.7}/tests/cli/test_cli_run.py +0 -0
  195. {execsql2-2.15.5 → execsql2-2.15.7}/tests/cli/test_lint.py +0 -0
  196. {execsql2-2.15.5 → execsql2-2.15.7}/tests/cli/test_ping.py +0 -0
  197. {execsql2-2.15.5 → execsql2-2.15.7}/tests/cli/test_profile.py +0 -0
  198. {execsql2-2.15.5 → execsql2-2.15.7}/tests/conftest.py +0 -0
  199. {execsql2-2.15.5 → execsql2-2.15.7}/tests/db/__init__.py +0 -0
  200. {execsql2-2.15.5 → execsql2-2.15.7}/tests/db/test_base.py +0 -0
  201. {execsql2-2.15.5 → execsql2-2.15.7}/tests/db/test_factory.py +0 -0
  202. {execsql2-2.15.5 → execsql2-2.15.7}/tests/db/test_postgres.py +0 -0
  203. {execsql2-2.15.5 → execsql2-2.15.7}/tests/db/test_sqlite.py +0 -0
  204. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/__init__.py +0 -0
  205. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_base.py +0 -0
  206. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_db.py +0 -0
  207. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_delimited.py +0 -0
  208. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_duckdb_exporter.py +0 -0
  209. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_exporters.py +0 -0
  210. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_feather.py +0 -0
  211. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_html_extended.py +0 -0
  212. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_html_latex.py +0 -0
  213. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_json.py +0 -0
  214. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_json_extended.py +0 -0
  215. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_latex_extended.py +0 -0
  216. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_markdown.py +0 -0
  217. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_ods.py +0 -0
  218. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_parquet.py +0 -0
  219. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_pretty_extended.py +0 -0
  220. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_raw_extended.py +0 -0
  221. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_sqlite_exporter.py +0 -0
  222. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_templates.py +0 -0
  223. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_templates_extended.py +0 -0
  224. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_values_extended.py +0 -0
  225. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_xls_xlsx.py +0 -0
  226. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_xlsx.py +0 -0
  227. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_xml.py +0 -0
  228. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_yaml.py +0 -0
  229. {execsql2-2.15.5 → execsql2-2.15.7}/tests/exporters/test_zip.py +0 -0
  230. {execsql2-2.15.5 → execsql2-2.15.7}/tests/gui/__init__.py +0 -0
  231. {execsql2-2.15.5 → execsql2-2.15.7}/tests/gui/test_backends.py +0 -0
  232. {execsql2-2.15.5 → execsql2-2.15.7}/tests/gui/test_compare_stats.py +0 -0
  233. {execsql2-2.15.5 → execsql2-2.15.7}/tests/gui/test_compute_row_diffs.py +0 -0
  234. {execsql2-2.15.5 → execsql2-2.15.7}/tests/importers/__init__.py +0 -0
  235. {execsql2-2.15.5 → execsql2-2.15.7}/tests/importers/test_base_extended.py +0 -0
  236. {execsql2-2.15.5 → execsql2-2.15.7}/tests/importers/test_csv_edge_cases.py +0 -0
  237. {execsql2-2.15.5 → execsql2-2.15.7}/tests/importers/test_csv_importer.py +0 -0
  238. {execsql2-2.15.5 → execsql2-2.15.7}/tests/importers/test_feather_importer.py +0 -0
  239. {execsql2-2.15.5 → execsql2-2.15.7}/tests/importers/test_json_importer.py +0 -0
  240. {execsql2-2.15.5 → execsql2-2.15.7}/tests/importers/test_ods_importer.py +0 -0
  241. {execsql2-2.15.5 → execsql2-2.15.7}/tests/importers/test_xls_importer.py +0 -0
  242. {execsql2-2.15.5 → execsql2-2.15.7}/tests/integration/__init__.py +0 -0
  243. {execsql2-2.15.5 → execsql2-2.15.7}/tests/integration/conftest.py +0 -0
  244. {execsql2-2.15.5 → execsql2-2.15.7}/tests/integration/test_dsn.py +0 -0
  245. {execsql2-2.15.5 → execsql2-2.15.7}/tests/integration/test_duckdb.py +0 -0
  246. {execsql2-2.15.5 → execsql2-2.15.7}/tests/integration/test_mysql.py +0 -0
  247. {execsql2-2.15.5 → execsql2-2.15.7}/tests/integration/test_postgres.py +0 -0
  248. {execsql2-2.15.5 → execsql2-2.15.7}/tests/integration/test_sqlite.py +0 -0
  249. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/__init__.py +0 -0
  250. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_assert.py +0 -0
  251. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_breakpoint.py +0 -0
  252. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_connect.py +0 -0
  253. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_io_export.py +0 -0
  254. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_io_import.py +0 -0
  255. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands.py +0 -0
  256. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_connect.py +0 -0
  257. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_data.py +0 -0
  258. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_extended.py +0 -0
  259. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  260. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_io.py +0 -0
  261. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  262. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  263. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_system.py +0 -0
  264. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  265. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_pg_upsert.py +0 -0
  266. {execsql2-2.15.5 → execsql2-2.15.7}/tests/metacommands/test_row_count.py +0 -0
  267. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_config.py +0 -0
  268. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_config_data.py +0 -0
  269. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_config_extended.py +0 -0
  270. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_debug_repl.py +0 -0
  271. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_engine.py +0 -0
  272. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_error_messages.py +0 -0
  273. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_exceptions.py +0 -0
  274. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_format.py +0 -0
  275. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_mail.py +0 -0
  276. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_models.py +0 -0
  277. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_package.py +0 -0
  278. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_parser.py +0 -0
  279. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_registry.py +0 -0
  280. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_state.py +0 -0
  281. {execsql2-2.15.5 → execsql2-2.15.7}/tests/test_types.py +0 -0
  282. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/__init__.py +0 -0
  283. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_auth.py +0 -0
  284. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_auth_extra.py +0 -0
  285. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_crypto.py +0 -0
  286. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_datetime.py +0 -0
  287. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_errors.py +0 -0
  288. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_errors_extra.py +0 -0
  289. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_fileio.py +0 -0
  290. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_fileio_extra.py +0 -0
  291. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_numeric.py +0 -0
  292. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_regex.py +0 -0
  293. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_strings.py +0 -0
  294. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_timer.py +0 -0
  295. {execsql2-2.15.5 → execsql2-2.15.7}/tests/utils/test_timer_extra.py +0 -0
  296. {execsql2-2.15.5 → execsql2-2.15.7}/zensical.toml +0 -0
@@ -13,6 +13,38 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.15.7] - 2026-04-20
17
+
18
+ ### Fixed
19
+
20
+ - `CounterVars.substitute` now correctly searches the full string. `re.I` was mistakenly passed as the `pos` argument to `re.search`, causing the first two characters of every string to be skipped when looking for counter variable references.
21
+ - `DataTypeError`, `DbTypeError`, and `DatabaseNotImplementedError` now call `super().__init__()` instead of bypassing the MRO with `Exception.__init__(self, ...)`, fixing `repr()` crashes for these exception types.
22
+ - SQL injection in MySQL `LOAD DATA INFILE`: file path, field delimiter, and quote character are now escaped before being interpolated into the import SQL statement.
23
+ - SQLite and DuckDB `exec_cmd` no longer encodes the SQL string to bytes before passing it to `execute()`, which always raised `TypeError` in Python 3.
24
+ - Substitution variable token matching in `_substitute_nested` now uses `str.find()` on a lower-cased copy of the string instead of compiling a new regex per variable per call, eliminating unnecessary regex compilation on every substitution.
25
+ - Cursor leaks across all database adapters (PostgreSQL, MySQL, SQLite, DuckDB, Firebird, Access, SQL Server, Oracle) — call sites that manually opened cursors now use the `with self._cursor()` context manager so cursors are always closed, including on exceptions.
26
+ - `__delattr__` in `state.py` now instantiates a fresh `RuntimeContext()` when resetting an attribute to its default, rather than reading from a cached `_DEFAULT_CTX` instance. This prevents mutable defaults (lists, dicts) from being shared across resets.
27
+ - `cmds_run` counter no longer overcounts: the `StopIteration` branch in `runscripts()` now calls `continue` after popping the command list stack, preventing the increment that follows from executing.
28
+ - Config file chaining is now capped at 20 files to prevent an infinite loop when `config_file` entries form a cycle.
29
+ - Temp file creation in `TempFileMgr` now uses `tempfile.mkstemp()` instead of `tempfile.NamedTemporaryFile().name`, eliminating the TOCTOU race where another process could claim the name between creation and use.
30
+ - JSON export now serializes column names with `json.dumps()` instead of bare f-string interpolation, preventing malformed JSON when column names contain quotes, backslashes, or other special characters.
31
+ - PostgreSQL `VACUUM` autocommit session state is now restored in a `finally` block, ensuring the connection returns to non-autocommit mode even if the vacuum statement raises an exception.
32
+ - HTML export now HTML-escapes the description, author, and CSS href meta tag values, preventing malformed HTML when these values contain `<`, `>`, `"`, or `&` characters.
33
+ - `shlex.split` on Windows is now called with `posix=False` instead of pre-escaping backslashes, which produced incorrect splits for paths with consecutive backslashes.
34
+ - Duplicate `JsonDatatype.integer = "integer"` assignment in `models.py` removed; `JsonDatatype.number` is the correct attribute and was already present.
35
+ - MySQL adapter constructor no longer coerces `None` arguments to the string `"None"` for `server_name`, `db_name`, and `user_name`; `None` values are now preserved as `None`.
36
+ - `importfile()` parameter renamed from `columname` to `column_name`, matching the internal variable name used throughout the function body.
37
+
38
+ ______________________________________________________________________
39
+
40
+ ## [2.15.6] - 2026-04-16
41
+
42
+ ### Fixed
43
+
44
+ - Nested substitution variable names (e.g., `!!N_!!CHECK_GROUP!!_CHECKS!!`) now resolve correctly, matching original execsql behavior. The single-pass token regex introduced in 2.15.0 could not find inner `!!var!!` tokens embedded within an outer variable name; a per-variable substring fallback now handles this edge case.
45
+
46
+ ______________________________________________________________________
47
+
16
48
  ## [2.15.5] - 2026-04-15
17
49
 
18
50
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.15.5
3
+ Version: 2.15.7
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
@@ -210,6 +210,7 @@ These are behavioral changes driven by security or correctness issues in the ups
210
210
  | Database metadata queries | `schema_exists()`, `table_exists()`, `column_exists()`, `table_columns()`, `view_exists()`, `role_exists()` across all 9 adapters now use parameterized queries. Upstream used string interpolation. |
211
211
  | `import_entire_file()` | Column names are quoted with `quote_identifier()` instead of interpolated into INSERT statements. |
212
212
  | PostgreSQL `CREATE DATABASE` | Database name and encoding are quoted. COPY delimiter and quote character are validated. |
213
+ | MySQL `LOAD DATA INFILE` | File path, delimiter, and quotechar are now escaped with `replace("'", "''")` before interpolation into the SQL statement. |
213
214
  | `$SHEETS_TABLES_VALUES` | Sheet names from ODS/XLS imports are escaped before embedding in SQL. |
214
215
  | HTTP `Content-Disposition` | Filename is sanitized to prevent HTTP response splitting in SERVE. |
215
216
 
@@ -220,7 +221,7 @@ These are behavioral changes driven by security or correctness issues in the ups
220
221
  | Jinja2 sandboxing | Templates run in `SandboxedEnvironment` instead of the default `jinja2.Template`. |
221
222
  | HTML export | Column headers and cell values are escaped with `html.escape()` to prevent XSS. |
222
223
  | XML export | Values are escaped with `xml.sax.saxutils.escape()`. Invalid XML element name characters are replaced. |
223
- | JSON export | The `description` field uses `json.dumps()` instead of string interpolation. |
224
+ | JSON export | The `description` field and all column names use `json.dumps()` instead of string interpolation. |
224
225
 
225
226
  ### Credential and Logging Safety
226
227
 
@@ -254,6 +255,14 @@ These are behavioral changes driven by security or correctness issues in the ups
254
255
  | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. Inherited from upstream. |
255
256
  | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. Inherited from upstream. |
256
257
  | `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. Inherited from upstream. |
258
+ | `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. Inherited from upstream. |
259
+ | `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. Inherited from upstream. |
260
+ | 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. Inherited from upstream. |
261
+ | 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. Inherited from upstream. |
262
+ | Cursor leaks in database adapters | ~15 methods across all adapters used `curs = self.cursor()` / `curs.close()` without `try/finally`. If the query raised, the cursor leaked. Converted to `with self._cursor() as curs:`. Inherited from upstream. |
263
+ | JSON export malformed on special column names | Column names containing `"` or `\` produced invalid JSON. Now uses `json.dumps()` for all field names. Inherited from upstream. |
264
+ | Temp file creation TOCTOU race | `TempFileMgr.new_temp_fn()` discarded the `NamedTemporaryFile` handle, creating a race window. Now uses `tempfile.mkstemp()` for secure creation. Inherited from upstream. |
265
+ | `shlex.split` on Windows incorrect mode | Called without `posix=False` on Windows, mishandling backslash-heavy paths in SHELL commands. Inherited from upstream. |
257
266
 
258
267
  ______________________________________________________________________
259
268
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.15.5"
7
+ version = "2.15.7"
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" }
@@ -164,7 +164,7 @@ skip-magic-trailing-comma = false
164
164
  line-ending = "auto"
165
165
 
166
166
  [tool.bumpversion]
167
- current_version = "2.15.5"
167
+ current_version = "2.15.7"
168
168
  commit = true
169
169
  commit_args = "--no-verify"
170
170
  tag = true
@@ -290,8 +290,11 @@ class ConfigData:
290
290
  config_files = [sys_config_file, user_config_file, script_config_file, startdir_config_file]
291
291
  else:
292
292
  config_files = [sys_config_file, user_config_file, startdir_config_file]
293
+ _MAX_CONFIG_CHAIN = 20 # Guard against circular config_file references.
293
294
  self.files_read: list = []
294
295
  for ix, configfile in enumerate(config_files):
296
+ if len(self.files_read) >= _MAX_CONFIG_CHAIN:
297
+ break
295
298
  if configfile not in self.files_read and Path(configfile).is_file():
296
299
  self.files_read.append(configfile)
297
300
  cp = ConfigParser()
@@ -243,16 +243,16 @@ class AccessDatabase(Database):
243
243
  self.temp_query_names.append(qn)
244
244
  else:
245
245
  self.dao_flush_check()
246
- curs = self.cursor()
247
- if self.jet4:
248
- encoded_sql = str(sql)
249
- else:
250
- encoded_sql = str(sql).encode(self.encoding)
251
- if paramlist is None:
252
- curs.execute(encoded_sql)
253
- else:
254
- curs.execute(encoded_sql, paramlist)
255
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
246
+ with self._cursor() as curs:
247
+ if self.jet4:
248
+ encoded_sql = str(sql)
249
+ else:
250
+ encoded_sql = str(sql).encode(self.encoding)
251
+ if paramlist is None:
252
+ curs.execute(encoded_sql)
253
+ else:
254
+ curs.execute(encoded_sql, paramlist)
255
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
256
256
 
257
257
  if type(sqlcmd) in (list, tuple):
258
258
  for sql in sqlcmd:
@@ -269,10 +269,10 @@ class AccessDatabase(Database):
269
269
  # Returns the results of the sql select statement.
270
270
  # The Access driver returns data as unicode, so no decoding is necessary.
271
271
  self.dao_flush_check()
272
- curs = self.cursor()
273
- curs.execute(sql)
274
- rows = curs.fetchall()
275
- return [d[0] for d in curs.description], rows
272
+ with self._cursor() as curs:
273
+ curs.execute(sql)
274
+ rows = curs.fetchall()
275
+ return [d[0] for d in curs.description], rows
276
276
 
277
277
  def select_rowsource(self, sql: str) -> tuple[list[str], Any]:
278
278
  """Return column names and an iterable that yields rows one at a time."""
@@ -308,20 +308,20 @@ class AccessDatabase(Database):
308
308
  def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
309
309
  """Return True if the named table exists in the Access database."""
310
310
  self.dao_flush_check()
311
- curs = self.cursor()
312
- try:
313
- sql = "select Name from MSysObjects where Name=? And Type In (1,4,6);"
314
- curs.execute(sql, (table_name,))
315
- except ErrInfo:
316
- raise
317
- except Exception as e:
318
- raise ErrInfo(
319
- type="db",
320
- command_text=sql,
321
- exception_msg=exception_desc(),
322
- other_msg=f"Failure on test for existence of Access table {table_name}",
323
- ) from e
324
- rows = curs.fetchall()
311
+ sql = "select Name from MSysObjects where Name=? And Type In (1,4,6);"
312
+ with self._cursor() as curs:
313
+ try:
314
+ curs.execute(sql, (table_name,))
315
+ except ErrInfo:
316
+ raise
317
+ except Exception as e:
318
+ raise ErrInfo(
319
+ type="db",
320
+ command_text=sql,
321
+ exception_msg=exception_desc(),
322
+ other_msg=f"Failure on test for existence of Access table {table_name}",
323
+ ) from e
324
+ rows = curs.fetchall()
325
325
  return len(rows) > 0
326
326
 
327
327
  def column_exists(
@@ -332,41 +332,41 @@ class AccessDatabase(Database):
332
332
  ) -> bool:
333
333
  """Return True if the named column exists in the given Access table."""
334
334
  self.dao_flush_check()
335
- curs = self.cursor()
336
335
  quoted_col = self.quote_identifier(column_name)
337
336
  quoted_tbl = self.quote_identifier(table_name)
338
337
  sql = f"select top 1 {quoted_col} from {quoted_tbl};"
339
- try:
340
- curs.execute(sql)
341
- except Exception:
342
- return False
338
+ with self._cursor() as curs:
339
+ try:
340
+ curs.execute(sql)
341
+ except Exception:
342
+ return False
343
343
  return True
344
344
 
345
345
  def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
346
346
  """Return a list of column names for the given Access table."""
347
347
  self.dao_flush_check()
348
- curs = self.cursor()
349
348
  quoted_tbl = self.quote_identifier(table_name)
350
- curs.execute(f"select top 1 * from {quoted_tbl};")
351
- return [d[0] for d in curs.description]
349
+ with self._cursor() as curs:
350
+ curs.execute(f"select top 1 * from {quoted_tbl};")
351
+ return [d[0] for d in curs.description]
352
352
 
353
353
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
354
354
  """Return True if the named view or query exists in the Access database."""
355
355
  self.dao_flush_check()
356
- curs = self.cursor()
357
- try:
358
- sql = "select Name from MSysObjects where Name=? And Type = 5;"
359
- curs.execute(sql, (view_name,))
360
- except ErrInfo:
361
- raise
362
- except Exception as e:
363
- raise ErrInfo(
364
- type="db",
365
- command_text=sql,
366
- exception_msg=exception_desc(),
367
- other_msg=f"Test for existence of Access view/query {view_name}",
368
- ) from e
369
- rows = curs.fetchall()
356
+ sql = "select Name from MSysObjects where Name=? And Type = 5;"
357
+ with self._cursor() as curs:
358
+ try:
359
+ curs.execute(sql, (view_name,))
360
+ except ErrInfo:
361
+ raise
362
+ except Exception as e:
363
+ raise ErrInfo(
364
+ type="db",
365
+ command_text=sql,
366
+ exception_msg=exception_desc(),
367
+ other_msg=f"Test for existence of Access view/query {view_name}",
368
+ ) from e
369
+ rows = curs.fetchall()
370
370
  return len(rows) > 0
371
371
 
372
372
  def schema_exists(self, schema_name: str) -> bool:
@@ -447,4 +447,5 @@ class AccessDatabase(Database):
447
447
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
448
448
  quoted_col = self.quote_identifier(column_name)
449
449
  sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
450
- self.cursor().execute(sql, (pyodbc.Binary(filedata),))
450
+ with self._cursor() as curs:
451
+ curs.execute(sql, (pyodbc.Binary(filedata),))
@@ -460,7 +460,6 @@ class Database(ABC):
460
460
  paramspec = self.paramsubs(len(columns))
461
461
  sql = f"insert into {sq_name} ({colspec}) values ({paramspec});"
462
462
  rows = iter(rowsource)
463
- curs = self.cursor()
464
463
  eof = False
465
464
  total_rows = 0
466
465
 
@@ -483,7 +482,7 @@ class Database(ABC):
483
482
  except ImportError:
484
483
  use_progress = False
485
484
 
486
- def _import_loop() -> int:
485
+ def _import_loop(curs) -> int:
487
486
  nonlocal eof, total_rows, task_id
488
487
  while True:
489
488
  b = []
@@ -575,12 +574,13 @@ class Database(ABC):
575
574
  break
576
575
  return total_rows
577
576
 
578
- if use_progress and progress_ctx is not None:
579
- with progress_ctx:
580
- task_id = progress_ctx.add_task(sq_name, total=None)
581
- _import_loop()
582
- else:
583
- _import_loop()
577
+ with self._cursor() as curs:
578
+ if use_progress and progress_ctx is not None:
579
+ with progress_ctx:
580
+ task_id = progress_ctx.add_task(sq_name, total=None)
581
+ _import_loop(curs)
582
+ else:
583
+ _import_loop(curs)
584
584
 
585
585
  if _state.exec_log:
586
586
  _state.exec_log.log_status_info(
@@ -145,4 +145,5 @@ class DsnDatabase(Database):
145
145
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
146
146
  quoted_col = self.quote_identifier(column_name)
147
147
  sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
148
- self.cursor().execute(sql, (pyodbc.Binary(filedata),))
148
+ with self._cursor() as curs:
149
+ curs.execute(sql, (pyodbc.Binary(filedata),))
@@ -67,7 +67,7 @@ class DuckDBDatabase(Database):
67
67
  with self._cursor() as curs:
68
68
  cmd = f"select * from {querycommand};"
69
69
  try:
70
- curs.execute(cmd.encode(self.encoding))
70
+ curs.execute(cmd)
71
71
  _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
72
72
  except Exception:
73
73
  self.rollback()
@@ -127,30 +127,29 @@ class FirebirdDatabase(Database):
127
127
 
128
128
  def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
129
129
  """Return True if the named table exists in the Firebird database."""
130
- curs = self.cursor()
131
130
  sql = (
132
131
  "SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
133
132
  "WHERE RDB$SYSTEM_FLAG=0 AND RDB$VIEW_BLR IS NULL "
134
133
  "AND RDB$RELATION_NAME=?;"
135
134
  )
136
- try:
137
- curs.execute(sql, (table_name.upper(),))
138
- except ErrInfo:
139
- raise
140
- except Exception as e:
135
+ with self._cursor() as curs:
141
136
  try:
142
- self.rollback()
143
- except Exception:
144
- pass # Rollback is best-effort after a failed query.
145
- raise ErrInfo(
146
- type="db",
147
- command_text=sql,
148
- exception_msg=exception_desc(),
149
- other_msg=f"Failed test for existence of Firebird table {table_name}",
150
- ) from e
151
- rows = curs.fetchall()
137
+ curs.execute(sql, (table_name.upper(),))
138
+ except ErrInfo:
139
+ raise
140
+ except Exception as e:
141
+ try:
142
+ self.rollback()
143
+ except Exception:
144
+ pass # Rollback is best-effort after a failed query.
145
+ raise ErrInfo(
146
+ type="db",
147
+ command_text=sql,
148
+ exception_msg=exception_desc(),
149
+ other_msg=f"Failed test for existence of Firebird table {table_name}",
150
+ ) from e
151
+ rows = curs.fetchall()
152
152
  self.conn.commit()
153
- curs.close()
154
153
  return len(rows) > 0
155
154
 
156
155
  def column_exists(
@@ -160,53 +159,52 @@ class FirebirdDatabase(Database):
160
159
  schema_name: str | None = None,
161
160
  ) -> bool:
162
161
  """Return True if the named column exists in the given Firebird table."""
163
- curs = self.cursor()
164
162
  quoted_col = self.quote_identifier(column_name)
165
163
  quoted_tbl = self.quote_identifier(table_name)
166
164
  sql = f"select first 1 {quoted_col} from {quoted_tbl};"
167
- try:
168
- curs.execute(sql)
169
- except Exception:
170
- return False
165
+ with self._cursor() as curs:
166
+ try:
167
+ curs.execute(sql)
168
+ except Exception:
169
+ return False
171
170
  return True
172
171
 
173
172
  def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
174
173
  """Return a list of column names for the given Firebird table."""
175
- curs = self.cursor()
176
174
  quoted_tbl = self.quote_identifier(table_name)
177
175
  sql = f"select first 1 * from {quoted_tbl};"
178
- try:
179
- curs.execute(sql)
180
- except ErrInfo:
181
- raise
182
- except Exception as e:
183
- self.rollback()
184
- raise ErrInfo(
185
- type="db",
186
- command_text=sql,
187
- exception_msg=exception_desc(),
188
- other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
189
- ) from e
190
- return [d[0] for d in curs.description]
176
+ with self._cursor() as curs:
177
+ try:
178
+ curs.execute(sql)
179
+ except ErrInfo:
180
+ raise
181
+ except Exception as e:
182
+ self.rollback()
183
+ raise ErrInfo(
184
+ type="db",
185
+ command_text=sql,
186
+ exception_msg=exception_desc(),
187
+ other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
188
+ ) from e
189
+ return [d[0] for d in curs.description]
191
190
 
192
191
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
193
192
  """Return True if the named view exists in the Firebird database."""
194
- curs = self.cursor()
195
193
  sql = "select distinct rdb$view_name from rdb$view_relations where rdb$view_name = ?;"
196
- try:
197
- curs.execute(sql, (view_name,))
198
- except ErrInfo:
199
- raise
200
- except Exception as e:
201
- self.rollback()
202
- raise ErrInfo(
203
- type="db",
204
- command_text=sql,
205
- exception_msg=exception_desc(),
206
- other_msg=f"Failed test for existence of Firebird view {view_name}",
207
- ) from e
208
- rows = curs.fetchall()
209
- curs.close()
194
+ with self._cursor() as curs:
195
+ try:
196
+ curs.execute(sql, (view_name,))
197
+ except ErrInfo:
198
+ raise
199
+ except Exception as e:
200
+ self.rollback()
201
+ raise ErrInfo(
202
+ type="db",
203
+ command_text=sql,
204
+ exception_msg=exception_desc(),
205
+ other_msg=f"Failed test for existence of Firebird view {view_name}",
206
+ ) from e
207
+ rows = curs.fetchall()
210
208
  return len(rows) > 0
211
209
 
212
210
  def schema_exists(self, schema_name: str) -> bool:
@@ -215,14 +213,13 @@ class FirebirdDatabase(Database):
215
213
 
216
214
  def role_exists(self, rolename: str) -> bool:
217
215
  """Return True if the named role or user exists in the Firebird database."""
218
- curs = self.cursor()
219
- curs.execute(
220
- "SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = ? union "
221
- " SELECT DISTINCT RDB$ROLE_NAME FROM RDB$ROLES WHERE RDB$ROLE_NAME = ?;",
222
- (rolename, rolename),
223
- )
224
- rows = curs.fetchall()
225
- curs.close()
216
+ with self._cursor() as curs:
217
+ curs.execute(
218
+ "SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = ? union "
219
+ " SELECT DISTINCT RDB$ROLE_NAME FROM RDB$ROLES WHERE RDB$ROLE_NAME = ?;",
220
+ (rolename, rolename),
221
+ )
222
+ rows = curs.fetchall()
226
223
  return len(rows) > 0
227
224
 
228
225
  def drop_table(self, tablename: str) -> None: