execsql2 2.31.2__tar.gz → 2.32.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 (506) hide show
  1. {execsql2-2.31.2 → execsql2-2.32.0}/CHANGELOG.md +13 -0
  2. {execsql2-2.31.2 → execsql2-2.32.0}/PKG-INFO +2 -2
  3. {execsql2-2.31.2 → execsql2-2.32.0}/README.md +1 -1
  4. {execsql2-2.31.2 → execsql2-2.32.0}/docs/about/divergence.md +2 -0
  5. {execsql2-2.31.2 → execsql2-2.32.0}/docs/dev/adding_db_adapters.md +1 -0
  6. {execsql2-2.31.2 → execsql2-2.32.0}/docs/dev/architecture.md +1 -1
  7. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/formatter.md +1 -1
  8. {execsql2-2.31.2 → execsql2-2.32.0}/docs/reference/lint.md +1 -1
  9. {execsql2-2.31.2 → execsql2-2.32.0}/extras/vscode-execsql/package-lock.json +2 -2
  10. {execsql2-2.31.2 → execsql2-2.32.0}/extras/vscode-execsql/package.json +1 -1
  11. {execsql2-2.31.2 → execsql2-2.32.0}/pyproject.toml +2 -2
  12. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/base.py +19 -1
  13. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/duckdb.py +60 -1
  14. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/duckdb.py +2 -10
  15. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/models.py +73 -48
  16. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/types.py +14 -0
  17. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/datetime.py +16 -2
  18. execsql2-2.32.0/tests/db/test_bulk_inserts.py +169 -0
  19. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_postgres_inprocess.py +58 -0
  20. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_models.py +22 -0
  21. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_datetime.py +31 -0
  22. {execsql2-2.31.2 → execsql2-2.32.0}/uv.lock +1 -1
  23. {execsql2-2.31.2 → execsql2-2.32.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  24. {execsql2-2.31.2 → execsql2-2.32.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  25. {execsql2-2.31.2 → execsql2-2.32.0}/.github/dependabot.yml +0 -0
  26. {execsql2-2.31.2 → execsql2-2.32.0}/.github/workflows/ci-cd.yml +0 -0
  27. {execsql2-2.31.2 → execsql2-2.32.0}/.gitignore +0 -0
  28. {execsql2-2.31.2 → execsql2-2.32.0}/.pre-commit-config.yaml +0 -0
  29. {execsql2-2.31.2 → execsql2-2.32.0}/.pre-commit-hooks.yaml +0 -0
  30. {execsql2-2.31.2 → execsql2-2.32.0}/.python-version +0 -0
  31. {execsql2-2.31.2 → execsql2-2.32.0}/.readthedocs.yaml +0 -0
  32. {execsql2-2.31.2 → execsql2-2.32.0}/CONTRIBUTING.md +0 -0
  33. {execsql2-2.31.2 → execsql2-2.32.0}/LICENSE.txt +0 -0
  34. {execsql2-2.31.2 → execsql2-2.32.0}/NOTICE +0 -0
  35. {execsql2-2.31.2 → execsql2-2.32.0}/SECURITY.md +0 -0
  36. {execsql2-2.31.2 → execsql2-2.32.0}/docs/about/contributors.md +0 -0
  37. {execsql2-2.31.2 → execsql2-2.32.0}/docs/about/copyright.md +0 -0
  38. {execsql2-2.31.2 → execsql2-2.32.0}/docs/api/cli.md +0 -0
  39. {execsql2-2.31.2 → execsql2-2.32.0}/docs/api/db.md +0 -0
  40. {execsql2-2.31.2 → execsql2-2.32.0}/docs/api/exporters.md +0 -0
  41. {execsql2-2.31.2 → execsql2-2.32.0}/docs/api/importers.md +0 -0
  42. {execsql2-2.31.2 → execsql2-2.32.0}/docs/api/index.md +0 -0
  43. {execsql2-2.31.2 → execsql2-2.32.0}/docs/api/metacommands.md +0 -0
  44. {execsql2-2.31.2 → execsql2-2.32.0}/docs/dev/adding_exporters.md +0 -0
  45. {execsql2-2.31.2 → execsql2-2.32.0}/docs/dev/adding_importers.md +0 -0
  46. {execsql2-2.31.2 → execsql2-2.32.0}/docs/dev/adding_metacommands.md +0 -0
  47. {execsql2-2.31.2 → execsql2-2.32.0}/docs/dev/releasing.md +0 -0
  48. {execsql2-2.31.2 → execsql2-2.32.0}/docs/getting-started/installation.md +0 -0
  49. {execsql2-2.31.2 → execsql2-2.32.0}/docs/getting-started/requirements.md +0 -0
  50. {execsql2-2.31.2 → execsql2-2.32.0}/docs/getting-started/syntax.md +0 -0
  51. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/debugging.md +0 -0
  52. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/documentation.md +0 -0
  53. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/editors.md +0 -0
  54. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/encoding.md +0 -0
  55. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/examples.md +0 -0
  56. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/logging.md +0 -0
  57. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/sql_syntax.md +0 -0
  58. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/usage.md +0 -0
  59. {execsql2-2.31.2 → execsql2-2.32.0}/docs/guides/using_scripts.md +0 -0
  60. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/Compare_planets.png +0 -0
  61. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/actions.png +0 -0
  62. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/actions2.png +0 -0
  63. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/checkboxes.png +0 -0
  64. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/connect.png +0 -0
  65. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/data_error1_screenshot.jpg +0 -0
  66. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/execsql_logo_01.png +0 -0
  67. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/fatals.png +0 -0
  68. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/pause_terminal_sm.png +0 -0
  69. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/set_build_commands.jpg +0 -0
  70. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/unit_conversions_029.png +0 -0
  71. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/unmatched.png +0 -0
  72. {execsql2-2.31.2 → execsql2-2.32.0}/docs/images/vim_execsql_highlight.png +0 -0
  73. {execsql2-2.31.2 → execsql2-2.32.0}/docs/index.md +0 -0
  74. {execsql2-2.31.2 → execsql2-2.32.0}/docs/reference/configuration.md +0 -0
  75. {execsql2-2.31.2 → execsql2-2.32.0}/docs/reference/metacommands.md +0 -0
  76. {execsql2-2.31.2 → execsql2-2.32.0}/docs/reference/security.md +0 -0
  77. {execsql2-2.31.2 → execsql2-2.32.0}/docs/reference/substitution_vars.md +0 -0
  78. {execsql2-2.31.2 → execsql2-2.32.0}/docs/templates/compare.md +0 -0
  79. {execsql2-2.31.2 → execsql2-2.32.0}/docs/templates/glossary.md +0 -0
  80. {execsql2-2.31.2 → execsql2-2.32.0}/docs/templates/index.md +0 -0
  81. {execsql2-2.31.2 → execsql2-2.32.0}/docs/templates/upsert.md +0 -0
  82. {execsql2-2.31.2 → execsql2-2.32.0}/extras/plugin-template/README.md +0 -0
  83. {execsql2-2.31.2 → execsql2-2.32.0}/extras/plugin-template/pyproject.toml +0 -0
  84. {execsql2-2.31.2 → execsql2-2.32.0}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  85. {execsql2-2.31.2 → execsql2-2.32.0}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  86. {execsql2-2.31.2 → execsql2-2.32.0}/extras/vscode-execsql/.vscodeignore +0 -0
  87. {execsql2-2.31.2 → execsql2-2.32.0}/extras/vscode-execsql/LICENSE.txt +0 -0
  88. {execsql2-2.31.2 → execsql2-2.32.0}/extras/vscode-execsql/README.md +0 -0
  89. {execsql2-2.31.2 → execsql2-2.32.0}/extras/vscode-execsql/extension.js +0 -0
  90. {execsql2-2.31.2 → execsql2-2.32.0}/extras/vscode-execsql/icon.png +0 -0
  91. {execsql2-2.31.2 → execsql2-2.32.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  92. {execsql2-2.31.2 → execsql2-2.32.0}/justfile +0 -0
  93. {execsql2-2.31.2 → execsql2-2.32.0}/scripts/benchmarks/README.md +0 -0
  94. {execsql2-2.31.2 → execsql2-2.32.0}/scripts/benchmarks/results/.gitignore +0 -0
  95. {execsql2-2.31.2 → execsql2-2.32.0}/scripts/benchmarks/run_benchmarks.py +0 -0
  96. {execsql2-2.31.2 → execsql2-2.32.0}/scripts/generate_vscode_grammar.py +0 -0
  97. {execsql2-2.31.2 → execsql2-2.32.0}/scripts/release_notes.py +0 -0
  98. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/__init__.py +0 -0
  99. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/__main__.py +0 -0
  100. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/api.py +0 -0
  101. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/__init__.py +0 -0
  102. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/application.py +0 -0
  103. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/commands/__init__.py +0 -0
  104. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/commands/config.py +0 -0
  105. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/commands/format.py +0 -0
  106. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/commands/init.py +0 -0
  107. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/commands/lint.py +0 -0
  108. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/commands/listing.py +0 -0
  109. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/commands/lsp.py +0 -0
  110. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/commands/run.py +0 -0
  111. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/dispatch.py +0 -0
  112. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/dsn.py +0 -0
  113. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/help.py +0 -0
  114. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/lint.py +0 -0
  115. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/options.py +0 -0
  116. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/cli/run.py +0 -0
  117. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/config.py +0 -0
  118. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/config_validation.py +0 -0
  119. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/data/__init__.py +0 -0
  120. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/data/execsql.conf.template +0 -0
  121. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/data/script_header.sql +0 -0
  122. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/__init__.py +0 -0
  123. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/access.py +0 -0
  124. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/dsn.py +0 -0
  125. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/factory.py +0 -0
  126. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/firebird.py +0 -0
  127. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/mysql.py +0 -0
  128. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/odbc.py +0 -0
  129. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/oracle.py +0 -0
  130. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/postgres.py +0 -0
  131. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/sqlite.py +0 -0
  132. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/sqlserver.py +0 -0
  133. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/db/tiers.py +0 -0
  134. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/debug/__init__.py +0 -0
  135. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/debug/repl.py +0 -0
  136. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exceptions.py +0 -0
  137. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/__init__.py +0 -0
  138. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/base.py +0 -0
  139. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/delimited.py +0 -0
  140. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/feather.py +0 -0
  141. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/html.py +0 -0
  142. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/json.py +0 -0
  143. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/latex.py +0 -0
  144. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/markdown.py +0 -0
  145. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/ods.py +0 -0
  146. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/parquet.py +0 -0
  147. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/pretty.py +0 -0
  148. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/protocol.py +0 -0
  149. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/raw.py +0 -0
  150. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/sqlite.py +0 -0
  151. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/templates.py +0 -0
  152. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/values.py +0 -0
  153. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/xls.py +0 -0
  154. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/xlsx.py +0 -0
  155. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/xml.py +0 -0
  156. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/yaml.py +0 -0
  157. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/exporters/zip.py +0 -0
  158. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/format.py +0 -0
  159. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/gui/__init__.py +0 -0
  160. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/gui/base.py +0 -0
  161. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/gui/console.py +0 -0
  162. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/gui/desktop.py +0 -0
  163. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/gui/tui.py +0 -0
  164. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/importers/__init__.py +0 -0
  165. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/importers/base.py +0 -0
  166. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/importers/csv.py +0 -0
  167. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/importers/feather.py +0 -0
  168. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/importers/json.py +0 -0
  169. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/importers/ods.py +0 -0
  170. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/importers/xls.py +0 -0
  171. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/interactive.py +0 -0
  172. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/__init__.py +0 -0
  173. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/_util.py +0 -0
  174. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/cache.py +0 -0
  175. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/code_actions.py +0 -0
  176. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/completion.py +0 -0
  177. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/diagnostics.py +0 -0
  178. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/document.py +0 -0
  179. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/folding.py +0 -0
  180. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/formatting.py +0 -0
  181. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/hover.py +0 -0
  182. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/navigation.py +0 -0
  183. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/rename.py +0 -0
  184. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/lsp/server.py +0 -0
  185. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/manifest.py +0 -0
  186. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/__init__.py +0 -0
  187. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/conditions.py +0 -0
  188. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/connect.py +0 -0
  189. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/control.py +0 -0
  190. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/data.py +0 -0
  191. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/debug.py +0 -0
  192. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/dispatch.py +0 -0
  193. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/effects.py +0 -0
  194. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/io.py +0 -0
  195. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/io_export.py +0 -0
  196. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/io_fileops.py +0 -0
  197. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/io_import.py +0 -0
  198. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/io_write.py +0 -0
  199. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/prompt.py +0 -0
  200. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/reference.py +0 -0
  201. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/script_ext.py +0 -0
  202. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/system.py +0 -0
  203. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/metacommands/upsert.py +0 -0
  204. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/parser.py +0 -0
  205. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/plugins.py +0 -0
  206. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/py.typed +0 -0
  207. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/script/__init__.py +0 -0
  208. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/script/ast.py +0 -0
  209. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/script/control.py +0 -0
  210. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/script/engine.py +0 -0
  211. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/script/executor.py +0 -0
  212. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/script/parser.py +0 -0
  213. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/script/variables.py +0 -0
  214. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/state.py +0 -0
  215. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/state.pyi +0 -0
  216. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/__init__.py +0 -0
  217. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/auth.py +0 -0
  218. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/color.py +0 -0
  219. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/crypto.py +0 -0
  220. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/errors.py +0 -0
  221. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/fileio.py +0 -0
  222. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/gui.py +0 -0
  223. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/mail.py +0 -0
  224. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/numeric.py +0 -0
  225. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/paths.py +0 -0
  226. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/regex.py +0 -0
  227. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/strings.py +0 -0
  228. {execsql2-2.31.2 → execsql2-2.32.0}/src/execsql/utils/timer.py +0 -0
  229. {execsql2-2.31.2 → execsql2-2.32.0}/templates/README.md +0 -0
  230. {execsql2-2.31.2 → execsql2-2.32.0}/templates/config_settings.sqlite +0 -0
  231. {execsql2-2.31.2 → execsql2-2.32.0}/templates/example_config_prompt.sql +0 -0
  232. {execsql2-2.31.2 → execsql2-2.32.0}/templates/make_config_db.sql +0 -0
  233. {execsql2-2.31.2 → execsql2-2.32.0}/templates/md_compare.sql +0 -0
  234. {execsql2-2.31.2 → execsql2-2.32.0}/templates/md_glossary.sql +0 -0
  235. {execsql2-2.31.2 → execsql2-2.32.0}/templates/md_upsert.sql +0 -0
  236. {execsql2-2.31.2 → execsql2-2.32.0}/templates/pg_compare.sql +0 -0
  237. {execsql2-2.31.2 → execsql2-2.32.0}/templates/pg_glossary.sql +0 -0
  238. {execsql2-2.31.2 → execsql2-2.32.0}/templates/script_template.sql +0 -0
  239. {execsql2-2.31.2 → execsql2-2.32.0}/templates/ss_compare.sql +0 -0
  240. {execsql2-2.31.2 → execsql2-2.32.0}/templates/ss_glossary.sql +0 -0
  241. {execsql2-2.31.2 → execsql2-2.32.0}/templates/ss_upsert.sql +0 -0
  242. {execsql2-2.31.2 → execsql2-2.32.0}/tests/__init__.py +0 -0
  243. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/__init__.py +0 -0
  244. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_cli.py +0 -0
  245. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_cli_e2e.py +0 -0
  246. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_cli_run.py +0 -0
  247. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_cli_surface.py +0 -0
  248. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_commands.py +0 -0
  249. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_config_validate.py +0 -0
  250. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_dispatch.py +0 -0
  251. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_error_location.py +0 -0
  252. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_init.py +0 -0
  253. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_lint.py +0 -0
  254. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_lint_fixtures.py +0 -0
  255. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_log_exit_record.py +0 -0
  256. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_manifest.py +0 -0
  257. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_ping.py +0 -0
  258. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_profile.py +0 -0
  259. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_sigterm.py +0 -0
  260. {execsql2-2.31.2 → execsql2-2.32.0}/tests/cli/test_undeclared_imports.py +0 -0
  261. {execsql2-2.31.2 → execsql2-2.32.0}/tests/conftest.py +0 -0
  262. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/formatter_shapes/comment_anchoring.sql +0 -0
  263. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/formatter_shapes/escapes_and_literals.sql +0 -0
  264. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/formatter_shapes/substitution_and_metacommands.sql +0 -0
  265. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/formatter_shapes/unparsable_sql.sql +0 -0
  266. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/lint/conditions_and_blocks.sql +0 -0
  267. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/lint/does_not_parse.sql +0 -0
  268. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/lint/every_metacommand.sql +0 -0
  269. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/lint/invalid.sql +0 -0
  270. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/ods/libreoffice.fods +0 -0
  271. {execsql2-2.31.2 → execsql2-2.32.0}/tests/data/ods/libreoffice.ods +0 -0
  272. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/__init__.py +0 -0
  273. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/conftest.py +0 -0
  274. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_access_windows.py +0 -0
  275. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_base.py +0 -0
  276. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_copy_new.py +0 -0
  277. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_db_adapters_mocked.py +0 -0
  278. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  279. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_dsn.py +0 -0
  280. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_duckdb.py +0 -0
  281. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_factory.py +0 -0
  282. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_factory_postgres_encoding.py +0 -0
  283. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_import_atomic.py +0 -0
  284. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_import_replacement_quoted.py +0 -0
  285. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_import_short_rows.py +0 -0
  286. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_import_timezones.py +0 -0
  287. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_mysql_case_folding.py +0 -0
  288. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_mysql_inprocess.py +0 -0
  289. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_mysql_trailing_blank_lines.py +0 -0
  290. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_new_table_checks.py +0 -0
  291. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_odbc_fresh_sessions.py +0 -0
  292. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_oracle_driver.py +0 -0
  293. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_postgres.py +0 -0
  294. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_quoted_substitution_roundtrip.py +0 -0
  295. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_replacement_missing_table.py +0 -0
  296. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_roundtrip_adapters.py +0 -0
  297. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_sqlite.py +0 -0
  298. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_sqlite_extra.py +0 -0
  299. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_sqlserver_driver18.py +0 -0
  300. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_sqlserver_inprocess.py +0 -0
  301. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_tiers.py +0 -0
  302. {execsql2-2.31.2 → execsql2-2.32.0}/tests/db/test_transactions.py +0 -0
  303. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/__init__.py +0 -0
  304. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/conftest.py +0 -0
  305. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_base.py +0 -0
  306. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_db.py +0 -0
  307. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_delimited.py +0 -0
  308. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  309. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_exporters.py +0 -0
  310. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_feather.py +0 -0
  311. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_html_extended.py +0 -0
  312. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_html_latex.py +0 -0
  313. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_json.py +0 -0
  314. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_json_extended.py +0 -0
  315. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_latex_extended.py +0 -0
  316. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_markdown.py +0 -0
  317. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_ods.py +0 -0
  318. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_ods_export.py +0 -0
  319. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_parquet.py +0 -0
  320. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_pretty_extended.py +0 -0
  321. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_raw_extended.py +0 -0
  322. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_columnar.py +0 -0
  323. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_dbfile.py +0 -0
  324. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_delimited.py +0 -0
  325. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_json.py +0 -0
  326. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_latex_xml.py +0 -0
  327. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_markup.py +0 -0
  328. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_ods.py +0 -0
  329. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_pretty_raw.py +0 -0
  330. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_template.py +0 -0
  331. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_values.py +0 -0
  332. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_roundtrip_xlsx.py +0 -0
  333. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  334. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_templates.py +0 -0
  335. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_templates_extended.py +0 -0
  336. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_values_extended.py +0 -0
  337. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_values_roundtrip.py +0 -0
  338. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_xls_xlsx.py +0 -0
  339. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_xlsx.py +0 -0
  340. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_xml.py +0 -0
  341. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_yaml.py +0 -0
  342. {execsql2-2.31.2 → execsql2-2.32.0}/tests/exporters/test_zip.py +0 -0
  343. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/__init__.py +0 -0
  344. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_backends.py +0 -0
  345. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_backends_extended.py +0 -0
  346. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_compare_stats.py +0 -0
  347. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_compute_row_diffs.py +0 -0
  348. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_desktop_dialogs.py +0 -0
  349. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_format_table.py +0 -0
  350. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_tui_pilot.py +0 -0
  351. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_tui_pilot_complex.py +0 -0
  352. {execsql2-2.31.2 → execsql2-2.32.0}/tests/gui/test_utils_gui_extended.py +0 -0
  353. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/__init__.py +0 -0
  354. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/conftest.py +0 -0
  355. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_base_extended.py +0 -0
  356. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_csv_blank_lines.py +0 -0
  357. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_csv_edge_cases.py +0 -0
  358. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_csv_importer.py +0 -0
  359. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_feather_importer.py +0 -0
  360. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_json_importer.py +0 -0
  361. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_ods_importer.py +0 -0
  362. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_ods_values.py +0 -0
  363. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_roundtrip_csv.py +0 -0
  364. {execsql2-2.31.2 → execsql2-2.32.0}/tests/importers/test_xls_importer.py +0 -0
  365. {execsql2-2.31.2 → execsql2-2.32.0}/tests/integration/__init__.py +0 -0
  366. {execsql2-2.31.2 → execsql2-2.32.0}/tests/integration/conftest.py +0 -0
  367. {execsql2-2.31.2 → execsql2-2.32.0}/tests/integration/test_dsn.py +0 -0
  368. {execsql2-2.31.2 → execsql2-2.32.0}/tests/integration/test_duckdb.py +0 -0
  369. {execsql2-2.31.2 → execsql2-2.32.0}/tests/integration/test_mysql.py +0 -0
  370. {execsql2-2.31.2 → execsql2-2.32.0}/tests/integration/test_postgres.py +0 -0
  371. {execsql2-2.31.2 → execsql2-2.32.0}/tests/integration/test_sqlite.py +0 -0
  372. {execsql2-2.31.2 → execsql2-2.32.0}/tests/live_db.py +0 -0
  373. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/__init__.py +0 -0
  374. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_cache.py +0 -0
  375. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_code_actions.py +0 -0
  376. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_completion.py +0 -0
  377. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_diagnostics.py +0 -0
  378. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_document.py +0 -0
  379. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_folding.py +0 -0
  380. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_formatting.py +0 -0
  381. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_hover.py +0 -0
  382. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_lsp_command.py +0 -0
  383. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_navigation.py +0 -0
  384. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_rename.py +0 -0
  385. {execsql2-2.31.2 → execsql2-2.32.0}/tests/lsp/test_server.py +0 -0
  386. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/__init__.py +0 -0
  387. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/conftest.py +0 -0
  388. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_assert.py +0 -0
  389. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_breakpoint.py +0 -0
  390. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_conditions_extra.py +0 -0
  391. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_connect.py +0 -0
  392. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_io_export.py +0 -0
  393. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_io_import.py +0 -0
  394. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands.py +0 -0
  395. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  396. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_data.py +0 -0
  397. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_extended.py +0 -0
  398. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  399. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_io.py +0 -0
  400. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  401. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  402. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_system.py +0 -0
  403. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  404. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_pg_upsert.py +0 -0
  405. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_prompt.py +0 -0
  406. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_row_count.py +0 -0
  407. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_serve_output.py +0 -0
  408. {execsql2-2.31.2 → execsql2-2.32.0}/tests/metacommands/test_show_scripts.py +0 -0
  409. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/__init__.py +0 -0
  410. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  411. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  412. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  413. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/audit_smoke.sql +0 -0
  414. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/config_runtime.sql +0 -0
  415. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/control_flow.sql +0 -0
  416. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  417. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/error_handling.sql +0 -0
  418. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  419. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/import_options.sql +0 -0
  420. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/includes/helper.sql +0 -0
  421. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/includes.sql +0 -0
  422. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/io_formats.sql +0 -0
  423. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  424. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/multi_db.sql +0 -0
  425. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  426. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  427. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/predicates.sql +0 -0
  428. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/scripts.sql +0 -0
  429. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/smoke.sql +0 -0
  430. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  431. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  432. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  433. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/template_export.sql +0 -0
  434. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/timer.sql +0 -0
  435. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/transactions.sql +0 -0
  436. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/write_create_table.sql +0 -0
  437. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  438. {execsql2-2.31.2 → execsql2-2.32.0}/tests/scripts/test_sql_scripts.py +0 -0
  439. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/__init__.py +0 -0
  440. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_env_detection.py +0 -0
  441. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_expansion_bomb.py +0 -0
  442. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_import_regex_hardening.py +0 -0
  443. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_log_redaction.py +0 -0
  444. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_log_secrets.py +0 -0
  445. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_network_paths.py +0 -0
  446. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_odbc_connection_strings.py +0 -0
  447. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_odbc_injection.py +0 -0
  448. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_path_containment.py +0 -0
  449. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_substitution_injection.py +0 -0
  450. {execsql2-2.31.2 → execsql2-2.32.0}/tests/security/test_zip_bomb.py +0 -0
  451. {execsql2-2.31.2 → execsql2-2.32.0}/tests/templates/__init__.py +0 -0
  452. {execsql2-2.31.2 → execsql2-2.32.0}/tests/templates/conftest.py +0 -0
  453. {execsql2-2.31.2 → execsql2-2.32.0}/tests/templates/test_compare.py +0 -0
  454. {execsql2-2.31.2 → execsql2-2.32.0}/tests/templates/test_glossary.py +0 -0
  455. {execsql2-2.31.2 → execsql2-2.32.0}/tests/templates/test_static.py +0 -0
  456. {execsql2-2.31.2 → execsql2-2.32.0}/tests/templates/test_upsert.py +0 -0
  457. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_api.py +0 -0
  458. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_api_errors.py +0 -0
  459. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_api_extended.py +0 -0
  460. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_api_filewriter.py +0 -0
  461. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_api_isolation.py +0 -0
  462. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_api_subprocess.py +0 -0
  463. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_ast.py +0 -0
  464. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_ast_parser.py +0 -0
  465. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_bundled_templates.py +0 -0
  466. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_config.py +0 -0
  467. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_config_data.py +0 -0
  468. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_config_extended.py +0 -0
  469. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_date_inference.py +0 -0
  470. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_debug_repl.py +0 -0
  471. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_engine.py +0 -0
  472. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_error_messages.py +0 -0
  473. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_exceptions.py +0 -0
  474. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_executor.py +0 -0
  475. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_executor_inprocess.py +0 -0
  476. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_format.py +0 -0
  477. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_interactive.py +0 -0
  478. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_mail.py +0 -0
  479. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_package.py +0 -0
  480. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_parser.py +0 -0
  481. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_parser_params.py +0 -0
  482. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_plugins.py +0 -0
  483. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_reference.py +0 -0
  484. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_registry.py +0 -0
  485. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_release_notes.py +0 -0
  486. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_script.py +0 -0
  487. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_state.py +0 -0
  488. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_tooling_versions.py +0 -0
  489. {execsql2-2.31.2 → execsql2-2.32.0}/tests/test_types.py +0 -0
  490. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/__init__.py +0 -0
  491. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_auth.py +0 -0
  492. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_auth_extra.py +0 -0
  493. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_crypto.py +0 -0
  494. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_encodedfile_context.py +0 -0
  495. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_errors.py +0 -0
  496. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_errors_extra.py +0 -0
  497. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_fileio.py +0 -0
  498. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_fileio_extra.py +0 -0
  499. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_keyring_rules.py +0 -0
  500. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_mail_tls.py +0 -0
  501. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_numeric.py +0 -0
  502. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_regex.py +0 -0
  503. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_strings.py +0 -0
  504. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_timer.py +0 -0
  505. {execsql2-2.31.2 → execsql2-2.32.0}/tests/utils/test_timer_extra.py +0 -0
  506. {execsql2-2.31.2 → execsql2-2.32.0}/zensical.toml +0 -0
@@ -13,6 +13,19 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.32.0] - 2026-10-16
17
+
18
+ ### Changed
19
+
20
+ - **Working out column types is about twice as fast** for `IMPORT`, `COPY` and the exports that create a table (`EXPORT … AS SQLITE`, `DUCKDB`, `JSON`, `FEATHER`); a CSV whose dates repeat scans close to three times as fast. The types chosen are the same.
21
+ - **`IMPORT`, `COPY` and `EXPORT … AS DUCKDB` into DuckDB are about five times as fast**: they insert many rows per statement. Copying 200,000 rows took 124 s and now takes 22 s.
22
+
23
+ ### Fixed
24
+
25
+ - **A column of large integers**, such as epoch milliseconds, no longer makes `IMPORT`, `COPY` or those exports hang or run out of memory while execsql works out the column types.
26
+
27
+ ______________________________________________________________________
28
+
16
29
  ## [2.31.2] - 2026-10-16
17
30
 
18
31
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.31.2
3
+ Version: 2.32.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/en/stable/
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -441,7 +441,7 @@ execsql format --no-sql --in-place scripts/
441
441
  ```yaml
442
442
  repos:
443
443
  - repo: https://github.com/geocoug/execsql
444
- rev: v2.31.2
444
+ rev: v2.32.0
445
445
  hooks:
446
446
  - id: execsql-format
447
447
  - id: execsql-lint
@@ -321,7 +321,7 @@ execsql format --no-sql --in-place scripts/
321
321
  ```yaml
322
322
  repos:
323
323
  - repo: https://github.com/geocoug/execsql
324
- rev: v2.31.2
324
+ rev: v2.32.0
325
325
  hooks:
326
326
  - id: execsql-format
327
327
  - id: execsql-lint
@@ -264,6 +264,7 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
264
264
  - **PostgreSQL driver is psycopg3** — the PostgreSQL adapter uses `psycopg` (psycopg3, installed via the `psycopg[binary]` extra) instead of upstream's `psycopg2`. The connection now passes the libpq `dbname` keyword (psycopg3 rejects psycopg2's `database`), `VACUUM` toggles `conn.autocommit` instead of the removed `set_session()`, server-side `COPY` import uses psycopg3's `cursor.copy()` context manager instead of `copy_expert()`, and binary `IMPORT` sends raw `bytes` instead of `psycopg2.Binary()`. This also makes `db.conn` directly usable by the `PG_UPSERT` metacommand's pg-upsert ≥1.26.0 dependency, which itself requires a psycopg3 connection. The connection sets `prepare_threshold=None` to disable psycopg3's automatic server-side prepared statements, which psycopg2 never used; without this, re-running a query (e.g. via `EXPORT`) against an object the script drops/recreates or alters raises `FeatureNotSupported: cached plan must not change result type`.
265
265
  - **Oracle driver is python-oracledb** — the Oracle adapter imports `oracledb`, the driver the `oracle` extra installs, instead of upstream's `cx_Oracle` (which ships no wheels for Python 3.12+). The `makedsn` / `connect` calls are the same. `cx_Oracle` is still used when it is the only Oracle driver installed.
266
266
  - **SQL Server tries ODBC Driver 18 first** — upstream's driver list stopped at Driver 17, so a machine with only Microsoft's current driver could not connect. Driver 18 encrypts the connection and verifies the server's certificate; the `trust_server_certificate` setting accepts a self-signed one.
267
+ - **DuckDB bulk inserts** — `IMPORT`, `COPY` and `EXPORT ... AS DUCKDB` insert many rows per `INSERT ... VALUES` statement instead of `executemany()`, which DuckDB runs one row at a time; the values stored are the same. While binding them, and only when pandas is not installed, `sys.modules["pandas"]` is `None`: the duckdb package looks pandas up twice per row, and without that entry each lookup searched `sys.path`.
267
268
  - **Each pyodbc connection is a new session** — the SQL Server, Access and ODBC DSN adapters set `pyodbc.pooling = False` before they connect. With pyodbc's default pooling, unixODBC (macOS, Linux) handed a closed connection's session unchanged to the next connection to the same server, so temp tables and `SET` options from one `CONNECT`, or one `execsql.run()`, were still there in the next. pyodbc applies the setting at the process's first pyodbc connection, so it also covers other pyodbc connections the process makes, and has no effect if the program connected with pyodbc before execsql did.
268
269
  - **`Database` is an ABC** — `open_db()` and `exec_cmd()` are abstract methods. Subclasses that omit them raise `TypeError` at instantiation instead of at call time.
269
270
  - **Connection timeouts** — PostgreSQL and SQLite adapters accept a connection timeout parameter (default 30 seconds).
@@ -434,6 +435,7 @@ These are behavioral changes driven by security or correctness issues in the ups
434
435
  | ODS export replaced newlines inside a value with spaces | String cells were written after `item.replace("\n", " ")`, so an exported value silently differed from the one in the database. An XML parser does normalise a newline inside an attribute, so `office:string-value` cannot carry one — but ODF represents a multi-line cell as one `<text:p>` per line, which is what LibreOffice writes and what execsql2 now writes. Single-line values still use the attribute. |
435
436
  | XLSX export wrote `numeric` columns as text | `_cell_value()` passed `int` and `float` straight to openpyxl but let every other type fall through to `str()`. PostgreSQL, MySQL, and DuckDB all return `Decimal` for `numeric`/`decimal` columns, so measurement values arrived in Excel as text cells — invisible in a screenshot, useless in a formula, a sort, or a chart. openpyxl lists `Decimal` in its own `NUMERIC_TYPES` and stores it as a real numeric cell, so the conversion was never needed. Found by exporting from a live PostgreSQL container and checking `cell.data_type` rather than `cell.value`. |
436
437
  | `EMAIL` SMTP login without a password | Upstream calls `smtplib.login()` with only a username when `[email] password` and `enc_password` are both unset, which raises `TypeError` from the stdlib — `login()` has no single-argument form. execsql2 reports a configuration error naming the missing password instead. |
438
+ | Type scan of large integers | Working out a column's type tested each integer against the binary type by building `bytearray(n)`, n zero bytes, so a column of large integers (epoch milliseconds: about a terabyte per value) hung or ran out of memory. The binary test now answers for an integer without building it; the inferred types are unchanged. |
437
439
 
438
440
  ______________________________________________________________________
439
441
 
@@ -148,6 +148,7 @@ These are the instance attributes and methods you must configure correctly:
148
148
  | `schema_exists()` | method | **Must override.** Return `False` if schemas are not supported. |
149
149
  | `drop_table()` | method | **Must override.** Drop a table (used by IMPORT when creating fresh). |
150
150
  | `populate_table()` | method | **Must override.** Bulk-load rows from a generator (used by IMPORT). |
151
+ | `row_inserter()` | method | Override for a faster bulk path that stores exactly what `executemany()` stores. |
151
152
  | `exec_cmd()` | method | Override if the DBMS can execute stored procedures or views as commands. |
152
153
 
153
154
  Methods inherited from `Database` that you get for free include `execute()`, `cursor()`, `close()`, `commit()`, `rollback()`, `select_rowsource()`, `select_data()`, `schema_qualified_table_name()`, `quote_identifier()`, and `paramsubs()`.
@@ -297,7 +297,7 @@ ______________________________________________________________________
297
297
  1. An `IMPORT` metacommand is dispatched to the appropriate handler.
298
298
  1. The handler instantiates the correct importer (e.g., `importers/csv.py`, `importers/ods.py`) based on the file type.
299
299
  1. The importer reads the file and yields rows.
300
- 1. `Database.populate_table()` bulk-inserts the rows using `executemany()`, with optional progress bars.
300
+ 1. `Database.populate_table()` converts the rows and inserts them in batches of `import_row_buffer` rows, in one transaction, with optional progress bars. Each batch goes through the adapter's `row_inserter()`: `executemany()` by default, and multi-row `INSERT ... VALUES` on DuckDB, whose `executemany()` binds one row at a time.
301
301
 
302
302
  ### Export metadata
303
303
 
@@ -272,7 +272,7 @@ select id,name,created_at from users where active = true order by name;
272
272
  ```yaml
273
273
  repos:
274
274
  - repo: https://github.com/geocoug/execsql
275
- rev: v2.31.2
275
+ rev: v2.32.0
276
276
  hooks:
277
277
  - id: execsql-format
278
278
  - id: execsql-lint # optional: fail the commit on lint errors
@@ -158,7 +158,7 @@ The `execsql-lint` [pre-commit](https://pre-commit.com/) hook runs `execsql lint
158
158
  ```yaml
159
159
  repos:
160
160
  - repo: https://github.com/geocoug/execsql
161
- rev: v2.31.2
161
+ rev: v2.32.0
162
162
  hooks:
163
163
  - id: execsql-lint
164
164
  args: [--ignore, V002] # optional: any execsql lint option
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "execsql-syntax",
3
- "version": "2.31.2",
3
+ "version": "2.32.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "execsql-syntax",
9
- "version": "2.31.2",
9
+ "version": "2.32.0",
10
10
  "license": "GPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "vscode-languageclient": "10.1.2"
@@ -2,7 +2,7 @@
2
2
  "name": "execsql-syntax",
3
3
  "displayName": "execsql",
4
4
  "description": "Syntax highlighting, lint findings, completion, hover, navigation, quick fixes and formatting for execsql scripts.",
5
- "version": "2.31.2",
5
+ "version": "2.32.0",
6
6
  "publisher": "geocoug",
7
7
  "license": "GPL-3.0-or-later",
8
8
  "icon": "icon.png",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.31.2"
7
+ version = "2.32.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" }
@@ -188,7 +188,7 @@ line-ending = "auto"
188
188
  exclude = ["*.md"]
189
189
 
190
190
  [tool.bumpversion]
191
- current_version = "2.31.2"
191
+ current_version = "2.32.0"
192
192
  commit = true
193
193
  tag = true
194
194
  tag_name = "v{new_version}"
@@ -665,6 +665,23 @@ class Database(ABC):
665
665
  self.execute(f"drop table if exists {tablename} cascade;")
666
666
  self.commit()
667
667
 
668
+ def row_inserter(self, curs: Any, table: str, columns: list[str]) -> Callable[[list[list[Any]]], None]:
669
+ """Return a function that inserts one batch of rows into *table* through *curs*.
670
+
671
+ *table* is schema-qualified and quoted; *columns* are the quoted
672
+ column names, in the order of each row's values. IMPORT and COPY call
673
+ the function once per batch, all inside one transaction. This one
674
+ runs ``executemany``. An adapter may override it with a faster bulk
675
+ path only if that path stores exactly what ``executemany`` stores, for
676
+ every value type and column type: a faster load must not change data.
677
+ """
678
+ sql = f"insert into {table} ({','.join(columns)}) values ({self.paramsubs(len(columns))});"
679
+
680
+ def insert(rows: list[list[Any]]) -> None:
681
+ curs.executemany(sql, rows)
682
+
683
+ return insert
684
+
668
685
  def populate_table(
669
686
  self,
670
687
  schema_name: str | None,
@@ -740,6 +757,7 @@ class Database(ABC):
740
757
 
741
758
  def _import_loop(curs) -> int:
742
759
  nonlocal eof, total_rows, task_id
760
+ insert = self.row_inserter(curs, sq_name, columns)
743
761
  while True:
744
762
  b = []
745
763
  for _j in range(_state.conf.import_row_buffer):
@@ -809,7 +827,7 @@ class Database(ABC):
809
827
  b.append(row)
810
828
  if len(b) > 0:
811
829
  try:
812
- curs.executemany(sql, b)
830
+ insert(b)
813
831
  except ErrInfo:
814
832
  raise
815
833
  except Exception as e:
@@ -8,8 +8,12 @@ analytics databases via the ``duckdb`` package. Corresponds to ``-t k``
8
8
  on the CLI.
9
9
  """
10
10
 
11
+ import importlib.util
11
12
  import re
13
+ import sys
12
14
  from collections import deque
15
+ from collections.abc import Callable, Iterator
16
+ from contextlib import contextmanager
13
17
  from pathlib import Path
14
18
  from typing import Any
15
19
 
@@ -18,7 +22,7 @@ from execsql.exceptions import ErrInfo
18
22
  from execsql.utils.errors import exception_desc, fatal_error, install_hint
19
23
  import execsql.state as _state
20
24
 
21
- __all__ = ["DuckDBDatabase"]
25
+ __all__ = ["DuckDBDatabase", "insert_rows"]
22
26
 
23
27
  # Statements that return rows; any other statement DuckDB answers with a
24
28
  # one-column "Count" result (rows inserted, updated or deleted).
@@ -28,6 +32,53 @@ _QUERY_RX = re.compile(r"(?:SELECT|WITH|VALUES|FROM|TABLE|SHOW|DESCRIBE|SUMMARIZ
28
32
  _TRANSACTION_RX = re.compile(r"(?P<begin>BEGIN|START\s+TRANSACTION)\b|(?P<end>COMMIT|END|ROLLBACK|ABORT)\b", re.I)
29
33
 
30
34
 
35
+ # Parameters bound by one INSERT ... VALUES statement in insert_rows(); a
36
+ # wide table gets fewer rows per statement.
37
+ _PARAMS_PER_STATEMENT = 10_000
38
+
39
+
40
+ @contextmanager
41
+ def _pandas_lookup_skipped() -> Iterator[None]:
42
+ """Make DuckDB's check for pandas fail at once while it binds rows.
43
+
44
+ The duckdb package asks Python's import system for ``pandas`` twice for
45
+ every row it binds, and when pandas is not installed nothing remembers
46
+ the answer, so each row searches ``sys.path`` again: about three
47
+ quarters of an INSERT's time. A ``None`` in ``sys.modules`` makes that
48
+ import fail without the search. It is set only when pandas is not
49
+ installed, so no import that could have succeeded fails, and it is
50
+ removed afterwards.
51
+ """
52
+ if "pandas" in sys.modules or importlib.util.find_spec("pandas") is not None:
53
+ yield
54
+ return
55
+ sys.modules["pandas"] = None # type: ignore[assignment]
56
+ try:
57
+ yield
58
+ finally:
59
+ if "pandas" in sys.modules and sys.modules["pandas"] is None:
60
+ del sys.modules["pandas"]
61
+
62
+
63
+ def insert_rows(curs: Any, table: str, columns: list[str], rows: list[Any]) -> None:
64
+ """Insert *rows* into *table* with multi-row ``INSERT ... VALUES`` statements.
65
+
66
+ *table* is quoted (and schema-qualified as needed); *columns* are the
67
+ quoted column names, in the order of each row's values. DuckDB's
68
+ ``executemany`` runs one row at a time; binding many rows in one
69
+ statement is several times faster and stores the same values.
70
+ """
71
+ if not rows:
72
+ return
73
+ per_statement = max(1, _PARAMS_PER_STATEMENT // max(1, len(columns)))
74
+ head = f"insert into {table} ({','.join(columns)}) values "
75
+ row_marks = "(" + ",".join("?" * len(columns)) + ")"
76
+ with _pandas_lookup_skipped():
77
+ for start in range(0, len(rows), per_statement):
78
+ chunk = rows[start : start + per_statement]
79
+ curs.execute(head + ",".join([row_marks] * len(chunk)), [value for row in chunk for value in row])
80
+
81
+
31
82
  class _SharedCursor:
32
83
  """A cursor that runs on the database's one connection.
33
84
 
@@ -261,6 +312,14 @@ class DuckDBDatabase(Database):
261
312
  curs.execute(sql, params)
262
313
  return len(curs.fetchall()) > 0
263
314
 
315
+ def row_inserter(self, curs: Any, table: str, columns: list[str]) -> Callable[[list[list[Any]]], None]:
316
+ """Insert each batch with multi-row ``INSERT ... VALUES`` (:func:`insert_rows`)."""
317
+
318
+ def insert(rows: list[list[Any]]) -> None:
319
+ insert_rows(curs, table, columns, rows)
320
+
321
+ return insert
322
+
264
323
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
265
324
  """Return True if the named view exists in the DuckDB database."""
266
325
  # DuckDB information_schema has no 'views' table; views are listed in 'tables'
@@ -10,7 +10,6 @@ Requires the ``execsql2[duckdb]`` extra.
10
10
 
11
11
  import datetime as _datetime
12
12
  import decimal as _decimal
13
- import math
14
13
  from pathlib import Path
15
14
  from typing import Any
16
15
 
@@ -38,9 +37,9 @@ def export_duckdb(
38
37
  fatal_error(f"The duckdb module is required to export data in that format. {install_hint('duckdb')}")
39
38
  return
40
39
 
40
+ from execsql.db.duckdb import insert_rows
41
41
  from execsql.models import DataTable
42
42
 
43
- chunksize = 10000
44
43
  # B07a/F026: tablename and catalog are substituted from user input.
45
44
  # Identifier-quote the table-name interpolation site and use
46
45
  # placeholders for the information_schema literal-value query.
@@ -70,15 +69,8 @@ def export_duckdb(
70
69
  curs.execute(sql)
71
70
  # Export all rows of data
72
71
  columns = [dbt_duckdb.quoted(col) for col in hdrs]
73
- colspec = ",".join(columns)
74
- paramspec = ",".join(("?",) * len(columns))
75
- sql = f"insert into {qtable} ({colspec}) values ({paramspec});"
76
- n_chunks = math.ceil(len(rowdata) / chunksize)
77
72
  curs.execute("BEGIN TRANSACTION;")
78
- for i in range(n_chunks):
79
- start = i * chunksize
80
- end = start + chunksize
81
- curs.executemany(sql, rowdata[start:end])
73
+ insert_rows(curs, qtable, columns, rowdata)
82
74
  curs.execute("COMMIT;")
83
75
  curs.close()
84
76
  ddb.close()
@@ -50,6 +50,16 @@ __all__ = [
50
50
  ColumnType = tuple[str, type[DataType], int | None, bool, int | None, int | None]
51
51
 
52
52
 
53
+ def _text_length(value: Any) -> int:
54
+ """Return the length of *value* written as text (a column may turn out to be text)."""
55
+ if isinstance(value, str):
56
+ return len(value)
57
+ try:
58
+ return len(str(value))
59
+ except Exception:
60
+ return len(bytes(value))
61
+
62
+
53
63
  class Column:
54
64
  """Compile data-type match statistics for a single column of imported data."""
55
65
 
@@ -84,40 +94,39 @@ class Column:
84
94
  """Test whether a non-null value matches this data type and update statistics."""
85
95
  # datavalue must be non-null
86
96
  if not self.failed:
87
- is_match = self.dt.matches(datavalue)
88
- if is_match:
89
- self.count += 1
90
- if isinstance(datavalue, str):
91
- vlen = len(datavalue)
92
- else:
93
- # This column may turn out to have to be text, so we need the maximum length
94
- # of any data value when represented as text.
95
- try:
96
- vlen = len(str(datavalue))
97
- except Exception:
98
- vlen = len(bytes(datavalue))
99
- if self.maxlen > 0 and vlen != self.maxlen:
100
- self.varlen = True
101
- if vlen > self.maxlen:
102
- self.maxlen = vlen
103
- if self.dt.precision is not None and self.dt.scale is not None:
104
- int_digits = self.dt.precision - self.dt.scale
105
- if self.maxprecision is None:
106
- self.maxprecision = self.dt.precision
107
- self.scale = self.dt.scale
108
- self.maxscale = self.dt.scale
109
- self.max_int_digits = int_digits
110
- else:
111
- assert self.maxscale is not None
112
- assert self.max_int_digits is not None
113
- self.maxprecision = max(self.dt.precision, self.maxprecision)
114
- if self.dt.scale != self.scale:
115
- self.varscale = True
116
- self.maxscale = max(self.dt.scale, self.maxscale)
117
- self.max_int_digits = max(int_digits, self.max_int_digits)
97
+ if self.dt.matches(datavalue):
98
+ self.record(_text_length(datavalue))
118
99
  else:
119
100
  self.failed = True
120
101
 
102
+ def record(self, vlen: int) -> None:
103
+ """Count a value this data type matched, *vlen* characters long as text.
104
+
105
+ Call it right after :meth:`DataType.matches` accepted the value:
106
+ a decimal type's precision and scale are those of the value it
107
+ last matched.
108
+ """
109
+ self.count += 1
110
+ if self.maxlen > 0 and vlen != self.maxlen:
111
+ self.varlen = True
112
+ if vlen > self.maxlen:
113
+ self.maxlen = vlen
114
+ if self.dt.precision is not None and self.dt.scale is not None:
115
+ int_digits = self.dt.precision - self.dt.scale
116
+ if self.maxprecision is None:
117
+ self.maxprecision = self.dt.precision
118
+ self.scale = self.dt.scale
119
+ self.maxscale = self.dt.scale
120
+ self.max_int_digits = int_digits
121
+ else:
122
+ assert self.maxscale is not None
123
+ assert self.max_int_digits is not None
124
+ self.maxprecision = max(self.dt.precision, self.maxprecision)
125
+ if self.dt.scale != self.scale:
126
+ self.varscale = True
127
+ self.maxscale = max(self.dt.scale, self.maxscale)
128
+ self.max_int_digits = max(int_digits, self.max_int_digits)
129
+
121
130
  def __init__(self, colname: str, infer_strings: bool = True) -> None:
122
131
  """Create a column characteriser for the named column.
123
132
 
@@ -177,6 +186,15 @@ class Column:
177
186
  self.str_accums = tuple(ac for ac in self.accums if isinstance(ac.dt, (DT_Character, DT_Varchar, DT_Text)))
178
187
  # A bytes value is checked as binary or text only: int(b"12") is 12.
179
188
  self.bytes_accums = tuple(ac for ac in self.accums if isinstance(ac.dt, DT_Binary)) + self.str_accums
189
+ # A value is tried only against the types that matched every value
190
+ # before it: a type that failed once cannot be the column's type.
191
+ self._live = list(self.accums)
192
+ self._live_str = list(self.str_accums)
193
+ self._live_bytes = list(self.bytes_accums)
194
+ # The settings the scan reads for every value, read once.
195
+ self._trim_strings = conf.trim_strings
196
+ self._replace_newlines = conf.replace_newlines
197
+ self._empty_strings = conf.empty_strings
180
198
  self.dt_eval = False
181
199
  # self.dt is a tuple of: 0: column name; 1: data type class; 2: max length or None;
182
200
  # 3: bool indicating any null values; 4: precision or None; 5: scale or None.
@@ -187,31 +205,38 @@ class Column:
187
205
 
188
206
  def eval_types(self, column_value: Any) -> None:
189
207
  """Evaluate which data types the value matches and update counters."""
190
- # Evaluate which data type(s) the value matches, and increment the appropriate counter(s).
191
- import execsql.state as _state
192
-
193
208
  self.dt_eval = False
194
209
  self.rowcount += 1
195
- conf = _state.conf
196
- if column_value is not None and isinstance(column_value, str):
197
- if conf.trim_strings:
210
+ is_str = isinstance(column_value, str)
211
+ if is_str:
212
+ if self._trim_strings:
198
213
  column_value = column_value.strip()
199
- if conf.replace_newlines:
214
+ if self._replace_newlines:
200
215
  column_value = re.sub(r"[\s\t]*[\r\n]+[\s\t]*", " ", column_value)
201
- if column_value is None or (
202
- not conf.empty_strings and isinstance(column_value, str) and len(column_value.strip()) == 0
203
- ):
216
+ if column_value is None or (not self._empty_strings and is_str and len(column_value.strip()) == 0):
204
217
  self.nullrows += 1
205
218
  return
206
- if not self.infer_strings and isinstance(column_value, str):
207
- for dt in self.str_accums:
208
- dt.check(column_value)
219
+ if not self.infer_strings and is_str:
220
+ live = self._live_str
209
221
  elif not self.infer_strings and isinstance(column_value, (bytes, bytearray, memoryview)):
210
- for dt in self.bytes_accums:
211
- dt.check(column_value)
222
+ live = self._live_bytes
212
223
  else:
213
- for dt in self.accums:
214
- dt.check(column_value)
224
+ live = self._live
225
+ vlen = None
226
+ any_failed = False
227
+ # The value is not NULL, the one case DataType.matches() adds to _is_match().
228
+ for ac in live:
229
+ if ac.dt._is_match(column_value):
230
+ if vlen is None:
231
+ vlen = _text_length(column_value)
232
+ ac.record(vlen)
233
+ else:
234
+ ac.failed = True
235
+ any_failed = True
236
+ if any_failed:
237
+ self._live = [ac for ac in self._live if not ac.failed]
238
+ self._live_str = [ac for ac in self._live_str if not ac.failed]
239
+ self._live_bytes = [ac for ac in self._live_bytes if not ac.failed]
215
240
 
216
241
  def column_type(self) -> ColumnType:
217
242
  """Return the inferred type of this column as a 6-tuple."""
@@ -550,6 +550,8 @@ class DT_Character(DataType):
550
550
  return "DT_Character()"
551
551
 
552
552
  def _is_match(self, data: object) -> bool:
553
+ if isinstance(data, str): # what _from_data() returns a str as, unchanged
554
+ return True
553
555
  if isinstance(data, bytearray):
554
556
  return False
555
557
  return super()._is_match(data)
@@ -582,6 +584,8 @@ class DT_Varchar(DataType):
582
584
  return "DT_Varchar()"
583
585
 
584
586
  def _is_match(self, data: object) -> bool:
587
+ if isinstance(data, str): # the length check _from_data() makes
588
+ return len(data) <= 255
585
589
  if isinstance(data, bytearray):
586
590
  return False
587
591
  return super()._is_match(data)
@@ -609,6 +613,8 @@ class DT_Text(DataType):
609
613
  return "DT_Text()"
610
614
 
611
615
  def _is_match(self, data: object) -> bool:
616
+ if isinstance(data, str): # what _from_data() returns a str as, unchanged
617
+ return True
612
618
  if isinstance(data, bytearray):
613
619
  return False
614
620
  return super()._is_match(data)
@@ -634,6 +640,14 @@ class DT_Binary(DataType):
634
640
  def __repr__(self) -> str:
635
641
  return "DT_Binary()"
636
642
 
643
+ def _is_match(self, data: object) -> bool:
644
+ # bytearray(n) is n zero bytes, so an int converts when it is not
645
+ # negative. Building it to find out allocated n bytes per value: a
646
+ # column of epoch milliseconds asked for a terabyte each.
647
+ if isinstance(data, int):
648
+ return data >= 0
649
+ return super()._is_match(data)
650
+
637
651
 
638
652
  class DbType:
639
653
  """Map Python DataType subclasses to DBMS-specific SQL type names."""
@@ -14,7 +14,9 @@ today's date. ``1-2``, ``5/6`` and ``March 5`` are not timestamps.
14
14
  """
15
15
 
16
16
  import datetime
17
+ import functools
17
18
  import re
19
+ import time
18
20
  from typing import Any, cast
19
21
 
20
22
  from dateutil import parser as _dateutil_parser
@@ -58,9 +60,21 @@ _DEFAULT_B = datetime.datetime(2002, 2, 2)
58
60
 
59
61
  def _parse(datestr: str, strict: bool) -> datetime.datetime | None:
60
62
  """Parse *datestr* with dateutil; when *strict*, only a fully written date."""
61
- try:
62
- if not strict:
63
+ if not strict:
64
+ try:
63
65
  return cast(datetime.datetime, _dateutil_parser.parse(datestr))
66
+ except (ValueError, OverflowError, TypeError):
67
+ return None
68
+ # A strict parse depends only on the string and on the local zone names
69
+ # dateutil matches a written zone against, so repeated values (dates
70
+ # recur, and type inference parses each one for both timestamp types)
71
+ # are parsed once.
72
+ return _parse_strict(datestr, time.tzname)
73
+
74
+
75
+ @functools.lru_cache(maxsize=4096)
76
+ def _parse_strict(datestr: str, _local_zone_names: tuple[str, str]) -> datetime.datetime | None:
77
+ try:
64
78
  if not _FULL_YEAR.search(datestr):
65
79
  short = _SHORT_YEAR.match(datestr)
66
80
  # Month first only: dateutil would read 13-05-24 day-first,