execsql2 2.21.2__tar.gz → 2.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. {execsql2-2.21.2 → execsql2-2.22.0}/.github/workflows/ci-cd.yml +50 -1
  2. {execsql2-2.21.2 → execsql2-2.22.0}/.pre-commit-config.yaml +12 -0
  3. {execsql2-2.21.2 → execsql2-2.22.0}/CHANGELOG.md +15 -0
  4. {execsql2-2.21.2 → execsql2-2.22.0}/CONTRIBUTING.md +20 -8
  5. {execsql2-2.21.2 → execsql2-2.22.0}/PKG-INFO +3 -3
  6. {execsql2-2.21.2 → execsql2-2.22.0}/README.md +2 -2
  7. {execsql2-2.21.2 → execsql2-2.22.0}/docs/about/divergence.md +44 -40
  8. {execsql2-2.21.2 → execsql2-2.22.0}/docs/dev/architecture.md +1 -1
  9. {execsql2-2.21.2 → execsql2-2.22.0}/docs/getting-started/installation.md +2 -1
  10. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/formatter.md +1 -1
  11. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/sql_syntax.md +1 -1
  12. {execsql2-2.21.2 → execsql2-2.22.0}/docs/reference/configuration.md +3 -0
  13. {execsql2-2.21.2 → execsql2-2.22.0}/docs/reference/metacommands.md +9 -5
  14. {execsql2-2.21.2 → execsql2-2.22.0}/docs/reference/security.md +14 -2
  15. {execsql2-2.21.2 → execsql2-2.22.0}/justfile +2 -2
  16. {execsql2-2.21.2 → execsql2-2.22.0}/pyproject.toml +11 -2
  17. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/api.py +21 -4
  18. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/cli/run.py +37 -17
  19. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/config.py +37 -22
  20. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/data/execsql.conf.template +6 -0
  21. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/access.py +6 -5
  22. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/base.py +23 -5
  23. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/dsn.py +2 -0
  24. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/duckdb.py +1 -1
  25. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/mysql.py +6 -8
  26. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/sqlite.py +1 -1
  27. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exceptions.py +14 -11
  28. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/base.py +1 -1
  29. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/delimited.py +22 -16
  30. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/duckdb.py +6 -1
  31. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/feather.py +1 -1
  32. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/json.py +5 -3
  33. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/latex.py +2 -2
  34. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/markdown.py +1 -0
  35. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/ods.py +7 -3
  36. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/raw.py +8 -8
  37. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/sqlite.py +11 -1
  38. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/templates.py +2 -1
  39. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/xls.py +10 -10
  40. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/xml.py +1 -0
  41. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/yaml.py +1 -0
  42. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/zip.py +9 -7
  43. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/format.py +6 -6
  44. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/gui/console.py +3 -3
  45. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/gui/desktop.py +28 -10
  46. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/gui/tui.py +8 -6
  47. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/importers/base.py +14 -8
  48. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/importers/csv.py +8 -3
  49. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/importers/xls.py +1 -0
  50. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/conditions.py +14 -14
  51. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/connect.py +2 -2
  52. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/control.py +1 -1
  53. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/data.py +2 -1
  54. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/io_export.py +4 -0
  55. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/io_fileops.py +85 -39
  56. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/io_import.py +1 -0
  57. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/io_write.py +1 -1
  58. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/prompt.py +24 -22
  59. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/system.py +14 -1
  60. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/models.py +56 -17
  61. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/parser.py +8 -8
  62. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/plugins.py +1 -1
  63. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/script/engine.py +4 -4
  64. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/script/executor.py +67 -28
  65. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/script/parser.py +13 -12
  66. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/script/variables.py +4 -2
  67. execsql2-2.22.0/src/execsql/state.pyi +122 -0
  68. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/types.py +31 -26
  69. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/auth.py +6 -2
  70. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/datetime.py +3 -3
  71. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/errors.py +7 -5
  72. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/fileio.py +77 -31
  73. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/gui.py +15 -11
  74. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/mail.py +9 -2
  75. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/numeric.py +1 -1
  76. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/regex.py +9 -9
  77. {execsql2-2.21.2 → execsql2-2.22.0}/tests/cli/test_cli_run.py +45 -1
  78. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_mysql_case_folding.py +18 -1
  79. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_duckdb_exporter.py +29 -0
  80. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_feather.py +1 -1
  81. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_latex_extended.py +10 -7
  82. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_sqlite_exporter.py +38 -0
  83. {execsql2-2.21.2 → execsql2-2.22.0}/tests/importers/test_base_extended.py +19 -0
  84. {execsql2-2.21.2 → execsql2-2.22.0}/tests/importers/test_csv_importer.py +8 -25
  85. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_fileops_extra.py +102 -0
  86. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_system.py +66 -0
  87. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/test_log_redaction.py +45 -0
  88. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_api.py +33 -0
  89. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_config_data.py +26 -0
  90. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_models.py +106 -4
  91. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_types.py +2 -3
  92. {execsql2-2.21.2 → execsql2-2.22.0}/uv.lock +1 -1
  93. {execsql2-2.21.2 → execsql2-2.22.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  94. {execsql2-2.21.2 → execsql2-2.22.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  95. {execsql2-2.21.2 → execsql2-2.22.0}/.github/dependabot.yml +0 -0
  96. {execsql2-2.21.2 → execsql2-2.22.0}/.gitignore +0 -0
  97. {execsql2-2.21.2 → execsql2-2.22.0}/.pre-commit-hooks.yaml +0 -0
  98. {execsql2-2.21.2 → execsql2-2.22.0}/.python-version +0 -0
  99. {execsql2-2.21.2 → execsql2-2.22.0}/.readthedocs.yaml +0 -0
  100. {execsql2-2.21.2 → execsql2-2.22.0}/LICENSE.txt +0 -0
  101. {execsql2-2.21.2 → execsql2-2.22.0}/NOTICE +0 -0
  102. {execsql2-2.21.2 → execsql2-2.22.0}/SECURITY.md +0 -0
  103. {execsql2-2.21.2 → execsql2-2.22.0}/docs/about/contributors.md +0 -0
  104. {execsql2-2.21.2 → execsql2-2.22.0}/docs/about/copyright.md +0 -0
  105. {execsql2-2.21.2 → execsql2-2.22.0}/docs/api/cli.md +0 -0
  106. {execsql2-2.21.2 → execsql2-2.22.0}/docs/api/db.md +0 -0
  107. {execsql2-2.21.2 → execsql2-2.22.0}/docs/api/exporters.md +0 -0
  108. {execsql2-2.21.2 → execsql2-2.22.0}/docs/api/importers.md +0 -0
  109. {execsql2-2.21.2 → execsql2-2.22.0}/docs/api/index.md +0 -0
  110. {execsql2-2.21.2 → execsql2-2.22.0}/docs/api/metacommands.md +0 -0
  111. {execsql2-2.21.2 → execsql2-2.22.0}/docs/dev/adding_db_adapters.md +0 -0
  112. {execsql2-2.21.2 → execsql2-2.22.0}/docs/dev/adding_exporters.md +0 -0
  113. {execsql2-2.21.2 → execsql2-2.22.0}/docs/dev/adding_importers.md +0 -0
  114. {execsql2-2.21.2 → execsql2-2.22.0}/docs/dev/adding_metacommands.md +0 -0
  115. {execsql2-2.21.2 → execsql2-2.22.0}/docs/dev/releasing.md +0 -0
  116. {execsql2-2.21.2 → execsql2-2.22.0}/docs/getting-started/requirements.md +0 -0
  117. {execsql2-2.21.2 → execsql2-2.22.0}/docs/getting-started/syntax.md +0 -0
  118. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/debugging.md +0 -0
  119. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/documentation.md +0 -0
  120. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/encoding.md +0 -0
  121. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/examples.md +0 -0
  122. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/logging.md +0 -0
  123. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/usage.md +0 -0
  124. {execsql2-2.21.2 → execsql2-2.22.0}/docs/guides/using_scripts.md +0 -0
  125. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/Compare_planets.png +0 -0
  126. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/actions.png +0 -0
  127. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/actions2.png +0 -0
  128. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/checkboxes.png +0 -0
  129. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/connect.b64 +0 -0
  130. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/connect.png +0 -0
  131. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/create_conf.png +0 -0
  132. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/data_error1_screenshot.jpg +0 -0
  133. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/entry_form.png +0 -0
  134. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/execsql_console.png +0 -0
  135. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/execsql_logo_01.png +0 -0
  136. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/fatals.png +0 -0
  137. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/logo_small.png +0 -0
  138. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/pause_terminal.png +0 -0
  139. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/pause_terminal_sm.b64 +0 -0
  140. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/pause_terminal_sm.png +0 -0
  141. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/prompt_compare.png +0 -0
  142. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/set_build_commands.jpg +0 -0
  143. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/unit_conversions.b64 +0 -0
  144. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/unit_conversions_029.png +0 -0
  145. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/unmatched.png +0 -0
  146. {execsql2-2.21.2 → execsql2-2.22.0}/docs/images/vim_execsql_highlight.png +0 -0
  147. {execsql2-2.21.2 → execsql2-2.22.0}/docs/index.md +0 -0
  148. {execsql2-2.21.2 → execsql2-2.22.0}/docs/reference/substitution_vars.md +0 -0
  149. {execsql2-2.21.2 → execsql2-2.22.0}/extras/plugin-template/README.md +0 -0
  150. {execsql2-2.21.2 → execsql2-2.22.0}/extras/plugin-template/pyproject.toml +0 -0
  151. {execsql2-2.21.2 → execsql2-2.22.0}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  152. {execsql2-2.21.2 → execsql2-2.22.0}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  153. {execsql2-2.21.2 → execsql2-2.22.0}/extras/vscode-execsql/README.md +0 -0
  154. {execsql2-2.21.2 → execsql2-2.22.0}/extras/vscode-execsql/package.json +0 -0
  155. {execsql2-2.21.2 → execsql2-2.22.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  156. {execsql2-2.21.2 → execsql2-2.22.0}/scripts/generate_vscode_grammar.py +0 -0
  157. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/__init__.py +0 -0
  158. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/__main__.py +0 -0
  159. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/cli/__init__.py +0 -0
  160. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/cli/dsn.py +0 -0
  161. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/cli/help.py +0 -0
  162. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/cli/lint.py +0 -0
  163. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/data/__init__.py +0 -0
  164. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/__init__.py +0 -0
  165. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/factory.py +0 -0
  166. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/firebird.py +0 -0
  167. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/oracle.py +0 -0
  168. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/postgres.py +0 -0
  169. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/db/sqlserver.py +0 -0
  170. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/debug/__init__.py +0 -0
  171. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/debug/repl.py +0 -0
  172. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/__init__.py +0 -0
  173. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/html.py +0 -0
  174. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/parquet.py +0 -0
  175. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/pretty.py +0 -0
  176. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/protocol.py +0 -0
  177. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/values.py +0 -0
  178. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/exporters/xlsx.py +0 -0
  179. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/gui/__init__.py +0 -0
  180. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/gui/base.py +0 -0
  181. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/importers/__init__.py +0 -0
  182. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/importers/feather.py +0 -0
  183. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/importers/json.py +0 -0
  184. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/importers/ods.py +0 -0
  185. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/__init__.py +0 -0
  186. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/debug.py +0 -0
  187. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/dispatch.py +0 -0
  188. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/io.py +0 -0
  189. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/script_ext.py +0 -0
  190. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/metacommands/upsert.py +0 -0
  191. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/py.typed +0 -0
  192. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/script/__init__.py +0 -0
  193. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/script/ast.py +0 -0
  194. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/script/control.py +0 -0
  195. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/state.py +0 -0
  196. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/__init__.py +0 -0
  197. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/crypto.py +0 -0
  198. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/strings.py +0 -0
  199. {execsql2-2.21.2 → execsql2-2.22.0}/src/execsql/utils/timer.py +0 -0
  200. {execsql2-2.21.2 → execsql2-2.22.0}/templates/README.md +0 -0
  201. {execsql2-2.21.2 → execsql2-2.22.0}/templates/config_settings.sqlite +0 -0
  202. {execsql2-2.21.2 → execsql2-2.22.0}/templates/example_config_prompt.sql +0 -0
  203. {execsql2-2.21.2 → execsql2-2.22.0}/templates/make_config_db.sql +0 -0
  204. {execsql2-2.21.2 → execsql2-2.22.0}/templates/md_compare.sql +0 -0
  205. {execsql2-2.21.2 → execsql2-2.22.0}/templates/md_glossary.sql +0 -0
  206. {execsql2-2.21.2 → execsql2-2.22.0}/templates/md_upsert.sql +0 -0
  207. {execsql2-2.21.2 → execsql2-2.22.0}/templates/pg_compare.sql +0 -0
  208. {execsql2-2.21.2 → execsql2-2.22.0}/templates/pg_glossary.sql +0 -0
  209. {execsql2-2.21.2 → execsql2-2.22.0}/templates/pg_upsert.sql +0 -0
  210. {execsql2-2.21.2 → execsql2-2.22.0}/templates/script_template.sql +0 -0
  211. {execsql2-2.21.2 → execsql2-2.22.0}/templates/ss_compare.sql +0 -0
  212. {execsql2-2.21.2 → execsql2-2.22.0}/templates/ss_glossary.sql +0 -0
  213. {execsql2-2.21.2 → execsql2-2.22.0}/templates/ss_upsert.sql +0 -0
  214. {execsql2-2.21.2 → execsql2-2.22.0}/tests/__init__.py +0 -0
  215. {execsql2-2.21.2 → execsql2-2.22.0}/tests/cli/__init__.py +0 -0
  216. {execsql2-2.21.2 → execsql2-2.22.0}/tests/cli/test_cli.py +0 -0
  217. {execsql2-2.21.2 → execsql2-2.22.0}/tests/cli/test_cli_e2e.py +0 -0
  218. {execsql2-2.21.2 → execsql2-2.22.0}/tests/cli/test_lint.py +0 -0
  219. {execsql2-2.21.2 → execsql2-2.22.0}/tests/cli/test_ping.py +0 -0
  220. {execsql2-2.21.2 → execsql2-2.22.0}/tests/cli/test_profile.py +0 -0
  221. {execsql2-2.21.2 → execsql2-2.22.0}/tests/conftest.py +0 -0
  222. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/__init__.py +0 -0
  223. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_access_windows.py +0 -0
  224. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_base.py +0 -0
  225. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_db_adapters_mocked.py +0 -0
  226. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  227. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_dsn.py +0 -0
  228. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_duckdb.py +0 -0
  229. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_factory.py +0 -0
  230. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_mysql_inprocess.py +0 -0
  231. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_postgres.py +0 -0
  232. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_postgres_inprocess.py +0 -0
  233. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_sqlite.py +0 -0
  234. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_sqlite_extra.py +0 -0
  235. {execsql2-2.21.2 → execsql2-2.22.0}/tests/db/test_sqlserver_inprocess.py +0 -0
  236. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/__init__.py +0 -0
  237. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_base.py +0 -0
  238. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_db.py +0 -0
  239. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_delimited.py +0 -0
  240. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_exporters.py +0 -0
  241. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_html_extended.py +0 -0
  242. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_html_latex.py +0 -0
  243. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_json.py +0 -0
  244. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_json_extended.py +0 -0
  245. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_markdown.py +0 -0
  246. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_ods.py +0 -0
  247. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_ods_export.py +0 -0
  248. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_parquet.py +0 -0
  249. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_pretty_extended.py +0 -0
  250. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_raw_extended.py +0 -0
  251. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_templates.py +0 -0
  252. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_templates_extended.py +0 -0
  253. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_values_extended.py +0 -0
  254. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_xls_xlsx.py +0 -0
  255. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_xlsx.py +0 -0
  256. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_xml.py +0 -0
  257. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_yaml.py +0 -0
  258. {execsql2-2.21.2 → execsql2-2.22.0}/tests/exporters/test_zip.py +0 -0
  259. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/__init__.py +0 -0
  260. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/test_backends.py +0 -0
  261. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/test_backends_extended.py +0 -0
  262. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/test_compare_stats.py +0 -0
  263. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/test_compute_row_diffs.py +0 -0
  264. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/test_desktop_dialogs.py +0 -0
  265. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/test_tui_pilot.py +0 -0
  266. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/test_tui_pilot_complex.py +0 -0
  267. {execsql2-2.21.2 → execsql2-2.22.0}/tests/gui/test_utils_gui_extended.py +0 -0
  268. {execsql2-2.21.2 → execsql2-2.22.0}/tests/importers/__init__.py +0 -0
  269. {execsql2-2.21.2 → execsql2-2.22.0}/tests/importers/test_csv_edge_cases.py +0 -0
  270. {execsql2-2.21.2 → execsql2-2.22.0}/tests/importers/test_feather_importer.py +0 -0
  271. {execsql2-2.21.2 → execsql2-2.22.0}/tests/importers/test_json_importer.py +0 -0
  272. {execsql2-2.21.2 → execsql2-2.22.0}/tests/importers/test_ods_importer.py +0 -0
  273. {execsql2-2.21.2 → execsql2-2.22.0}/tests/importers/test_xls_importer.py +0 -0
  274. {execsql2-2.21.2 → execsql2-2.22.0}/tests/integration/__init__.py +0 -0
  275. {execsql2-2.21.2 → execsql2-2.22.0}/tests/integration/conftest.py +0 -0
  276. {execsql2-2.21.2 → execsql2-2.22.0}/tests/integration/test_dsn.py +0 -0
  277. {execsql2-2.21.2 → execsql2-2.22.0}/tests/integration/test_duckdb.py +0 -0
  278. {execsql2-2.21.2 → execsql2-2.22.0}/tests/integration/test_mysql.py +0 -0
  279. {execsql2-2.21.2 → execsql2-2.22.0}/tests/integration/test_postgres.py +0 -0
  280. {execsql2-2.21.2 → execsql2-2.22.0}/tests/integration/test_sqlite.py +0 -0
  281. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/__init__.py +0 -0
  282. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_assert.py +0 -0
  283. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_breakpoint.py +0 -0
  284. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_conditions_extra.py +0 -0
  285. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_connect.py +0 -0
  286. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_io_export.py +0 -0
  287. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_io_import.py +0 -0
  288. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands.py +0 -0
  289. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  290. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_data.py +0 -0
  291. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_extended.py +0 -0
  292. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_io.py +0 -0
  293. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  294. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  295. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  296. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_pg_upsert.py +0 -0
  297. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_prompt.py +0 -0
  298. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_row_count.py +0 -0
  299. {execsql2-2.21.2 → execsql2-2.22.0}/tests/metacommands/test_show_scripts.py +0 -0
  300. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/__init__.py +0 -0
  301. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  302. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  303. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  304. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/audit_smoke.sql +0 -0
  305. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/config_runtime.sql +0 -0
  306. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/control_flow.sql +0 -0
  307. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  308. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/error_handling.sql +0 -0
  309. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  310. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/import_options.sql +0 -0
  311. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/includes/helper.sql +0 -0
  312. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/includes.sql +0 -0
  313. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/io_formats.sql +0 -0
  314. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  315. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/multi_db.sql +0 -0
  316. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  317. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  318. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/predicates.sql +0 -0
  319. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/scripts.sql +0 -0
  320. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/smoke.sql +0 -0
  321. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  322. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  323. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  324. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/template_export.sql +0 -0
  325. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/timer.sql +0 -0
  326. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/transactions.sql +0 -0
  327. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/write_create_table.sql +0 -0
  328. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  329. {execsql2-2.21.2 → execsql2-2.22.0}/tests/scripts/test_sql_scripts.py +0 -0
  330. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/__init__.py +0 -0
  331. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/test_env_detection.py +0 -0
  332. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/test_expansion_bomb.py +0 -0
  333. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/test_import_regex_hardening.py +0 -0
  334. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/test_odbc_injection.py +0 -0
  335. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/test_path_containment.py +0 -0
  336. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/test_substitution_injection.py +0 -0
  337. {execsql2-2.21.2 → execsql2-2.22.0}/tests/security/test_zip_bomb.py +0 -0
  338. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_api_extended.py +0 -0
  339. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_ast.py +0 -0
  340. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_ast_parser.py +0 -0
  341. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_bundled_templates.py +0 -0
  342. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_config.py +0 -0
  343. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_config_extended.py +0 -0
  344. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_debug_repl.py +0 -0
  345. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_engine.py +0 -0
  346. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_error_messages.py +0 -0
  347. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_exceptions.py +0 -0
  348. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_executor.py +0 -0
  349. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_executor_inprocess.py +0 -0
  350. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_format.py +0 -0
  351. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_mail.py +0 -0
  352. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_package.py +0 -0
  353. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_parser.py +0 -0
  354. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_parser_params.py +0 -0
  355. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_plugins.py +0 -0
  356. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_registry.py +0 -0
  357. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_script.py +0 -0
  358. {execsql2-2.21.2 → execsql2-2.22.0}/tests/test_state.py +0 -0
  359. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/__init__.py +0 -0
  360. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_auth.py +0 -0
  361. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_auth_extra.py +0 -0
  362. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_crypto.py +0 -0
  363. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_datetime.py +0 -0
  364. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_encodedfile_context.py +0 -0
  365. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_errors.py +0 -0
  366. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_errors_extra.py +0 -0
  367. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_fileio.py +0 -0
  368. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_fileio_extra.py +0 -0
  369. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_numeric.py +0 -0
  370. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_regex.py +0 -0
  371. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_strings.py +0 -0
  372. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_timer.py +0 -0
  373. {execsql2-2.21.2 → execsql2-2.22.0}/tests/utils/test_timer_extra.py +0 -0
  374. {execsql2-2.21.2 → execsql2-2.22.0}/zensical.toml +0 -0
@@ -28,6 +28,55 @@ jobs:
28
28
  - run: ruff check src/ tests/
29
29
  - run: ruff format --check src/ tests/
30
30
 
31
+ typecheck:
32
+ name: typecheck
33
+ runs-on: ubuntu-latest
34
+ permissions:
35
+ contents: read
36
+ steps:
37
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
38
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
39
+ with:
40
+ python-version: "3.13"
41
+ cache: "pip"
42
+ - run: python -m pip install --upgrade pip
43
+ - run: python -m pip install ".[dev]"
44
+ - run: mypy src/execsql/
45
+
46
+ pre-commit:
47
+ name: pre-commit
48
+ runs-on: ubuntu-latest
49
+ permissions:
50
+ contents: read
51
+ steps:
52
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
53
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
54
+ with:
55
+ python-version: "3.13"
56
+ cache: "pip"
57
+ - run: python -m pip install --upgrade pip
58
+ - run: python -m pip install ".[dev]"
59
+ # mypy runs in the dedicated typecheck job; the local hook needs uv
60
+ - run: pre-commit run --all-files
61
+ env:
62
+ SKIP: mypy
63
+
64
+ docs:
65
+ name: docs
66
+ runs-on: ubuntu-latest
67
+ permissions:
68
+ contents: read
69
+ steps:
70
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
71
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
72
+ with:
73
+ python-version: "3.13"
74
+ cache: "pip"
75
+ - run: python -m pip install --upgrade pip
76
+ - run: python -m pip install ".[dev]"
77
+ - run: cp CHANGELOG.md docs/about/change_log.md
78
+ - run: zensical build
79
+
31
80
  tests:
32
81
  strategy:
33
82
  fail-fast: false
@@ -233,7 +282,7 @@ jobs:
233
282
  build:
234
283
  name: Build distribution 📦
235
284
  runs-on: ubuntu-latest
236
- needs: [ lint, tests, integration-tests ]
285
+ needs: [ lint, typecheck, pre-commit, docs, tests, integration-tests ]
237
286
  if: startsWith(github.ref, 'refs/tags/v')
238
287
  permissions:
239
288
  contents: read
@@ -80,3 +80,15 @@ repos:
80
80
  rev: v0.25
81
81
  hooks:
82
82
  - id: validate-pyproject
83
+ # Static type checking — same command as `just typecheck`, run in the
84
+ # project environment so results match local and CI. Whole-program run
85
+ # (pass_filenames: false) because mypy cannot check files in isolation.
86
+ # The CI pre-commit job sets SKIP=mypy; CI has a dedicated typecheck gate.
87
+ - repo: local
88
+ hooks:
89
+ - id: mypy
90
+ name: mypy
91
+ entry: uv run mypy src/execsql/
92
+ language: system
93
+ pass_filenames: false
94
+ files: ^(src/execsql/.*\.pyi?|pyproject\.toml)$
@@ -13,6 +13,21 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.22.0] - 2026-07-03
17
+
18
+ ### Added
19
+
20
+ - `run()` now accepts `allow_rm_file=False` and `allow_serve=False` to disable the `RM_FILE` and `SERVE` metacommands for library callers, matching the existing `allow_system_cmd` kwarg and the `--no-rm-file` / `--no-serve` CLI flags.
21
+ - `[config] system_cmd_timeout` can bound foreground `SYSTEM_CMD` execution; the default remains no timeout.
22
+
23
+ ### Fixed
24
+
25
+ - `EXPORT … FORMAT SQLITE` and `EXPORT … FORMAT DUCKDB` correctly preserve source column types when exporting from a typed database: text columns stay text (not re-inferred as TIMESTAMP or numeric from value shape), `decimal.Decimal` values bind without error (converted to their exact string representation for SQLite), and unsupported Python objects such as PostGIS geometry are stringified instead of raising a bind error.
26
+ - `EXPORT … FORMAT LATEX` into a ZIP archive no longer crashes with `AttributeError`.
27
+ - Connecting to a server-based database without a server or database name now stops with a clear error message instead of a driver traceback.
28
+
29
+ ______________________________________________________________________
30
+
16
31
  ## [2.21.2] - 2026-06-30
17
32
 
18
33
  ### Fixed
@@ -32,13 +32,19 @@ Linting and formatting are handled by [ruff](https://docs.astral.sh/ruff/):
32
32
  just lint
33
33
  ```
34
34
 
35
- [pre-commit](https://pre-commit.com/) hooks enforce additional checks (gitleaks, uv-lock, mdformat, markdownlint, typos, validate-pyproject) on every commit. To run them manually against all files:
35
+ Static type checking is handled by [mypy](https://mypy.readthedocs.io/) and must pass with zero errors:
36
+
37
+ ```bash
38
+ just typecheck
39
+ ```
40
+
41
+ [pre-commit](https://pre-commit.com/) hooks enforce additional checks (gitleaks, uv-lock, mdformat, markdownlint, typos, validate-pyproject, mypy) on every commit. To run them manually against all files:
36
42
 
37
43
  ```bash
38
44
  just pre-commit
39
45
  ```
40
46
 
41
- CI rejects PRs that fail linting or any pre-commit check.
47
+ CI rejects PRs that fail ruff lint/format, mypy type checking, the full pre-commit suite, docs build, or tests.
42
48
 
43
49
  ## Running Tests
44
50
 
@@ -96,12 +102,18 @@ Pre-releases are published to PyPI but are not installed by default — `pip ins
96
102
 
97
103
  Triggered on pushes to `main`, any `v*.*.*` tag, and pull requests.
98
104
 
99
- | Job | Trigger | What it does |
100
- | ------------------ | ------------------ | ----------------------------------------------- |
101
- | `tests` | all events | Runs the test matrix (3 OS × 5 Python versions) |
102
- | `build` | `v*.*.*` tags only | Builds sdist + wheel, checks with twine |
103
- | `publish` | `v*.*.*` tags only | Publishes to PyPI via OIDC trusted publishing |
104
- | `generate-release` | `v*.*.*` tags only | Creates a GitHub Release with dist artifacts |
105
+ | Job | Trigger | What it does |
106
+ | ---------------------- | ------------------ | ----------------------------------------------------- |
107
+ | `lint` | all events | Runs `ruff check` and `ruff format --check` |
108
+ | `typecheck` | all events | Runs `mypy src/execsql/` (zero-error gate) |
109
+ | `pre-commit` | all events | Runs the full pre-commit suite against all files |
110
+ | `docs` | all events | Builds the documentation site |
111
+ | `tests` | all events | Runs the test matrix (3 OS × 5 Python versions) |
112
+ | `integration-tests` | all events | Runs tests against live database service containers |
113
+ | `access-tests-windows` | all events | Runs MS Access driver tests on Windows (non-blocking) |
114
+ | `build` | `v*.*.*` tags only | Builds sdist + wheel, checks with twine |
115
+ | `publish` | `v*.*.*` tags only | Publishes to PyPI via OIDC trusted publishing |
116
+ | `generate-release` | `v*.*.*` tags only | Creates a GitHub Release with dist artifacts |
105
117
 
106
118
  ### PyPI trusted publishing setup
107
119
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.21.2
3
+ Version: 2.22.0
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -173,7 +173,7 @@ Optional extras install database drivers and feature bundles:
173
173
 
174
174
  ```bash
175
175
  # Database drivers
176
- pip install execsql2[postgres] # PostgreSQL (psycopg2-binary)
176
+ pip install execsql2[postgres] # PostgreSQL (psycopg3 / psycopg[binary])
177
177
  pip install execsql2[mysql] # MySQL / MariaDB (pymysql)
178
178
  pip install execsql2[mssql] # SQL Server (pyodbc)
179
179
  pip install execsql2[duckdb] # DuckDB
@@ -403,7 +403,7 @@ execsql-format --no-sql --in-place scripts/
403
403
  ```yaml
404
404
  repos:
405
405
  - repo: https://github.com/geocoug/execsql
406
- rev: v2.21.2
406
+ rev: v2.22.0
407
407
  hooks:
408
408
  - id: execsql-format
409
409
  ```
@@ -42,7 +42,7 @@ Optional extras install database drivers and feature bundles:
42
42
 
43
43
  ```bash
44
44
  # Database drivers
45
- pip install execsql2[postgres] # PostgreSQL (psycopg2-binary)
45
+ pip install execsql2[postgres] # PostgreSQL (psycopg3 / psycopg[binary])
46
46
  pip install execsql2[mysql] # MySQL / MariaDB (pymysql)
47
47
  pip install execsql2[mssql] # SQL Server (pyodbc)
48
48
  pip install execsql2[duckdb] # DuckDB
@@ -272,7 +272,7 @@ execsql-format --no-sql --in-place scripts/
272
272
  ```yaml
273
273
  repos:
274
274
  - repo: https://github.com/geocoug/execsql
275
- rev: v2.21.2
275
+ rev: v2.22.0
276
276
  hooks:
277
277
  - id: execsql-format
278
278
  ```
@@ -179,7 +179,7 @@ ______________________________________________________________________
179
179
 
180
180
  ### CLI Interface
181
181
 
182
- The CLI framework changed from `optparse` to [Typer](https://typer.tiangolo.com/) with Rich-formatted help text. All original short flags (`-a` through `-z`) are preserved. The tool can be invoked as either `execsql` or `execsql2`.
182
+ The CLI framework changed from `optparse` to [Typer](https://typer.tiangolo.com/) with Rich-formatted help text. All original short flags (`-a` through `-z`) are preserved. The PyPI distribution is named `execsql2`, but the installed executable remains `execsql` for compatibility.
183
183
 
184
184
  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.
185
185
 
@@ -271,45 +271,49 @@ These are behavioral changes driven by security or correctness issues in the ups
271
271
 
272
272
  ### Bug Fixes
273
273
 
274
- | Area | Fix |
275
- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
276
- | Oracle default port | Corrected from `5432` (PostgreSQL) to `1521`. |
277
- | MySQL `LOAD DATA INFILE` encoding | Python encoding names are now mapped to MySQL charset names. |
278
- | `dt_cast` type converters | Base `Database` class auto-populates 8 type converters that were previously left empty after the refactor. |
279
- | `FileWriter` CPU busy-loop | Uses blocking `queue.get(timeout=0.1)` instead of `get_nowait()` in a tight loop. |
280
- | Substitution variable cycles | 100-iteration limit prevents infinite loops on cyclic variable references. |
281
- | Script location in error messages | `ErrInfo.script_file` and `script_line_no` are now populated via `stamp_errinfo()` so error output includes "Line N of script foo.sql" context — restoring behavior present in the monolith. |
282
- | `$ERROR_MESSAGE` not updated | `$ERROR_MESSAGE` is now set on every error path: `exit_now()`, non-halting SQL errors, and non-halting metacommand errors. Previously it was initialized to `""` and never changed. |
283
- | Metacommand error message lost | When `halt_on_metacommand_err` is `ON`, the original handler `ErrInfo` is now re-raised; the generic "Unknown metacommand" message no longer replaces the specific error from the handler. |
284
- | Empty script name in error msg | `_execute_script_direct()` and `_execute_script_textual_console()` no longer append "in script , line 0" to uncaught-exception messages when `current_script_line()` returns an empty string. |
285
- | `PROMPT COMPARE` diff comparison | Diff engine uses native Python equality instead of string comparison — numeric types, Decimals, and booleans compare correctly. `None` is distinguished from empty string. Columns are matched by name (not position), key columns are excluded from comparison, and duplicate PKs keep the first row. |
286
- | `win_config_file` broken | Checked `os.name == "windows"` which Python never returns (correct value is `"nt"`). Fixed to `os.name == "nt"`. |
287
- | ELSEIF + ANDIF/ORIF | ANDIF/ORIF after an ELSEIF were silently attached to the parent IF condition instead of the ELSEIF clause. The compound condition was never evaluated for the ELSEIF branch. Fixed in the AST parser and executor. |
288
- | Cursor leak in `select_rowsource()` | Cursor was not closed on query execution failure. Row generators in EXPORT and COPY paths were not explicitly closed on error, relying on garbage collection for cleanup. |
289
- | `NumericParser` right-associative | Arithmetic operators were parsed right-to-left. `10 - 3 - 2` evaluated as `9` instead of `5`. Fixed to left-associative parsing. |
290
- | Empty-column check precedence | `DataTable` and `Database.populate_table()` had an operator precedence bug in the extra-column emptiness check — a redundant `and conf.del_empty_cols` caused incorrect short-circuit evaluation. |
291
- | SQLite import string processing | `SQLiteDatabase.populate_table()` applied `trim_strings`, `replace_newlines`, and `empty_strings` after copying row data, so processing never reached the INSERT. Fixed to process before extraction. |
292
- | `$CURRENT_DATABASE`/`$CURRENT_DBMS` stale after USE | These variables were only set at startup and on CONNECT, not refreshed when `USE` switched the active database. Now set in `set_static_system_vars()` so they update on any connection change. |
293
- | `DT_Text.data_type_name` wrong | Was `"character"` (same as `DT_Character`), making error messages indistinguishable. Corrected to `"text"`. |
294
- | `DT_Varchar` non-string data unchecked | `_from_data()` only enforced the 255-char limit for `str` inputs; non-string values passed through without conversion or length check. |
295
- | `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. |
296
- | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. |
297
- | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. |
298
- | `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. |
299
- | `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. |
300
- | `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. |
301
- | 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. |
302
- | 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. |
303
- | 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:`. |
304
- | JSON export malformed on special column names | Column names containing `"` or `\` produced invalid JSON. Now uses `json.dumps()` for all field names. |
305
- | Temp file creation TOCTOU race | `TempFileMgr.new_temp_fn()` discarded the `NamedTemporaryFile` handle, creating a race window. Now uses `tempfile.mkstemp()` for secure creation. |
306
- | `shlex.split` on Windows incorrect mode | Called without `posix=False` on Windows, mishandling backslash-heavy paths in SHELL commands. |
307
- | AST executor `~`/`+` variable scoping broken | The AST executor passed `localvars` through function parameters but never pushed `CommandList` frames onto `commandliststack`. Legacy metacommand handlers (`x_sub`, `x_rm_sub`, `xf_sub_defined`, `SUB_LOCAL`, prompt handlers, REPL) access `commandliststack[-1]` for `~` local and `+` outer-scope variables. This caused `~` vars to be invisible to SQL, `SUB_DEFINED(~var)` to always return false, and the REPL `.vars`/`.stack` to show empty state. Fixed by pushing/popping `CommandList` frames in `execute()` and `_execute_script_native()`. |
308
- | Firebird adapter import | Adapter imported `fdb` (the unmaintained legacy driver) while the `[firebird]` extra declared `firebird-driver`. `pip install execsql2[firebird]` followed by a Firebird connect raised `ModuleNotFoundError: No module named 'fdb'`. Adapter now imports `firebird.driver`, matching the declared dependency. |
309
- | Connect timeouts on network adapters | MySQL, SQL Server / Access / DSN (pyodbc), Firebird, and SMTP all now apply a 30 s connect timeout matching the existing PostgreSQL default. A silently-dropped network peer (firewall rule, dead VM) used to hang scripts indefinitely; now the connect fails fast. |
310
- | AST parser INCLUDE quoted paths broken | The AST parser captured the full INCLUDE target including surrounding quotes (`"path"`), but the legacy dispatch regex stripped them. Quoted INCLUDE paths failed with "File does not exist" even when the file was present. Fixed by stripping matched quote pairs in the parser. |
311
- | AST parser `BEGIN SCRIPT name(params)` rejected | The regex required whitespace between the script name and parameter list. `BEGIN SCRIPT foo(a,b)` (no space before `(`) silently failed to match, causing the matching `END SCRIPT` to raise "Unmatched END SCRIPT metacommand." Fixed by allowing optional whitespace before the parameter expression. |
312
- | AST executor forward SCRIPT references broken | The legacy engine registered all `BEGIN SCRIPT` blocks at parse time (two-pass), so `EXECUTE SCRIPT foo` could appear before the `BEGIN SCRIPT foo` definition. The AST executor walked the tree in a single pass, so forward references failed with "There is no SCRIPT named foo." Fixed by adding a pre-registration scan of all SCRIPT blocks before execution begins. |
274
+ | Area | Fix |
275
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
276
+ | Oracle default port | Corrected from `5432` (PostgreSQL) to `1521`. |
277
+ | MySQL `LOAD DATA INFILE` encoding | Python encoding names are now mapped to MySQL charset names. |
278
+ | `dt_cast` type converters | Base `Database` class auto-populates 8 type converters that were previously left empty after the refactor. |
279
+ | `FileWriter` CPU busy-loop | Uses blocking `queue.get(timeout=0.1)` instead of `get_nowait()` in a tight loop. |
280
+ | Substitution variable cycles | 100-iteration limit prevents infinite loops on cyclic variable references. |
281
+ | Script location in error messages | `ErrInfo.script_file` and `script_line_no` are now populated via `stamp_errinfo()` so error output includes "Line N of script foo.sql" context — restoring behavior present in the monolith. |
282
+ | `$ERROR_MESSAGE` not updated | `$ERROR_MESSAGE` is now set on every error path: `exit_now()`, non-halting SQL errors, and non-halting metacommand errors. Previously it was initialized to `""` and never changed. |
283
+ | Metacommand error message lost | When `halt_on_metacommand_err` is `ON`, the original handler `ErrInfo` is now re-raised; the generic "Unknown metacommand" message no longer replaces the specific error from the handler. |
284
+ | Empty script name in error msg | `_execute_script_direct()` and `_execute_script_textual_console()` no longer append "in script , line 0" to uncaught-exception messages when `current_script_line()` returns an empty string. |
285
+ | `PROMPT COMPARE` diff comparison | Diff engine uses native Python equality instead of string comparison — numeric types, Decimals, and booleans compare correctly. `None` is distinguished from empty string. Columns are matched by name (not position), key columns are excluded from comparison, and duplicate PKs keep the first row. |
286
+ | `win_config_file` broken | Checked `os.name == "windows"` which Python never returns (correct value is `"nt"`). Fixed to `os.name == "nt"`. |
287
+ | ELSEIF + ANDIF/ORIF | ANDIF/ORIF after an ELSEIF were silently attached to the parent IF condition instead of the ELSEIF clause. The compound condition was never evaluated for the ELSEIF branch. Fixed in the AST parser and executor. |
288
+ | Cursor leak in `select_rowsource()` | Cursor was not closed on query execution failure. Row generators in EXPORT and COPY paths were not explicitly closed on error, relying on garbage collection for cleanup. |
289
+ | `NumericParser` right-associative | Arithmetic operators were parsed right-to-left. `10 - 3 - 2` evaluated as `9` instead of `5`. Fixed to left-associative parsing. |
290
+ | Empty-column check precedence | `DataTable` and `Database.populate_table()` had an operator precedence bug in the extra-column emptiness check — a redundant `and conf.del_empty_cols` caused incorrect short-circuit evaluation. |
291
+ | SQLite import string processing | `SQLiteDatabase.populate_table()` applied `trim_strings`, `replace_newlines`, and `empty_strings` after copying row data, so processing never reached the INSERT. Fixed to process before extraction. |
292
+ | `$CURRENT_DATABASE`/`$CURRENT_DBMS` stale after USE | These variables were only set at startup and on CONNECT, not refreshed when `USE` switched the active database. Now set in `set_static_system_vars()` so they update on any connection change. |
293
+ | `DT_Text.data_type_name` wrong | Was `"character"` (same as `DT_Character`), making error messages indistinguishable. Corrected to `"text"`. |
294
+ | `DT_Varchar` non-string data unchecked | `_from_data()` only enforced the 255-char limit for `str` inputs; non-string values passed through without conversion or length check. |
295
+ | `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. |
296
+ | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. |
297
+ | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. |
298
+ | `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. |
299
+ | `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. |
300
+ | `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. |
301
+ | 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. |
302
+ | 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`. |
303
+ | 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. |
304
+ | 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:`. |
305
+ | JSON export malformed on special column names | Column names containing `"` or `\` produced invalid JSON. Now uses `json.dumps()` for all field names. |
306
+ | Temp file creation TOCTOU race | `TempFileMgr.new_temp_fn()` discarded the `NamedTemporaryFile` handle, creating a race window. Now uses `tempfile.mkstemp()` for secure creation. |
307
+ | `shlex.split` on Windows incorrect mode | Called without `posix=False` on Windows, mishandling backslash-heavy paths in SHELL commands. |
308
+ | AST executor `~`/`+` variable scoping broken | The AST executor passed `localvars` through function parameters but never pushed `CommandList` frames onto `commandliststack`. Legacy metacommand handlers (`x_sub`, `x_rm_sub`, `xf_sub_defined`, `SUB_LOCAL`, prompt handlers, REPL) access `commandliststack[-1]` for `~` local and `+` outer-scope variables. This caused `~` vars to be invisible to SQL, `SUB_DEFINED(~var)` to always return false, and the REPL `.vars`/`.stack` to show empty state. Fixed by pushing/popping `CommandList` frames in `execute()` and `_execute_script_native()`. |
309
+ | Firebird adapter import | Adapter imported `fdb` (the unmaintained legacy driver) while the `[firebird]` extra declared `firebird-driver`. `pip install execsql2[firebird]` followed by a Firebird connect raised `ModuleNotFoundError: No module named 'fdb'`. Adapter now imports `firebird.driver`, matching the declared dependency. |
310
+ | Connect timeouts on network adapters | MySQL, SQL Server / Access / DSN (pyodbc), Firebird, and SMTP all now apply a 30 s connect timeout matching the existing PostgreSQL default. A silently-dropped network peer (firewall rule, dead VM) used to hang scripts indefinitely; now the connect fails fast. |
311
+ | AST parser INCLUDE quoted paths broken | The AST parser captured the full INCLUDE target including surrounding quotes (`"path"`), but the legacy dispatch regex stripped them. Quoted INCLUDE paths failed with "File does not exist" even when the file was present. Fixed by stripping matched quote pairs in the parser. |
312
+ | AST parser `BEGIN SCRIPT name(params)` rejected | The regex required whitespace between the script name and parameter list. `BEGIN SCRIPT foo(a,b)` (no space before `(`) silently failed to match, causing the matching `END SCRIPT` to raise "Unmatched END SCRIPT metacommand." Fixed by allowing optional whitespace before the parameter expression. |
313
+ | AST executor forward SCRIPT references broken | The legacy engine registered all `BEGIN SCRIPT` blocks at parse time (two-pass), so `EXECUTE SCRIPT foo` could appear before the `BEGIN SCRIPT foo` definition. The AST executor walked the tree in a single pass, so forward references failed with "There is no SCRIPT named foo." Fixed by adding a pre-registration scan of all SCRIPT blocks before execution begins. |
314
+ | DB-export type inference overrides source types | `DataTable`/`Column` inferred column types from the *string shape* of values — correct for CSV import (everything is `str`) but wrong for DB-query exports where the driver returns typed Python objects. A text column with values like `"2026-06-30 10:07:20 PM"` was inferred as `TIMESTAMP`, causing DuckDB bind errors. SQLite, DuckDB, HDF5, and JSON-schema exports now pass `infer_strings=False` to `DataTable`: string values are classified as text types only, while native Python objects (`Decimal`, `datetime`, etc.) still run through the full accumulator set for accurate schema inference. |
315
+ | `EXPORT FORMAT SQLITE` rejects `decimal.Decimal` | SQLite's Python adapter does not accept `Decimal` in `executemany`, raising `ProgrammingError`. SQLite exports now convert `Decimal` values to their exact string representation before binding; SQLite's NUMERIC affinity stores them without loss of precision. |
316
+ | Unsupported Python objects crash native exports | `EXPORT FORMAT SQLITE` and `EXPORT FORMAT DUCKDB` raised bind errors when the DB driver returned user-defined Python objects (e.g. PostGIS geometry). Both exporters now stringify any object whose type is not in the adapter's safe-binding set, preserving values as their `str()` form instead of crashing. |
313
317
 
314
318
  ______________________________________________________________________
315
319
 
@@ -6,7 +6,7 @@ ______________________________________________________________________
6
6
 
7
7
  ## Execution Flow
8
8
 
9
- When a user runs `execsql2 script.sql mydb.sqlite -t l`, the following sequence occurs:
9
+ When a user runs `execsql script.sql mydb.sqlite -t l`, the following sequence occurs:
10
10
 
11
11
  ```mermaid
12
12
  flowchart TD
@@ -14,7 +14,8 @@ Or with [uv](https://docs.astral.sh/uv/):
14
14
  uv add execsql2
15
15
  ```
16
16
 
17
- This installs the `execsql2` command-line tool.
17
+ This installs the `execsql` and `execsql-format` command-line tools. The PyPI distribution is named
18
+ `execsql2`; the executable remains `execsql` for compatibility.
18
19
 
19
20
  To install with optional dependencies:
20
21
 
@@ -160,7 +160,7 @@ select id,name,created_at from users where active = true order by name;
160
160
  ```yaml
161
161
  repos:
162
162
  - repo: https://github.com/geocoug/execsql
163
- rev: v2.21.2
163
+ rev: v2.22.0
164
164
  hooks:
165
165
  - id: execsql-format
166
166
  ```
@@ -12,7 +12,7 @@ When adding a very large amount of data with the [IMPORT](../reference/metacomma
12
12
 
13
13
  ## Implicit DROP TABLE Statements
14
14
 
15
- The "REPLACEMENT" keyword for the [IMPORT](../reference/metacommands.md#import) and [COPY](../reference/metacommands.md#copy) metacommands allows a previously existing table to be replaced. To accomplish this, execsql issues a "DROP TABLE" statement to the database in use. PostgreSQL, SQLite, MySQL, MariaDB, Oracle, and DuckDB support a form of the "DROP TABLE" statement that automatically removes all foreign keys to the named table. execsql uses these forms of the "DROP TABLE" statement for these DBMSs, and therefore use of the "REPLACEMENT" keyword always succeeds at removing the named table before trying to create a new table with the same name. SQL Server, MS-Access, and Firebird do not have a form of the "DROP TABLE" statement that automatically removes foreign keys. Therefore, if the "REPLACEMENT" keyword is used with any of these three DBMSs, for a table that has foreign keys into it, that table will not be dropped, and an error will subsequently occur when execsql issues a "CREATE TABLE" statement to create a new table of the same name. To avoid this, when using any of these three DBMSs, you should include in the script the appropriate SQL commands to remove foreign keys (and possibly even to remove the table) before using the IMPORT or COPY metacommands.
15
+ The "REPLACEMENT" keyword for the [IMPORT](../reference/metacommands.md#import) and [COPY](../reference/metacommands.md#copy) metacommands allows a previously existing table to be replaced. To accomplish this, execsql issues a "DROP TABLE" statement to the database in use. This is not an atomic replacement: if the drop succeeds but a later create or load step fails, the old table has already been removed. If the drop fails, execsql stops the metacommand before creating or loading the replacement table. PostgreSQL, SQLite, MySQL, MariaDB, Oracle, and DuckDB support a form of the "DROP TABLE" statement that automatically removes all foreign keys to the named table. execsql uses these forms of the "DROP TABLE" statement for these DBMSs. SQL Server, MS-Access, and Firebird do not have a form of the "DROP TABLE" statement that automatically removes foreign keys. Therefore, if the "REPLACEMENT" keyword is used with any of these three DBMSs, for a table that has foreign keys into it, that table will not be dropped, and the IMPORT or COPY command will stop. To avoid this, when using any of these three DBMSs, you should include in the script the appropriate SQL commands to remove foreign keys (and possibly even to remove the table) before using the IMPORT or COPY metacommands.
16
16
 
17
17
  ## Boolean Data Types
18
18
 
@@ -333,6 +333,9 @@ The section and property names that may be used in a configuration file are list
333
333
  `allow_system_cmd` { #allow_system_cmd }
334
334
  : When set to "No", the `SYSTEM_CMD` (SHELL) metacommand is disabled. Any script that attempts to execute an OS command will fail with an error. The default is "Yes". This can also be set via the `--no-system-cmd` CLI flag or `allow_system_cmd=False` in the library API. See [Security — Disabling SYSTEM_CMD](security.md#disable_system_cmd) for details.
335
335
 
336
+ `system_cmd_timeout` { #system_cmd_timeout }
337
+ : Maximum number of seconds a foreground `SYSTEM_CMD` may run before *execsql* stops it. The default is `0`, which means no timeout. When a foreground command times out, `$SYSTEM_CMD_EXIT_STATUS` is set to `124` and the metacommand fails. `SYSTEM_CMD ... CONTINUE` starts a detached background process and is not timeout-managed by this setting.
338
+
336
339
  `allow_rm_file` { #allow_rm_file }
337
340
  : When set to "No", the `RM_FILE` metacommand (which deletes a file) is disabled. Any script that attempts to remove a file will fail with an error. The default is "Yes". This can also be set via the `--no-rm-file` CLI flag or `allow_rm_file=False` in the library API. Symmetric with `allow_system_cmd`.
338
341
 
@@ -857,8 +857,8 @@ The second (destination) table must have column names that are identical to the
857
857
 
858
858
  If the "NEW" keyword is used, the destination table will be automatically created with column names and data types that are compatible with the first (source) table. The data types used for the columns in the newly created table will be determined by a scan of all of the data in the first table, but may not exactly match those in the first table. If the destination table already exists when the "NEW" keyword is used, an error will occur.
859
859
 
860
- If the "REPLACEMENT" keyword is used, the destination table will also be created to be compatible with the source table, but any existing destination table of the same name will be dropped first. *execsql* uses a "drop table" statement to drop an existing destination table, and this statement may not succeed if there are dependencies on that table (see the discussion of [implicit drop table
861
- statements](../guides/sql_syntax.md#implicit_drop)). If the destination table is not dropped, then data from the source table will be added to the existing table, or an error will occur if the table formats are not compatible.
860
+ If the "REPLACEMENT" keyword is used, the destination table will also be created to be compatible with the source table, but any existing destination table of the same name will be dropped first. This replacement is not atomic: if the drop succeeds but a later create or load step fails, the old destination table has already been removed. If the drop fails, *execsql* stops the COPY command before creating or loading the replacement table. *execsql* uses a "drop table" statement to drop an existing destination table, and this statement may not succeed if there are dependencies on that table (see the discussion of [implicit drop table
861
+ statements](../guides/sql_syntax.md#implicit_drop)).
862
862
 
863
863
  If there are constraints on the second table that are not met by the data being added, an error will occur. If an error occurs at any point during the data copying process, no new data will be added to the second table.
864
864
 
@@ -1108,10 +1108,12 @@ DUCKDB
1108
1108
 
1109
1109
  : A [DuckDB](https://duckdb.org/) file-based database. If the specified database file does not exist, it will be created. If the database file already exists and has a table of the same name as the table or view that is exported, then the existing table will be replaced unless the "APPEND" keyword is used. When "APPEND" is used and the table already exists, an error will occur. The "DESCRIPTION" keyword is ignored when this format is used. DuckDB database files cannot be created in a zipfile; if the "ZIPFILE" keyword is used, an error will occur.
1110
1110
 
1111
+ When exporting a query result from a typed database (PostgreSQL, MySQL, etc.), the column schema is inferred from the Python types returned by the database driver rather than re-parsed from the string shape of values. A `TEXT` column whose values look like timestamps or numbers is exported as `VARCHAR`, not as `TIMESTAMP` or `INTEGER`. Python objects not natively bindable by DuckDB (e.g. PostGIS geometry) are stringified rather than raising a bind error.
1112
+
1111
1113
 
1112
1114
  FEATHER
1113
1115
 
1114
- : The Feather binary file format established by the [Apache Arrow](https://arrow.apache.org/) project. The "APPEND" and "DESCRIPTION" keywords are ignored when this format is used. Exporting data in this format requires that the entire data set be first converted to a [pandas](https://pandas.pydata.org/) data frame in memory, so there is a system-specific limit to the size of the data set that can be exported in this format. The *feather* and *pandas* libraries must be installed to export data in the Feather format. Not all data types that may be present in a database can necessarily be exported to a Feather data file, so some data types, like timestamps, may have to be converted to character data before export. Data exported in feather format cannot be written into a zipfile.
1116
+ : The Feather binary file format established by the [Apache Arrow](https://arrow.apache.org/) project. The "APPEND" and "DESCRIPTION" keywords are ignored when this format is used. Exporting data in this format uses [Polars](https://pola.rs/) and materializes the result in memory before writing, so there is a system-specific limit to the size of the data set that can be exported in this format. Install the `formats` extra, which includes `polars`, to export data in Feather format. Not all data types that may be present in a database can necessarily be exported to a Feather data file, so some data types, like timestamps, may have to be converted to character data before export. Data exported in Feather format cannot be written into a zipfile.
1115
1117
 
1116
1118
 
1117
1119
  HDF5
@@ -1163,6 +1165,8 @@ SQLITE
1163
1165
 
1164
1166
  : A [SQLite](https://www.sqlite.org/index.html) file-based database. If the specified database file does not exist, it will be created. If the database file already exists and has a table of the same name as the table or view that is exported, then the existing table will be replaced unless the "APPEND" keyword is used. When "APPEND" is used and the table already exists, an error will occur. The "DESCRIPTION" keyword is ignored when this format is used. SQLite database files cannot be created in a zipfile; if the "ZIPFILE" keyword is used, an error will occur.
1165
1167
 
1168
+ When exporting a query result from a typed database (PostgreSQL, MySQL, etc.), the column schema is inferred from the Python types returned by the database driver rather than re-parsed from the string shape of values. A `TEXT` column whose values look like timestamps or numbers is exported as a text column, not as `TIMESTAMP` or `INTEGER`. `decimal.Decimal` values are stored as their exact string representation (SQLite's NUMERIC affinity handles them losslessly). Python objects not natively bindable by SQLite (e.g. PostGIS geometry) are stringified rather than raising a bind error.
1169
+
1166
1170
 
1167
1171
 
1168
1172
  ODS
@@ -1905,7 +1909,7 @@ The delimiter characters that will be recognized in a text file, and that can be
1905
1909
 
1906
1910
  The SKIP key phrase specifies the number of lines (or rows) at the beginning of the file (or worksheet) to discard before evaluating the remainder of the input as a data table.
1907
1911
 
1908
- If the NEW keyword is used, the input will be scanned to determine the data type of each column, and a CREATE TABLE statement run to create a new table for the data. Scanning of the file to determine data formats is separate from the scanning that may be done to determine the quote and delimiter characters. If the table already exists when the NEW keyword is used, a fatal error will result and *execsql* will halt. If the REPLACEMENT keyword is used, the result is the same as if the NEW keyword were used, except that an existing table of the given name will be deleted first. *execsql* uses a "drop table" statement to drop an existing table, and the "drop table" statement may not succeed if there are dependencies on that table (see the discussion of [implicit drop table
1912
+ If the NEW keyword is used, the input will be scanned to determine the data type of each column, and a CREATE TABLE statement run to create a new table for the data. Scanning of the file to determine data formats is separate from the scanning that may be done to determine the quote and delimiter characters. If the table already exists when the NEW keyword is used, a fatal error will result and *execsql* will halt. If the REPLACEMENT keyword is used, the result is the same as if the NEW keyword were used, except that an existing table of the given name will be deleted first. This replacement is not atomic: if the drop succeeds but a later create or load step fails, the old table has already been removed. If the drop fails, *execsql* stops the IMPORT command before creating or loading the replacement table. *execsql* uses a "drop table" statement to drop an existing table, and the "drop table" statement may not succeed if there are dependencies on that table (see the discussion of [implicit drop table
1909
1913
  statements](../guides/sql_syntax.md#implicit_drop)). If the table to be dropped does not exist, an informational message will be written to the log.
1910
1914
 
1911
1915
  If neither the NEW or REPLACEMENT keywords are used, the table must exist, must have column names identical to those in the input data, and columns must have data types that are compatible with (though not necessarily identical to) those in the input data. If neither the NEW or REPLACEMENT keywords are used, the input data is not scanned to determine the data type of each column.
@@ -3061,7 +3065,7 @@ On non-POSIX operating systems (specifically, Windows), any backslashes in the c
3061
3065
 
3062
3066
  The command line that is run will be automatically [logged](../guides/logging.md#logging) in `execsql.log`.
3063
3067
 
3064
- The exit status of the command that is invoked will be stored in the [system variable](substitution_vars.md#system_vars) \$SYSTEM_CMD_EXIT_STATUS if the CONTINUE keyword has not been used. When the CONTINUE keyword is used, the process ID of the background process is stored in \$SYSTEM_CMD_PID.
3068
+ The exit status of the command that is invoked will be stored in the [system variable](substitution_vars.md#system_vars) \$SYSTEM_CMD_EXIT_STATUS if the CONTINUE keyword has not been used. A foreground command can be bounded with the [`system_cmd_timeout`](configuration.md#system_cmd_timeout) configuration setting; when a command times out, \$SYSTEM_CMD_EXIT_STATUS is set to `124` and the metacommand fails. When the CONTINUE keyword is used, the process ID of the background process is stored in \$SYSTEM_CMD_PID, and the background process is detached rather than timeout-managed.
3065
3069
 
3066
3070
 
3067
3071
  ## TIMER
@@ -109,9 +109,9 @@ This catches mainstream cloud, payment, observability, and VCS conventions: `AWS
109
109
 
110
110
  ## File System Access { #filesystem }
111
111
 
112
- execsql can read and write any file that the process user has permission to access. There is no base-directory restriction, no path allowlist, and no protection against `../` traversal sequences in output paths specified by `EXPORT`, `WRITE`, or `INCLUDE` metacommands.
112
+ By default, execsql can read and write any file that the process user has permission to access. Configure containment roots for shared or semi-trusted environments; otherwise path arguments may resolve outside the current directory.
113
113
 
114
- The [`INCLUDE`](metacommands.md#include) metacommand executes a script from any accessible path with full privileges. If the included path is constructed from a variable, an attacker who controls that variable can cause execsql to execute an arbitrary script file.
114
+ The [`INCLUDE`](metacommands.md#include) metacommand executes a script from any accessible path unless `include_root` is set. If the included path is constructed from a variable, an attacker who controls that variable can cause execsql to execute an arbitrary script file.
115
115
 
116
116
  ```sql
117
117
  -- Risky: included path derived from a variable
@@ -121,6 +121,18 @@ The [`INCLUDE`](metacommands.md#include) metacommand executes a script from any
121
121
 
122
122
  Validate any variable used to construct file paths before use in file-related metacommands.
123
123
 
124
+ ## Path containment roots { #path-containment-roots }
125
+
126
+ Containment roots are opt-in. When configured, paths must resolve under the configured root; attempts to escape with `../`, absolute paths, drive letters, or UNC paths are rejected.
127
+
128
+ | Setting | Applies to | Default |
129
+ | --------------- | ----------------------------------------- | ------------ |
130
+ | `include_root` | `INCLUDE` and `EXECUTE SCRIPT` targets | unrestricted |
131
+ | `serve_root` | `SERVE` targets | unrestricted |
132
+ | `template_root` | Jinja2 and `string.Template` loader paths | unrestricted |
133
+
134
+ These roots do not contain every filesystem operation. In particular, `EXPORT` and `WRITE` output paths are governed by their output-directory behavior, and remain writable anywhere the process user can write unless separately constrained by how you invoke execsql.
135
+
124
136
  ## Email (SMTP) { #smtp }
125
137
 
126
138
  TLS is off by default. The relevant `execsql.conf` settings are:
@@ -24,9 +24,9 @@ update-hooks:
24
24
 
25
25
  # ── Code Quality ──────────────────────────────────────────────────────────────
26
26
 
27
- # Lint, spell-check, and test
27
+ # Lint, type-check, and test
28
28
  [group('quality')]
29
- check: format lint test
29
+ check: format lint typecheck test
30
30
 
31
31
  # Run linter
32
32
  [group('quality')]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.21.2"
7
+ version = "2.22.0"
8
8
  description = "Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = { file = "LICENSE.txt" }
@@ -178,7 +178,7 @@ skip-magic-trailing-comma = false
178
178
  line-ending = "auto"
179
179
 
180
180
  [tool.bumpversion]
181
- current_version = "2.21.2"
181
+ current_version = "2.22.0"
182
182
  commit = true
183
183
  tag = true
184
184
  tag_name = "v{new_version}"
@@ -236,6 +236,15 @@ exclude = [
236
236
  "_execsql/",
237
237
  ]
238
238
 
239
+ [[tool.mypy.overrides]]
240
+ # numpy is only reached transitively (via polars and duckdb's stubs), and
241
+ # numpy >= 2.5 stubs use PEP 695 `type` statements that fail to parse under
242
+ # python_version 3.10. follow_imports_for_stubs is required for the skip to
243
+ # apply to numpy's .pyi files.
244
+ module = ["numpy", "numpy.*"]
245
+ follow_imports = "skip"
246
+ follow_imports_for_stubs = true
247
+
239
248
  [tool.typos.default.extend-words]
240
249
  # SQL table alias used throughout upsert templates
241
250
  ot = "ot"